# 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`.