Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AdvancedCoop/CoopAdvancedHardening.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ public static void ReceiveLobbyState(string payload)
if (string.IsNullOrWhiteSpace(payload))
return;

// Current build uses this mainly as a live-room heartbeat. The menu/connection UI already reads NetNode.HasRemote;
// receiving this packet marks the remote alive in NetNode. Keep parsing intentionally loose for forward compatibility.
// Live-room heartbeat only. Username updates are applied when the value actually changes
// (ReceiveRemoteUsername is change-gated) so this path must not spam logs/UI refreshes.
try
{
var parts = payload.Split('|');
Expand Down
206 changes: 196 additions & 10 deletions GameDataSync/GameDataSync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ internal partial class GameDataSync : IEventReceiver, IOnAdvancedModuleInitializ
static public bool _mode;

static public LaunchMode _launch = default!;
private static readonly object _sameRunRestartSync = new();
private static bool _sameRunRestartPending;
private static int _sameRunRestartSeed;
private static readonly object _bossRuneLock = new();
private static int? _remoteBossRune;
private static int? _hostBossRune;
Expand Down Expand Up @@ -143,35 +146,75 @@ public static void user_hook_new_game(Hook_User.orig_newGame orig,
bool mode,
LaunchMode gdata)
{
isCustom = false;
mode = false;
Seed = lvl;
var sameRunRestart = TryPeekSameRunRestartSeed(out var restartSeed);
var effectiveStreamEnabled = isCustom;
var effectiveCustomMode = mode;
var effectiveLaunch = gdata;
if (gdata is LaunchMode.NewGame newGame)
{
if (sameRunRestart)
{
effectiveCustomMode = ResolveCurrentRunIsCustom();
effectiveStreamEnabled = ResolveCurrentRunStreamEnabled();
effectiveLaunch = new LaunchMode.NewGame(effectiveCustomMode, effectiveStreamEnabled);
}
else if (GameMenu.TryGetAuthoritativePendingNewGameLaunch(out var selectedCustom, out var selectedStreamEnabled))
{
effectiveCustomMode = selectedCustom;
effectiveStreamEnabled = selectedStreamEnabled;
effectiveLaunch = new LaunchMode.NewGame(selectedCustom, selectedStreamEnabled);
}
else
{
// Keep Normal Mode multiplayer runs deterministic unless Custom Mode
// explicitly authored a pending launch.
effectiveCustomMode = false;
effectiveStreamEnabled = false;
}
}

isCustom = effectiveStreamEnabled;
mode = effectiveCustomMode;
gdata = effectiveLaunch;

Seed = sameRunRestart ? restartSeed : lvl;
ModEntry.me = null!;
ModEntry.ResetClientSlots();
ModEntry.kingInitialized = false;
ModEntry._ghost = null!;
ModEntry.ResetHeroCosmeticSendCache();
ClearPendingBossRuneReloadState();
_lastHeroSkinSyncNet = null;
_lastHeroSkinSyncPayload = null;
_lastHeroHeadSkinSyncNet = null;
_lastHeroHeadSkinSyncPayload = null;
var net = GameMenu.NetRef;
var launchKind = GetLaunchKind(gdata);
var nativeBossRushLaunch = IsBossRushLaunchKind(launchKind);
var expectedBossRushLaunch = nativeBossRushLaunch ||
(net?.IsHost == true && GameMenu.HasPrecommittedHostBossRushLaunch()) ||
(net != null && !net.IsHost && GameMenu.HasPendingRemoteBossRushLaunch());
var expectedBossRushLaunch = !sameRunRestart &&
(nativeBossRushLaunch ||
(net?.IsHost == true && GameMenu.HasPrecommittedHostBossRushLaunch()) ||
(net != null && !net.IsHost && GameMenu.HasPendingRemoteBossRushLaunch()));
if (expectedBossRushLaunch && !nativeBossRushLaunch)
{
// Some generated bindings expose the Boss Rush launch variant with a runtime name
// that does not contain the expected token. The synchronized door precommit is a
// stronger signal than the local type name, so normalize the wire identity here.
launchKind = "dc.LaunchMode+BossRush";
}
var shouldSynchronizeSeed = gdata is LaunchMode.NewGame || expectedBossRushLaunch;
var shouldSynchronizeSeed = !sameRunRestart && (gdata is LaunchMode.NewGame || expectedBossRushLaunch);
if (net == null || !net.IsAlive)
RestoreOriginalUserState(self, true);

if (net != null && net.IsHost)
{
var reusedPrecommittedSeed = false;
var seedSequence = 0;
if (shouldSynchronizeSeed &&
if (sameRunRestart)
{
Seed = restartSeed;
}
else if (shouldSynchronizeSeed &&
GameMenu.TryConsumePrecommittedHostRunSeed(launchKind, out var precommittedSeed, out var precommittedSequence))
{
Seed = precommittedSeed;
Expand Down Expand Up @@ -210,7 +253,11 @@ public static void user_hook_new_game(Hook_User.orig_newGame orig,
}
else if (net != null)
{
if (shouldSynchronizeSeed &&
if (sameRunRestart)
{
Seed = restartSeed;
}
else if (shouldSynchronizeSeed &&
GameMenu.TryConsumeNextRemoteRunSeed(out var remoteSeed, out var remoteSequence, out var remoteLaunchKind))
{
Seed = remoteSeed;
Expand Down Expand Up @@ -291,7 +338,29 @@ public static void user_hook_new_game(Hook_User.orig_newGame orig,
self.pickDeathItem();
SendHeroSkin(self, net);
SendHeroHeadSkin(self, net);
orig(self, lvl, isTwitch, isCustom, mode, gdata);
if (mode)
{
// Custom Mode GameData ctor calls CustomGameData.checkIntegrity(user) which
// immediately touches user.itemMeta. Main.getGame loads User via Save.tryLoad,
// so TitleScreen prep alone is not enough on the client auto-start path.
if (!GameMenu.PrepareUserForCustomModeLaunch(self))
{
_log?.Warning(
"[NetMod] Custom Mode newGame: User.itemMeta could not be prepared (role={Role})",
net?.IsHost == true ? "host" : "client");
}
}

try
{
orig(self, lvl, isTwitch, isCustom, mode, gdata);
}
finally
{
GameMenu.ClearAuthoritativePendingNewGameLaunch();
if (sameRunRestart)
ClearSameRunRestart();
}

}

Expand All @@ -304,6 +373,12 @@ private static bool ShouldSynchronizeRunSeed(LaunchMode? launch)
// unconsumed nested seed; that restart is now suppressed for Boss Rush kinds in
// GameMenu.ReceiveHostRunSeed, so sharing the seed is safe. Challenge rooms, daily
// modes and other nested launches stay local.
lock (_sameRunRestartSync)
{
if (_sameRunRestartPending)
return false;
}

return launch is LaunchMode.NewGame || IsBossRushLaunch(launch);
}

Expand Down Expand Up @@ -345,6 +420,92 @@ private static string GetLaunchKind(LaunchMode? launch)

public static void MarkProgressPayloadDirty() { }

internal static void BeginSameRunRestart(int seed)
{
lock (_sameRunRestartSync)
{
_sameRunRestartPending = true;
_sameRunRestartSeed = seed;
}
}

private static bool TryPeekSameRunRestartSeed(out int seed)
{
lock (_sameRunRestartSync)
{
if (_sameRunRestartPending)
{
seed = _sameRunRestartSeed;
return true;
}
}

seed = 0;
return false;
}

private static void ClearSameRunRestart()
{
lock (_sameRunRestartSync)
{
_sameRunRestartPending = false;
_sameRunRestartSeed = 0;
}
}

internal static void CancelSameRunRestart()
{
ClearSameRunRestart();
}

internal static bool ResolveCurrentRunIsCustom()
{
try
{
var currentCustomGame = dc.pr.Game.Class.ME?.data?.cgData;
if (currentCustomGame != null)
return true;
}
catch
{
}

try
{
var mainGameData = dc.Main.Class.ME?.user?.mainGameData;
if (mainGameData != null)
return mainGameData.isCustom;
}
catch
{
}

return _isCustom;
}

internal static bool ResolveCurrentRunStreamEnabled()
{
try
{
var game = dc.pr.Game.Class.ME;
if (game?.data != null)
return game.data._twitchMode;
}
catch
{
}

if (_launch is LaunchMode.NewGame storedNewGame)
return storedNewGame.Param1;

return false;
}

internal static LaunchMode BuildSameRunRestartLaunchMode()
{
return new LaunchMode.NewGame(ResolveCurrentRunIsCustom(), ResolveCurrentRunStreamEnabled());
}

private static string? GetCurrentProgressPayload(User user)
{
if (user == null)
Expand Down Expand Up @@ -886,6 +1047,31 @@ public static bool TryGetRemoteBossRune(out int bossRune)
return false;
}

internal static bool HasRemoteBossRune()
{
lock (_bossRuneLock)
{
return _remoteBossRune.HasValue;
}
}

internal static void SendCurrentHeroCosmetics(User? user, NetNode? net, bool force = false)
{
if (user == null || net == null || !net.IsAlive)
return;

if (force)
{
_lastHeroSkinSyncNet = null;
_lastHeroSkinSyncPayload = null;
_lastHeroHeadSkinSyncNet = null;
_lastHeroHeadSkinSyncPayload = null;
}

SendHeroSkin(user, net);
SendHeroHeadSkin(user, net);
}

public static void SaveOrigHpMultipliers()
{
if (_origHpMultipliersSaved)
Expand Down
Loading
Loading