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)
This commit is contained in:
75
docs/MASTER_SERVER.md
Normal file
75
docs/MASTER_SERVER.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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](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
|
||||
|
||||
```bash
|
||||
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`.
|
||||
Reference in New Issue
Block a user