Skip to content

Releases: shefben/earthbreakers

Release list

Initial Release

Choose a tag to compare

@shefben shefben released this 25 Aug 01:06

Full Changelog: https://github.com/shefben/earthbreakers/commits/1.0

EarthBreakers multiplayer patch

EarthBreakers (Steam AppID 1319010, Grasshopper-Win64-Shipping.exe) shipped
with its multiplayer wired to a Steam matchmaking backend that no longer
exists for this build. This patch replaces that backend with a small
steam_api64.dll shim plus a standalone Python list server, so the game can
find and join servers again without Steam.

It does not modify the game's .exe or any .pak content. Everything
lives in a drop-in steam_api64.dll and a config file.

What it does

  • Lets the client browse and join servers again (ISteamMatchmakingServers
    is intercepted and backed by an HTTP list server instead of Steam).
  • Lets a listen/dedicated server register itself on that list.
  • Gives every player a unique Steam ID (the stock Steam emulator handed every
    copy of the game the same ID, which capped a session at two players).
  • Fixes several server-side join/spawn bugs the game hits when running
    without a real Steam backend (see Known issues and fixes below).
  • Ships pointed at a public list server by default —
    the.yankees-suck.net — so installing the patch is enough to see
    other players' games; you don't need to stand up your own list server
    unless you want a private/LAN session.

How it works

+----------------+    HTTP/JSON      +--------------------------+
|  Game client   |  --- /servers --> |  Python list server      |
|  (steam_api64  |  <--------------  |   list_server/server.py  |
|   shim DLL)    |                   |   default: the.yankees-  |
+----------------+                   |   suck.net:8088          |
       ^                             +--------------------------+
       | ISteamMatchmakingServers
       | vtable + P2P/session calls
+----------------+
| OS / UE Steam  |
| OSS code paths |
+----------------+

The shim is a drop-in replacement for steam_api64.dll. It forwards every
Steam call it doesn't care about to the existing Steam emulator (renamed
steam_api64_orig.dll next to it), and intercepts the handful of calls that
matter for multiplayer:

  • SteamInternal_FindOrCreateUserInterface(..., "SteamMatchMakingServers002")
    returns the shim's own matchmaking-servers object. Its
    RequestInternetServerList does an HTTP GET against the list server's
    /servers endpoint, parses the JSON, and feeds the results back through
    the standard ISteamMatchmakingServerListResponse callbacks the SDK
    normally drives — the game's own server browser UI doesn't know the
    difference.
  • EnableHeartbeats(true) (a listen/dedicated server going live) makes
    the shim POST /servers/register and then heartbeat itself on the list
    server, so it shows up for everyone else.
  • P2P session / networking calls are patched to work over real UDP
    between arbitrary machines instead of loopback-only, with a SteamID →
    endpoint peer table so reconnects and multiple simultaneous players work.
  • ISteamUser identity is patched so each installed copy derives a
    unique SteamID from the machine name + Windows username instead of every
    copy sharing one ID.
  • A set of targeted, server-side fixes (below) work around bugs the game's
    own code hits once it's not talking to real Steam.

None of this needs the game's source or a recompiled .exe — it all happens
by being the DLL the game already loads at startup.

Known issues and fixes already in the shim

