# 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": , "ClientMessageType": , "Action": , "Message": } 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` = `{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:///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:///gameclient/connect` **with HTTP upgrade header `Authorization: `** (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=, Message:null}`; reply `Message` carries the result. Verified live: | Action | n | Result | |---|---|---| | GetCompartmentDefinitions | 23 | `List` — EpbId, HP, Weight, VisualWeight, **Properties[{Key,Value}]**, CrownPrice, T1/T2/T3_MetalPrice (126 items) | | GetShopItems | 39 | `List` — 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_.json`.