Files
SandTools/docs/MASTER_SERVER.md
DownloadPizza fc6b270fa8 master-server replay + trampler RE: protocol, hashes, footprints, map renderer
- master_scrape.py: live master-server (ger.hologryph.com) ClientMessage replay over the
  two-socket /login + /connect handshake (PlayFab ticket auth). Pulled compartment defs,
  shop prices, research tree, storage, characters, expedition -> extracted/master_*.json
- PlayFab confirmed auth-only for this title (Economy disabled); docs corrected
- trampler_hashes.py: blueprint hash algo MD5(UTF8(compact-JSON)); CompartmentsHash(#1) and
  ConnectionsHash(#3) verified & generatable from scratch
- walkerdto_to_blueprint.py: WalkerDto(expedition) -> WalkerBlueprintDto, enum int<->name,
  verified by storage->WS->storage round-trip
- render_trampler.py: per-floor map from CompartmentsDatabase cell footprints (rotation solved
  via overlap check) + doors/hatches from Connections + turret arcs + cargo C1-C8 in game order
- docs/MASTER_SERVER.md, docs/TRAMPLER.md; ghidra address-offset bug fixed (no -0x1000)
2026-06-16 00:35:17 +02:00

4.9 KiB

Master server (*.hologryph.com) — protocol & scrape (WORKING)

Reverse-engineered from a fresh il2cpp/dump.cs + Ghidra decompile of the 2026-06-15 build (Steam buildid 23737037, GameAssembly.dll 143 MB) and verified livereverse/master_scrape.py pulls the full economy + stats from an external process (no game, no BattlEye interaction).

Supersedes earlier guesses: PlayFab is auth-only (see BACKEND_PLAYFAB.md); the master server is not SignalR/gRPC and not cleartext ws://:80. The June-5 build used a per-call WebSocket-per-op model; today's build changed to the two-socket ClientMessage protocol below — re-RE was required after the playtest updated the client.

Transport: ClientMessage over wss://, two sockets

Base region URL (live): wss://ger.hologryph.com/gameclient/ (also eus/westus/sin/bra/aus/sko/uae; client picks one via region selection). Everything is JSON text frames of:

ClientMessage = {"Id": <long>, "ClientMessageType": <int>, "Action": <int>, "Message": <string|null>}
  ClientMessageType: Error=0, Ping=1, Action=2, Event=3   (requests use 2; Ping must be echoed)
  Action (ClientAction enum): Login=0, Connect=1, GetCharacters=2, GetStorage=20, GetNews=21,
    GetCompartmentDefinitions=23, GetDatabaseGuid=26, GetDefaultWalkerBlueprints=28, GetServers=29,
    GetResearchTree=32, GetShopItems=39, ... (full list in il2cpp/dump.cs `enum ClientAction`)
  Message: the request payload as a JSON *string* (nested), or null for parameterless ops.

Serialization = Newtonsoft default: PascalCase fields, enums as integers. Each request gets an incrementing Id; the reply is a ClientMessage with the same Id (_pendingRequests correlates them), so replies may interleave with Event(3) pushes and Ping(1).

Reply Message is either the DTO directly (e.g. Login → LoginUserResultDto) or an OperationResult<T> = {IsSucceed,Error,Status,Result}master_scrape.py normalizes both.

The handshake (this is the part that 1008s if wrong)

Two sequential WebSocket connections:

  1. /login — connect wss://<region>/gameclient/login (NO auth header). Send ClientMessage{Action=Login(0), Message=JSON(LoginRequest{SessionTicket, Title, Platform, ClientVersion})}:
    • SessionTicket = the PlayFab SessionTicket (from LoginWithSteam; see playfab_scrape.py)
    • Title = PlayFab TitleId = 56693; Platform = 1 (STEAM, int); ClientVersion = the build's git commit hash (e.g. fbe5e4370daf86d54933ff4786b3312acd8c2d98, from Player.log's Send Login … ver:'…'; changes every build — server may version-gate → Status=5 VERSION_MISMATCH).
    • Reply Message = LoginUserResultDto{User{Id,PlayFabId,PlatformId,DisplayName}, SessionTicket}. Keep that SessionTicket (the server session ticket). Close the socket.
  2. /connect — connect wss://<region>/gameclient/connect with HTTP upgrade header Authorization: <server SessionTicket> (raw, no Bearer ). Send ClientMessage{Action=Connect(1), Message:"False"} (the isDebug bool). This is the persistent socket; send all data ops over it.

Why the earlier attempts got 1008 "User is unauthorized!": connecting straight to /connect with no Authorization header (the auth is the header, set from the ticket that only /login mints). There is no SetRequestHeader for /login (its ticket rides inside the Login message instead).

Data ops (over the /connect socket)

Send ClientMessage{Action=<n>, Message:null}; reply Message carries the result. Verified live:

Action n Result
GetCompartmentDefinitions 23 List<CompartmentDefinitionDto> — EpbId, HP, Weight, VisualWeight, Properties[{Key,Value}], CrownPrice, T1/T2/T3_MetalPrice (126 items)
GetShopItems 39 List<ShopItemDto> — DefinitionName, Amount, BuyPrice[PriceDto] (54 items)
GetResearchTree 32 ResearchTreeJsonDto{Roots[3], Nodes[98]}
GetDefaultWalkerBlueprints 28 default walker blueprints
GetServers / GetNews / GetDatabaseGuid / GetCharacters / GetStorage 29/21/26/2/20 server list / news / db guid / your characters / wallet+inventory

PriceDto = {ItemDefinition (currency item id), Amount}; names resolved via extracted/item_names.json.

How to run

venv/bin/python reverse/master_scrape.py --selftest        # offline
venv/bin/python reverse/master_scrape.py                   # dry run (prints the exact frames)
venv/bin/python reverse/master_scrape.py --go --data       # login + global data  -> extracted/master_*.json
venv/bin/python reverse/master_scrape.py --go --user       # + GetStorage / GetCharacters

Token from reverse/.secrets/playfab_token.json (gitignored): needs SessionTicket (fresh PlayFab login) + ClientVersion (current build's commit hash). Output → extracted/master_<Action>.json.