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:
DownloadPizza
2026-06-16 00:35:17 +02:00
parent 3df0797acc
commit fc6b270fa8
29 changed files with 61574 additions and 0 deletions

75
docs/MASTER_SERVER.md Normal file
View 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`.