Keeps Looterkings (see SteamDB) playable after the shutdown of its Zeus master servers, by adding a direct IP connection using F9 in the Main menu.
After the announcement of the server shutdown, my brother and I wanted to 100% the game. We tried for an hour to get a multiplayer session running, but without success. I asked Claude if it could build a workaround. A few iterations later we were able to connect and play together. Something that would have taken me days, done in a few hours.
What we were able to do with this modification:
- Play the football / soccer mini game.
- Play the normal mode and finish it.
- And in the end get all achievements.
Noteworthy: I play on Linux and my brother on Windows. The mod worked for both of us. We used Wireguard to be in the same local network.
Apart from this section, the rest is written by AI. I corrected a few links. Have fun.
The game uses Photon Bolt. The master server was only ever responsible for the server list and for punching connections through routers — the gameplay traffic always went directly between the players' machines. That is what makes playing on possible at all.
A direct IP input was even planned in the game, but never finished: the input field is
permanently greyed out and SessionBrowserScript.OnIpSet is an empty method body. This
project fills that gap, frees host and client from the master server dependency, and puts
the input on F9.
Status: working. The game has been played through to 100% by two people across two machines (Windows host / Linux+Proton client); round duration, automatic rematch, XP and gold multiplier and the test dummies all work.
Grab the release archive and follow dist/README.md: back up
Assembly-CSharp.dll, copy three files in, press F9 in the server browser.
There is no installer — it is one rename and three copies, and a script that has to be trusted with your game folder is a worse trade than doing it by hand.
LKDirect.dll is a new assembly holding all the logic. Patcher.exe (Mono.Cecil) rewrites
a small number of IL sites in the game's Assembly-CSharp.dll to call into it. Nothing else
about the game is modified.
| # | Site | Change |
|---|---|---|
| A | SessionBrowserScript.OnIpSet |
was an empty body (ret); now calls LKDirect.ConnectTo() |
| B | ConnectionUtility.StartServer (2×) |
hardcoded useMasterServer = true → LKDirect.UseMasterServer() |
| C | SessionBrowserScript.Setup |
calls LKDirect.Boot() — creates the overlay |
| D | Client-/ServerStartJob.OnBoltStarted |
Zeus.IsConnected → LKDirect.ZeusReady() |
| E | ServerStartJob.OnBoltStarted |
offline branch now runs InitHostInfo() before Finish() |
| F | Dbg.Log/LogFast/LogWarning |
empty bodies in the release build → LKDirect.GameLog() |
| G | MasterServerChoice.ResolveURL |
→ LKDirect.ResolveMaster(), survives a dead DNS record |
| H | HostProcess.SearchForSession |
FindSession → LKDirect.FindSessionOrLocal() |
| I | MatchManager_Server (3×) |
m_playTime → LKDirect.MatchTime() |
| J | MatchManager_Server.EndGame |
→ LKDirect.AutoRematch() |
| K/L/M/N/Q | Player, MatchManager_Server, LooterLeagueServerCallback |
dummy players (see below) |
| O/P | MatchManager_Server.EndGame / .StartMatch |
count a win once per match, not once per frame |
| R | Player.AddExperience |
p_amount → LKDirect.ScaleXp() |
| S | LootBehaviorScript.Start |
GetLootValue result → LKDirect.ScaleGold() |
The patcher refuses to run on an assembly that does not look like the expected original — it
aborts if OnIpSet is not the empty body it expects, or if a site count does not match. It
cannot be applied twice to the same file by accident.
Direct connect bypasses ConnectProcess, so a handful of things the normal join path
does have to be done by hand: setting ConnectionStatus.Connected (otherwise the screen
stays black after cutscenes), raising OnConnected (otherwise loot data stays null and
throws), switching to the lobby menu, and setting the game mode — which is why the F9 overlay
has a mode picker, since without a session the client cannot learn the host's mode.
Dummy players exist to test a round alone. GoblinBall requires at three points that
every player has acted (scene loaded, team picked, character picked); a dummy has no
BoltConnection and can trigger none of them, so each hurdle is cleared server-side. The
whole feature is gated behind dummy_players > 0 and can also be compiled out entirely
with --no-dummy.
GoblinBall-only options. dummy_players, goblinball_time and auto_rematch_* are
additionally gated on GameManager.mode == LooterLeague, so a config left over from a
GoblinBall evening does not leak into a normal round. The check is deliberately not
cached — the mode changes between matches without the game being restarted — while the
config values themselves still are. All three are host-only, so the host's menu selection
is the authority.
XP multiplier hooks Player.AddExperience, the only route by which XP is gained. The
Bolt state is server authoritative, so only the host's setting matters — it then applies to
everyone in the match.
Gold multiplier hooks the return value of GameInfo.GetLootValue(), which has exactly
one caller: LootBehaviorScript.Start(), where it initialises m_lootValue. That one field
then feeds everything downstream — Player.AddGold(), the "+X gold" popup via
FireOnLootCollectedEvent, and the halving when two players are stacked — so scaling there
keeps all three consistent. Player.AddGold() deliberately is not the hook: the shop
refund (StreamAction.SetSold) and the debug cheats go through it too.
Note that unlike AddExperience, AddGold has no clamp — it is a plain state.gold + amount
in int32, accumulated across a whole round. Both the factor (max 1000) and the per-pickup
result (max 1e6) are therefore capped, so the sum cannot overflow into negative gold.
Requirements:
- Mono C# compiler and runtime (
mcs,mono) —pacman -S mono/apt install mono-devel. - Mono.Cecil 0.11.x — the patcher is built against it. Usually already there: Mono ships
it in its GAC and
build.shfinds it by itself. See below if yours does not. - Your own copy of Looterkings, for the eight game assemblies the build references
(
Assembly-CSharp,mscorlib,System,System.Core,UnityEngine,bolt,bolt.user,udpkit.common). They are not in this repository. TheAssembly-CSharp.dllamong them has to be the unpatched one. - bash —
build.shis a bash script, so on Windows use WSL or Git Bash.
Nothing else is downloaded during the build, and nothing is written outside the repository.
On Arch, pacman -S mono covers it. On Debian it can be a separate package
(apt install libmono-cecil-cil). Failing that, drop a copy into tools/, or point
CECIL at one you already have (CECIL=/path/to/Mono.Cecil.dll ./build.sh):
curl -sL -o cecil.zip https://www.nuget.org/api/v2/package/Mono.Cecil/0.11.1
unzip -j cecil.zip lib/net40/Mono.Cecil.dll -d tools/0.9.x is a different API and will not compile. If several versions are installed, the highest one is used.
Copy them out of your own install into refs/ (gitignored):
cp "$HOME/.steam/steam/steamapps/common/Looterkings/looterkings_Data/Managed/"*.dll refs/
./build.shOr point REFS somewhere else: REFS=/path/to/Managed ./build.sh.
Either way that folder needs the unpatched Assembly-CSharp.dll. If the patch is
already installed, use the Assembly-CSharp.dll.orig backup you made when installing.
Output lands in dist/. Add --no-dummy for a build without the dummy-player patches.
src/LKDirect.cs the helper assembly — all logic lives here
src/Patcher.cs the Cecil patcher
tools/ optional local copy of Mono.Cecil (gitignored)
refs/ your local copy of the game assemblies (gitignored)
dist/ release payload: config template, built DLLs, player README
build.sh prints the MD5 of the unpatched Assembly-CSharp.dll it built against. That is
the value quoted in dist/README.md under Does the patch fit my version? — if you build
against a different game version, update it there.
Built against the final Steam release of Looterkings, Assembly-CSharp.dll MD5
218ad7184b1dff6ba2f019ecf94aa91d. Check yours before installing — the patched assembly
is built by rewriting exactly that file, and a different build will not work.
A Steam file verification reverts the patch (LKDirect.dll stays behind but is then inert).
Just copy the patched Assembly-CSharp.dll in again.