Skip to content
Closed
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
121 changes: 86 additions & 35 deletions ModEntry/ModEntry.Steam.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<GameLobbyJoinRequested_t>.Create(OnGameLobbyJoinRequested);
s_steamRichPresenceJoinCallback = Callback<GameRichPresenceJoinRequested_t>.Create(OnGameRichPresenceJoinRequested);
s_steamOverlayCallbackPending = false;
StartSteamCallbackPumpTimer();
Instance?.Logger.Information("[NetMod] Steam overlay join callbacks registered (game had Steam initialized)");
return;
}
s_steamOverlayJoinCallback = Callback<GameLobbyJoinRequested_t>.Create(OnGameLobbyJoinRequested);
s_steamRichPresenceJoinCallback = Callback<GameRichPresenceJoinRequested_t>.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<GameLobbyJoinRequested_t>.Create(OnGameLobbyJoinRequested);
s_steamRichPresenceJoinCallback = Callback<GameRichPresenceJoinRequested_t>.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)
Expand All @@ -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<GameLobbyJoinRequested_t>.Create(OnGameLobbyJoinRequested);
try
{
s_steamOverlayJoinCallback = Callback<GameLobbyJoinRequested_t>.Create(OnGameLobbyJoinRequested);
}
catch
{
s_steamOverlayDisabled = true;
}
}

private static void OnGameLobbyJoinRequested(GameLobbyJoinRequested_t data)
Expand Down Expand Up @@ -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;
}
}

/// <summary>
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}

Expand All @@ -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));
Expand Down
47 changes: 37 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
<img width="351" height="163" alt="image" src="https://github.com/user-attachments/assets/8886c291-e0fc-45fe-80e8-4a40550b61aa" />
5. Install [.NET 10 Runtime](https://dotnet.microsoft.com/download/dotnet/current/runtime) (Desktop Runtime, Windows x64) if not already installed.


---
Expand All @@ -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 doesnt exist)
2. Create a folder named `mods` (if it doesn't exist)
3. Extract the **DeadCellsMultiplayerMod** folder into the `mods` directory

Example:
Expand All @@ -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.

---
Expand Down
37 changes: 30 additions & 7 deletions SteamP2P/SteamP2PWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<WorkerEvent>();
using var p2pFailCallback = Callback<P2PSessionConnectFail_t>.Create(data =>
{
Expand Down