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)); 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 => {