The interesting bugs here aren't in the shim's own networking — they're bugs
in the game's code that only surface once you're running a server without
real Steam infrastructure behind it (no dedicated-server code path was ever
shipped/tested for this build). Each was root-caused by reverse engineering
the shipping binary and is worked around from the shim:

  • Players fall through the map. The ground is a UE Landscape in a
    streaming sublevel that never loads on a headless server. The shim forces
    that sublevel to stream in and recreates landscape collision; a flat-floor
    fallback catches anyone who still spawns over a hole.
  • New pawns spawn with no weapon / no collision. Pawns spawn
    "deferred" (construction never finishes) unless the shim explicitly
    finishes construction, which is also required before the weapon-equip
    logic has anything to attach to.
  • Joining players get kicked right after team select. PostLogin drives
    the player through spectator → team-select → Server_SetTeam, and a
    null-deref in cleanup code (hit only because the emulated Steam identity
    doesn't look like the game expects) kicks them. Gated off / patched around
    in the shim's join handling.
  • The buy menu is empty / the shop terminal has no sprite / the server
    dies when the last player leaves.
    All trace back to the player's team
    identity never being established server-side; the shim drives the
    BP-level team/economy state (UTeamStateData, UUpgrade grants, currency
    deduction) directly as the authoritative side, since there's no native
    Server_Purchase entry point to hook.

See docs/flow_*.md for the full reverse-engineered network/spawn/loadout
flow this is all built against.

Open issue: the AI harvester

Resource harvester units (AI-controlled, not player-driven) don't move on a
server run through this patch — root cause: the server's navmesh is empty
(never built/baked for a headless run), so the harvester's own Blueprint
MoveTo silently fails. Chasing this down through the possessed-controller
path was a dead end.

Current fix: the shim runs a direct autopilot for harvester pawns —
it re-issues movement commands straight at the pawn each tick and wakes its
PhysX body, bypassing the broken BP pathing entirely. Harvesters do move and
harvest with this in place.

Still unexplained: while chasing the throttle the harvester's vehicle
movement code reads, live RE turned up a real contradiction that's still
open — the AI does command full throttle (raw == 1.0) at the point the
value is read, but it reads back as 0 by the time UpdateState runs later
in the same tick. Seven separate theories (including a controller gate and
an early-return branch) have been tested and disproven against the live
process; nothing found so far explains the drop. This doesn't block anything
today because the autopilot workaround sidesteps the game's own throttle
path, but it means the stock harvester movement code is still broken
underneath, and would need this resolved before the autopilot patch could be
retired.

Layout

server_dev/
  steam_shim/                  # the shim: source of steam_api64.dll
    src/shim.cpp                 all interception, patches, autopilot, etc.
    src/steam_types.h            Steam SDK type/struct definitions
    CMakeLists.txt
  ebrk/                        # shared native watchdog/symbol-resolution lib
    src/                          crash-safe patching, logging, diagnostics
    tools/gen_symbols.py          regenerates generated/symbols.g.h from
                                   symbols.def against the symbolized IDB
  list_server/server.py        # the HTTP list server (default port 8088)
  release/                     # source-of-truth for the distributable zip
    game_files/                   files copied onto a game install
    install.ps1 / uninstall.ps1   non-destructive installer (backs up first)
  docs/                        # reverse-engineered flow documentation
    flow_01_network_join.md .. flow_05_verification.md
  ida_scripts/, findings/      # one-off RE scripts and their raw output
  tools/                       # ad hoc IDA/analysis scripts used during RE

Install (pre-built patch)

  1. Download the release archive and unzip it somewhere.
  2. Run INSTALL.cmd, point it at your EarthBreakers install folder (the one
    containing Grasshopper\ and Engine\). It backs up every file it
    replaces into _ebrk_backup\ first — safe to re-run.
  3. Join: run join.cmd from the game folder. It points at
    the.yankees-suck.net by default (see Grasshopper\Binaries\Win64\ebrk.ini
    to change it — same file lets you point at a friend's IP or 127.0.0.1
    for a same-machine test).
  4. Host: run host.cmd [MapName]. Your game registers itself with the
    public list automatically; other players see it in the server browser.
    Only run start_list_server.cmd if you specifically want to self-host
    the browser list instead (LAN parties, private testing).
  5. Log file for troubleshooting: %TEMP%\ebrk_shim.log.

To remove the patch: run UNINSTALL.cmd from the same package — it restores
everything from _ebrk_backup\.

Compile from source

Requires a C++17 toolchain with the Windows SDK (Visual Studio 2019+, the
build below uses the VS generator) and Python 3.8+ (only needed at build
time to verify ebrk/generated/symbols.g.h is still in sync with
ebrk/symbols.def; it does not regenerate it — that requires the symbolized
IDB, which isn't part of this repo).

cmake -S server_dev/steam_shim -B server_dev/steam_shim/build -G "Visual Studio 16 2019" -A x64
cmake --build server_dev/steam_shim/build --config Release

Output: server_dev/steam_shim/build/Release/steam_api64.dll.

Install it over the existing steam_api64.dll next to the game's
steamclient64.dll, at:

<game>\Engine\Binaries\ThirdParty\Steamworks\Steamv146\Win64\steam_api64.dll

(not Grasshopper\Binaries\... — that path is ignored). Keep the
original next to it as steam_api64_orig.dll; the shim forwards to it. For
local dev, install_shim.ps1 in this directory automates that swap
(hardcoded to a local M:\earthbreakers checkout — the release/install.ps1
script is the portable one meant for oth...

Read more