From 8eebde0b604ef2885d390f93c5a4accff302f477 Mon Sep 17 00:00:00 2001 From: lisniuse <17560235@qq.com> Date: Sun, 31 May 2026 12:10:44 +0800 Subject: [PATCH 1/4] fix(steam): guard overlay features against incomplete Steam API implementations Wrap Steam overlay join polling, callback registration, and RunCallbacks in try-catch blocks to prevent native crashes when running with emulated Steam environments (e.g. Goldberg emulator or non-Steam game versions). - Add s_steamOverlayDisabled flag to permanently disable overlay features after the first native failure, avoiding repeated crash attempts - Guard TryPollSteamOverlayJoinFromLaunchData against crashes from SteamApps.GetLaunchCommandLine / GetLaunchQueryParam (unsupported by Goldberg emulator) - Guard TryDeferredSteamOverlayCallbackRegistration and TryRegisterSteamOverlayJoinCallback against Callback.Create failures - Add safety checks to TryRunSteamCallbacks and background timer - All guards are transparent to real Steam users (no exceptions thrown on genuine Steam API) --- ModEntry/ModEntry.Steam.cs | 121 ++++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 35 deletions(-) diff --git a/ModEntry/ModEntry.Steam.cs b/ModEntry/ModEntry.Steam.cs index 875981c..fe73838 100644 --- a/ModEntry/ModEntry.Steam.cs +++ b/ModEntry/ModEntry.Steam.cs @@ -23,6 +23,8 @@ private static void TryParseConnectLobbyFromCommandLine() private static void TryDeferredSteamOverlayCallbackRegistration() { + if (s_steamOverlayDisabled) + return; if (!s_steamOverlayCallbackPending || (s_steamOverlayJoinCallback != null && s_steamRichPresenceJoinCallback != null)) return; if (s_steamOverlayCallbackRetryCount >= SteamOverlayCallbackMaxRetries) @@ -33,22 +35,31 @@ private static void TryDeferredSteamOverlayCallbackRegistration() } s_steamOverlayCallbackRetryCount++; var shouldLogFailure = s_steamOverlayCallbackRetryCount == 1 || s_steamOverlayCallbackRetryCount % 60 == 0; - if (!TryEnsureSteamApiInitialized($"callback registration attempt {s_steamOverlayCallbackRetryCount}", shouldLogFailure)) + try { - if (shouldLogFailure) - Instance?.Logger.Debug("[NetMod] Steam overlay: SteamAPI.Init()=false (attempt {Attempt}). Trying callback without Init (game may have Steam).", s_steamOverlayCallbackRetryCount); + if (!TryEnsureSteamApiInitialized($"callback registration attempt {s_steamOverlayCallbackRetryCount}", shouldLogFailure)) + { + if (shouldLogFailure) + Instance?.Logger.Debug("[NetMod] Steam overlay: SteamAPI.Init()=false (attempt {Attempt}). Trying callback without Init (game may have Steam).", s_steamOverlayCallbackRetryCount); + s_steamOverlayJoinCallback = Callback.Create(OnGameLobbyJoinRequested); + s_steamRichPresenceJoinCallback = Callback.Create(OnGameRichPresenceJoinRequested); + s_steamOverlayCallbackPending = false; + StartSteamCallbackPumpTimer(); + Instance?.Logger.Information("[NetMod] Steam overlay join callbacks registered (game had Steam initialized)"); + return; + } s_steamOverlayJoinCallback = Callback.Create(OnGameLobbyJoinRequested); s_steamRichPresenceJoinCallback = Callback.Create(OnGameRichPresenceJoinRequested); s_steamOverlayCallbackPending = false; StartSteamCallbackPumpTimer(); - Instance?.Logger.Information("[NetMod] Steam overlay join callbacks registered (game had Steam initialized)"); - return; + Instance?.Logger.Information("[NetMod] Steam overlay join callbacks registered (attempt {Attempt})", s_steamOverlayCallbackRetryCount); + } + catch + { + Instance?.Logger.Warning("[NetMod] Steam overlay callback registration failed. Disabling overlay features."); + s_steamOverlayDisabled = true; + s_steamOverlayCallbackPending = false; } - s_steamOverlayJoinCallback = Callback.Create(OnGameLobbyJoinRequested); - s_steamRichPresenceJoinCallback = Callback.Create(OnGameRichPresenceJoinRequested); - s_steamOverlayCallbackPending = false; - StartSteamCallbackPumpTimer(); - Instance?.Logger.Information("[NetMod] Steam overlay join callbacks registered (attempt {Attempt})", s_steamOverlayCallbackRetryCount); } private static void WriteOverlayJoinDiagnostic(string callbackType, string data) @@ -72,9 +83,16 @@ private static void WriteOverlayCallbackFailedDiagnostics(Exception ex) private static void TryRegisterSteamOverlayJoinCallback() { - if (s_steamOverlayJoinCallback != null) + if (s_steamOverlayDisabled || s_steamOverlayJoinCallback != null) return; - s_steamOverlayJoinCallback = Callback.Create(OnGameLobbyJoinRequested); + try + { + s_steamOverlayJoinCallback = Callback.Create(OnGameLobbyJoinRequested); + } + catch + { + s_steamOverlayDisabled = true; + } } private static void OnGameLobbyJoinRequested(GameLobbyJoinRequested_t data) @@ -142,7 +160,17 @@ private static ulong TryParseLobbyIdFromConnectString(string connect) private static void TryRunSteamCallbacks() { - SteamAPI.RunCallbacks(); + if (!s_steamApiReady) + return; + try + { + SteamAPI.RunCallbacks(); + } + catch + { + Instance?.Logger.Warning("[NetMod][Steam] SteamAPI.RunCallbacks failed. Disabling overlay features."); + s_steamOverlayDisabled = true; + } } /// @@ -199,9 +227,11 @@ private static bool TryEnsureSteamApiInitialized(string source, bool logFailure) return false; } + private static bool s_steamOverlayDisabled; + private static void TryPollSteamOverlayJoinFromLaunchData() { - if (!s_steamApiReady) + if (!s_steamApiReady || s_steamOverlayDisabled) return; var nowTicks = Environment.TickCount64; @@ -210,33 +240,50 @@ private static void TryPollSteamOverlayJoinFromLaunchData() s_nextSteamLaunchPollTicks = nowTicks + SteamOverlayLaunchPollIntervalMs; - string steamLaunchCommand = string.Empty; - var launchCommandLength = SteamApps.GetLaunchCommandLine(out steamLaunchCommand, 2048); - steamLaunchCommand = (steamLaunchCommand ?? string.Empty).Trim(); - if (launchCommandLength > 0 && - !string.IsNullOrWhiteSpace(steamLaunchCommand) && - !string.Equals(steamLaunchCommand, s_lastSteamLaunchCommand, StringComparison.Ordinal)) + try { - s_lastSteamLaunchCommand = steamLaunchCommand; - var lobbyId = TryParseLobbyIdFromConnectString(steamLaunchCommand); - if (lobbyId > 0UL) + string steamLaunchCommand = string.Empty; + var launchCommandLength = SteamApps.GetLaunchCommandLine(out steamLaunchCommand, 2048); + steamLaunchCommand = (steamLaunchCommand ?? string.Empty).Trim(); + if (launchCommandLength > 0 && + !string.IsNullOrWhiteSpace(steamLaunchCommand) && + !string.Equals(steamLaunchCommand, s_lastSteamLaunchCommand, StringComparison.Ordinal)) { - Instance?.Logger.Information("[NetMod][Steam] Detected overlay join from Steam launch command: {Command}", steamLaunchCommand); - EnqueueAndProcessOverlayJoin(lobbyId, "SteamApps.GetLaunchCommandLine"); - return; + s_lastSteamLaunchCommand = steamLaunchCommand; + var lobbyId = TryParseLobbyIdFromConnectString(steamLaunchCommand); + if (lobbyId > 0UL) + { + Instance?.Logger.Information("[NetMod][Steam] Detected overlay join from Steam launch command: {Command}", steamLaunchCommand); + EnqueueAndProcessOverlayJoin(lobbyId, "SteamApps.GetLaunchCommandLine"); + return; + } } } - - var connectLobby = (SteamApps.GetLaunchQueryParam("connect_lobby") ?? string.Empty).Trim(); - if (string.IsNullOrWhiteSpace(connectLobby) || - string.Equals(connectLobby, s_lastSteamLaunchConnectLobbyParam, StringComparison.Ordinal)) + catch + { + Instance?.Logger.Warning("[NetMod][Steam] SteamApps.GetLaunchCommandLine failed. Disabling overlay join polling."); + s_steamOverlayDisabled = true; return; + } + + try + { + var connectLobby = (SteamApps.GetLaunchQueryParam("connect_lobby") ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(connectLobby) || + string.Equals(connectLobby, s_lastSteamLaunchConnectLobbyParam, StringComparison.Ordinal)) + return; - s_lastSteamLaunchConnectLobbyParam = connectLobby; - if (ulong.TryParse(connectLobby, out var lobbyId2) && lobbyId2 > 0UL) + s_lastSteamLaunchConnectLobbyParam = connectLobby; + if (ulong.TryParse(connectLobby, out var lobbyId2) && lobbyId2 > 0UL) + { + Instance?.Logger.Information("[NetMod][Steam] Detected overlay join from Steam launch query param connect_lobby={LobbyId}", lobbyId2); + EnqueueAndProcessOverlayJoin(lobbyId2, "SteamApps.GetLaunchQueryParam"); + } + } + catch { - Instance?.Logger.Information("[NetMod][Steam] Detected overlay join from Steam launch query param connect_lobby={LobbyId}", lobbyId2); - EnqueueAndProcessOverlayJoin(lobbyId2, "SteamApps.GetLaunchQueryParam"); + Instance?.Logger.Warning("[NetMod][Steam] SteamApps.GetLaunchQueryParam failed. Disabling overlay join polling."); + s_steamOverlayDisabled = true; } } @@ -249,7 +296,11 @@ private static void StartSteamCallbackPumpTimer() if (s_steamCallbackPumpTimer != null) return; s_steamCallbackPumpTimer = new Timer( - _ => SteamAPI.RunCallbacks(), + _ => + { + if (s_steamOverlayDisabled) return; + try { SteamAPI.RunCallbacks(); } catch { s_steamOverlayDisabled = true; } + }, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(100)); From 921f4d60220d0c37be4028eeb09b2dd512155596 Mon Sep 17 00:00:00 2001 From: lisniuse <17560235@qq.com> Date: Sun, 31 May 2026 12:19:12 +0800 Subject: [PATCH 2/4] fix(steam): handle P2P worker Steam init failure gracefully on non-Steam clients Move Steam API init outside the main try block in WorkerEntry() so that init failures (SteamAPI.Init / RestartAppIfNecessary) write a bootstrap response before exiting, instead of throwing an unhandled exception that triggers a Windows crash dialog. Also update README with proper non-Steam setup instructions: - Rename steam.hdll to enable Goldberg emulator - Set EnableGoldberg: true in modcore.json - Launch via DeadCellsModding.exe directly - Document TCP/LAN limitation for non-Steam (P2P unavailable) Closes #42 --- README.md | 47 ++++++++++++++++++++++++++++++-------- SteamP2P/SteamP2PWorker.cs | 37 ++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index baae86b..f4e8b38 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,18 @@ If you are using a **non-Steam version** of Dead Cells: 1. Download the latest release of **DCCM** from the official repository: πŸ‘‰ [https://github.com/dead-cells-core-modding/core](https://github.com/dead-cells-core-modding/core) -2. Open your Dead Cells game directory. +2. Extract the DCCM files to your Dead Cells game directory (the `coremod` folder will be created). -3. Create a folder named `coremod`. +3. Rename `steam.hdll` to `steam.hdll.bak` in the game root directory (this triggers Goldberg emulator auto-detection). -4. Extract the downloaded DCCM files into the `coremod` folder. +4. Open `coremod\config\modcore.json` and ensure `EnableGoldberg` is set to `true`: + ```json + { + "EnableGoldberg": true + } + ``` -5. Open modcore.json that's in `your_game_path\Dead Cells\coremod\config\modcore.json` - -6. Ensure that EnableGoldberg is true! -image +5. Install [.NET 10 Runtime](https://dotnet.microsoft.com/download/dotnet/current/runtime) (Desktop Runtime, Windows x64) if not already installed. --- @@ -86,12 +88,12 @@ If you are using the **Steam version** of the game: --- -### πŸ”Ή Non-Steam version(DCCM doesn't support non-steam play now) +### πŸ”Ή Non-Steam version (GOG / other store versions) If you are using a **non-Steam version** of Dead Cells: 1. Navigate to your **DCCM directory** -2. Create a folder named `mods` (if it doesn’t exist) +2. Create a folder named `mods` (if it doesn't exist) 3. Extract the **DeadCellsMultiplayerMod** folder into the `mods` directory Example: @@ -102,11 +104,36 @@ Your game path/ └── DeadCellsMultiplayerMod/ ``` +#### βš™οΈ Required configuration for non-Steam versions + +1. **Rename `steam.hdll`** in the game root directory to `steam.hdll.bak` β€” this triggers DCCM to load the Goldberg Steam emulator instead of the real Steam API. + +2. **Enable Goldberg** in `coremod\config\modcore.json`: + ```json + "EnableGoldberg": true + ``` + +3. **Launch the game** via `coremod\core\host\startup\DeadCellsModding.exe` directly (not through SteamStartShell). + +> ⚠️ **Limitations on non-Steam versions:** +> - **TCP/LAN multiplayer** works normally (this is the default mode) +> - **Steam P2P mode** is not available (requires a real Steam client with Steam Networking) +> - **Steam overlay "Join Game"** is not available +> +> For online play with friends, use virtual LAN software (Hamachi, Radmin VPN, ZeroTier) and connect via IP address. +Your game path/ + └──coremod/ + └── mods/ + └── DeadCellsMultiplayerMod/ +``` + --- ### 3️⃣ Run the game via DCCM -Start **Dead Cells** using **DCCM**. +**Steam version:** Start Dead Cells via Steam as usual. DCCM loads automatically. + +**Non-Steam version:** Launch directly via `coremod\core\host\startup\DeadCellsModding.exe`. On the first launch, required configuration files will be generated automatically. --- diff --git a/SteamP2P/SteamP2PWorker.cs b/SteamP2P/SteamP2PWorker.cs index 4cb1b7f..9e0f0f5 100644 --- a/SteamP2P/SteamP2PWorker.cs +++ b/SteamP2P/SteamP2PWorker.cs @@ -667,6 +667,36 @@ public static void WorkerEntry() { var bootstrapResponsePath = Environment.GetEnvironmentVariable(SteamP2PWorkerEnvironment.EnvBootstrapResponsePath); + // Init Steam early so we can report failure before entering the main try block. + // On non-Steam environments (Goldberg, Rune, etc.) these native calls may + // cause access violations that bypass managed exception handlers. + try + { + SteamConnect.PrepareSteamNativePathForRuntime(); + if (SteamAPI.RestartAppIfNecessary(new AppId_t(DeadCellsAppId))) + { + if (!string.IsNullOrWhiteSpace(bootstrapResponsePath)) + TryWriteBootstrapResponse(bootstrapResponsePath, false, "Steam requested app restart"); + Environment.Exit(1); + return; + } + + if (!SteamAPI.Init()) + { + if (!string.IsNullOrWhiteSpace(bootstrapResponsePath)) + TryWriteBootstrapResponse(bootstrapResponsePath, false, "Steam API init failed in Steam P2P worker"); + Environment.Exit(1); + return; + } + } + catch (Exception ex) + { + if (!string.IsNullOrWhiteSpace(bootstrapResponsePath)) + TryWriteBootstrapResponse(bootstrapResponsePath, false, $"Steam init error: {ex.Message}"); + Environment.Exit(1); + return; + } + try { var roleText = Environment.GetEnvironmentVariable(SteamP2PWorkerEnvironment.EnvRole) ?? string.Empty; @@ -680,13 +710,6 @@ public static void WorkerEntry() if (!ulong.TryParse(hostSteamIdRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var hostSteamId)) hostSteamId = 0UL; - SteamConnect.PrepareSteamNativePathForRuntime(); - if (SteamAPI.RestartAppIfNecessary(new AppId_t(DeadCellsAppId))) - throw new InvalidOperationException("Steam requested app restart"); - - if (!SteamAPI.Init()) - throw new InvalidOperationException("Steam API init failed in Steam P2P worker"); - var sessionFailQueue = new ConcurrentQueue(); using var p2pFailCallback = Callback.Create(data => { From c274f95dda34db0a244d3a247f5ee2c935dd45e7 Mon Sep 17 00:00:00 2001 From: lisniuse <17560235@qq.com> Date: Sun, 31 May 2026 13:06:08 +0800 Subject: [PATCH 3/4] fix(fake-death): dismiss GameOver screen on client when host triggers restart When both players die and the host triggers a run restart, the client was left stuck on the Game Over screen because the GameOver UI was never dismissed before launching the new game. - Add GameOver removal (remove/destroy/dispose) in ResetAllDownedGameOverState - Add DismissGameOverScreen helper in GameMenu - Call it from both QueueHostRestartFromDeath and QueueClientRestartFromHostSeed to ensure cleanup on both sides --- FakeDeath/FakeDeath.cs | 14 ++++++++++++++ UI/GameMenu.cs | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/FakeDeath/FakeDeath.cs b/FakeDeath/FakeDeath.cs index d29fb7b..d053e06 100644 --- a/FakeDeath/FakeDeath.cs +++ b/FakeDeath/FakeDeath.cs @@ -1460,6 +1460,20 @@ private void ResetAllDownedGameOverState() _allDownedGameOverShown = false; _allDownedRestartQueued = false; _allDownedRestartAtTicks = 0; + + try + { + var gameOver = GameOver.Class.ME; + if (gameOver != null) + { + try { gameOver.remove(); } catch { } + try { gameOver.destroy(); } catch { } + try { gameOver.dispose(); } catch { } + } + } + catch + { + } } private bool TryUpdateDownedPositionFromCorpse(double corpseX, double corpseY) diff --git a/UI/GameMenu.cs b/UI/GameMenu.cs index a4f5dfe..efeaf12 100644 --- a/UI/GameMenu.cs +++ b/UI/GameMenu.cs @@ -458,6 +458,7 @@ internal static void QueueHostRestartFromDeath(string reason) EnqueueMainThread(() => { ModEntry.ResetDownedPlayersForRestart(); + DismissGameOverScreen(); var game = ModEntry.Instance?.game; if (game?.user == null) @@ -492,6 +493,7 @@ private static void QueueClientRestartFromHostSeed(int seed, string reason) EnqueueMainThread(() => { ModEntry.ResetDownedPlayersForRestart(); + DismissGameOverScreen(); var game = ModEntry.Instance?.game; if (game?.user == null) @@ -542,6 +544,23 @@ public static bool TryGetRemoteSeed(out int seed) return false; } + private static void DismissGameOverScreen() + { + try + { + var go = dc.ui.GameOver.Class.ME; + if (go != null) + { + try { go.remove(); } catch { } + try { go.destroy(); } catch { } + try { go.dispose(); } catch { } + } + } + catch + { + } + } + public static void ReceiveLevelDesc(string json) { try From 30a62e69fdbf751ed5011431aaa5513de2c11446 Mon Sep 17 00:00:00 2001 From: lisniuse <17560235@qq.com> Date: Sun, 31 May 2026 14:20:43 +0800 Subject: [PATCH 4/4] Revert "fix(fake-death): dismiss GameOver screen on client when host triggers restart" This reverts commit c274f95dda34db0a244d3a247f5ee2c935dd45e7. --- FakeDeath/FakeDeath.cs | 14 -------------- UI/GameMenu.cs | 19 ------------------- 2 files changed, 33 deletions(-) diff --git a/FakeDeath/FakeDeath.cs b/FakeDeath/FakeDeath.cs index d053e06..d29fb7b 100644 --- a/FakeDeath/FakeDeath.cs +++ b/FakeDeath/FakeDeath.cs @@ -1460,20 +1460,6 @@ private void ResetAllDownedGameOverState() _allDownedGameOverShown = false; _allDownedRestartQueued = false; _allDownedRestartAtTicks = 0; - - try - { - var gameOver = GameOver.Class.ME; - if (gameOver != null) - { - try { gameOver.remove(); } catch { } - try { gameOver.destroy(); } catch { } - try { gameOver.dispose(); } catch { } - } - } - catch - { - } } private bool TryUpdateDownedPositionFromCorpse(double corpseX, double corpseY) diff --git a/UI/GameMenu.cs b/UI/GameMenu.cs index efeaf12..a4f5dfe 100644 --- a/UI/GameMenu.cs +++ b/UI/GameMenu.cs @@ -458,7 +458,6 @@ internal static void QueueHostRestartFromDeath(string reason) EnqueueMainThread(() => { ModEntry.ResetDownedPlayersForRestart(); - DismissGameOverScreen(); var game = ModEntry.Instance?.game; if (game?.user == null) @@ -493,7 +492,6 @@ private static void QueueClientRestartFromHostSeed(int seed, string reason) EnqueueMainThread(() => { ModEntry.ResetDownedPlayersForRestart(); - DismissGameOverScreen(); var game = ModEntry.Instance?.game; if (game?.user == null) @@ -544,23 +542,6 @@ public static bool TryGetRemoteSeed(out int seed) return false; } - private static void DismissGameOverScreen() - { - try - { - var go = dc.ui.GameOver.Class.ME; - if (go != null) - { - try { go.remove(); } catch { } - try { go.destroy(); } catch { } - try { go.dispose(); } catch { } - } - } - catch - { - } - } - public static void ReceiveLevelDesc(string json) { try