- 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)
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 live — reverse/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-socketClientMessageprotocol 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:
/login— connectwss://<region>/gameclient/login(NO auth header). SendClientMessage{Action=Login(0), Message=JSON(LoginRequest{SessionTicket, Title, Platform, ClientVersion})}:SessionTicket= the PlayFab SessionTicket (fromLoginWithSteam; see playfab_scrape.py)Title= PlayFab TitleId =56693;Platform=1(STEAM, int);ClientVersion= the build's git commit hash (e.g.fbe5e4370daf86d54933ff4786b3312acd8c2d98, fromPlayer.log'sSend Login … ver:'…'; changes every build — server may version-gate →Status=5 VERSION_MISMATCH).- Reply
Message=LoginUserResultDto{User{Id,PlayFabId,PlatformId,DisplayName}, SessionTicket}. Keep thatSessionTicket(the server session ticket). Close the socket.
/connect— connectwss://<region>/gameclient/connectwith HTTP upgrade headerAuthorization: <server SessionTicket>(raw, noBearer). SendClientMessage{Action=Connect(1), Message:"False"}(theisDebugbool). 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.