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:
83
docs/TRAMPLER.md
Normal file
83
docs/TRAMPLER.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Trampler / walker blueprints — structure, hashes, footprints, rendering
|
||||
|
||||
All findings from this session. Data-derived unless marked otherwise. Tooling in `reverse/`.
|
||||
|
||||
## Where blueprints come from
|
||||
- **Your own trampler**: master-server `GetTrampler` (Action 16, `Message`=characterId) or PlayFab
|
||||
`TitleData` (`TramplerBlueprint3`, a default). Shape = `WalkerBlueprintDto`.
|
||||
- **Another crew member's / the host's**: `GetExpedition` (Action 7) → `ExpeditionDto.Trampler`
|
||||
(a `WalkerDto`), or `GetExpeditionWalker(characterId)`. `WalkerDto` is the same build in a
|
||||
slightly different wrapper (see conversion below).
|
||||
- **Local saved walkers**: `.wbt` files in `…/Hologryph/Sand/Data/Walkers/*.wbt` (slightly different
|
||||
serialization — parsed separately; not covered here).
|
||||
|
||||
## `WalkerBlueprintDto` (the blueprint)
|
||||
```
|
||||
Id, UniqueId, Version,
|
||||
Chassis: CompartmentBlueprintDto,
|
||||
Compartments: [CompartmentBlueprintDto], # ORDER IS MEANINGFUL = the game's order (matches Id field)
|
||||
Connections: [ConnectionBlueprintDto],
|
||||
CompartmentsHash, DefinitionsHash, ConnectionsHash # 3 MD5 hashes
|
||||
```
|
||||
- `CompartmentBlueprintDto = {Id, EpbId, CellCoordinate{x,y,z}, DecorationsInfo, Rotation,
|
||||
CompartmentHash, DefinitionHash}` — `CellCoordinate` is the **origin/anchor cell**; `y` = floor.
|
||||
- `ConnectionBlueprintDto = {Id, EpbId(null), ActualDirection{x,y,z}, GridCoordinate{x,y,z},
|
||||
SlotType, State}`.
|
||||
- Compartment **order = the game's order** (verified: cargo C1–C8 matched in-game). Same as the `Id`
|
||||
field; arrays preserve it.
|
||||
|
||||
## `WalkerDto` → `WalkerBlueprintDto` conversion (`reverse/walkerdto_to_blueprint.py`)
|
||||
`WalkerDto = {Id, FirstNameIndex, SecondNameIndex, BlueprintId, BlueprintUniqueId, Chassis,
|
||||
Compartments:[CompartmentDto], Connections:[ConnectionDto]}` where `CompartmentDto={Id, Blueprint}`
|
||||
and `ConnectionDto={Id, Blueprint}`.
|
||||
- The WS form serializes **enums as ints and omits null EpbId**; the blueprint/hash ("storage") form
|
||||
uses **enum NAME strings** and includes `EpbId:null`. Convert: unwrap `.Blueprint`, map enums
|
||||
int→name, add `EpbId:null`. Enum maps (from dump.cs): `ConnectionSlotType`
|
||||
DOOR0/HATCH1/STRUCTURE2/BALCONY3/DECK4; `ConnectionState` DEFAULT0/DOOR1/OPEN2;
|
||||
`ConnectionsCount` FULL0/PARTIAL1/ERROR2.
|
||||
- Verified by a **storage→WS→storage round-trip** on the sample (byte-identical, hashes reproduce).
|
||||
|
||||
## The 3 hashes (`reverse/trampler_hashes.py`)
|
||||
`MD5Utility.ComputeHash(obj) = MD5( UTF8( JsonConvert.SerializeObject(obj) ) )` → **uppercase hex,
|
||||
no separators** (`X2`). Newtonsoft compact: no whitespace, PascalCase, declaration-order keys, nulls
|
||||
included, enums as **name strings**.
|
||||
- **`CompartmentsHash = MD5(compact-JSON(Compartments))`** — VERIFIED.
|
||||
- **`ConnectionsHash = MD5(compact-JSON(Connections))`** — VERIFIED (this was the previously-hard #3).
|
||||
- `DefinitionsHash` = MD5 of the actual `CompartmentDefinitionDto`s — PROVISIONAL (hashes the
|
||||
definitions, not blueprint fields; verify live vs a current `GetTrampler` + `GetCompartmentDefinitions`).
|
||||
|
||||
## Footprints (`extracted/CompartmentsDatabase.json` → `compartments[].cells[]`)
|
||||
Each compartment def has `cells[] = {position{x,y,z}, sockets{dir:{slotType:…}}, volumeOccupied,
|
||||
requireSupport}` in the compartment's **local frame**. World cells = rotate local `(x,z)` by the
|
||||
blueprint `Rotation`, add origin; `y` world = origin.y + local.y.
|
||||
- **Rotation convention (verified by 0-overlap check across 56 solid cells):**
|
||||
`90→(z,-x), 180→(-x,-z), 270→(-z,x)`.
|
||||
- `volumeOccupied=true` = solid cell; `false` = non-solid (e.g. the reactor's north half-circle dome).
|
||||
- Reactor `Round_Metal_2x1` is actually a 2-wide tower **rising y0→y5** with a 2×2 solid base + a
|
||||
1-row dome to the north — the EpbId "2x1" is not the real footprint.
|
||||
|
||||
## Connections (doors / hatches) — blueprint `Connections[]`
|
||||
Per slot: `GridCoordinate` (world cell), `ActualDirection` (face), `SlotType`, `State`.
|
||||
- **DOOR** (horizontal, `dir.y==0`): `State` DOOR/OPEN = real door, DEFAULT = wall.
|
||||
- **HATCH** (vertical, `dir.y==±1`): `State` DOOR = hatch between floors.
|
||||
- STRUCTURE / DECK / BALCONY = structural/walkable connections.
|
||||
|
||||
## Weapons
|
||||
- `BattleRam_2x2` = **rams** (no firing arc — battering rams). `TurretSlot_*` = turret mounts;
|
||||
`…OpenCorner…`/`…MetalCorner…` = **45° corner** mounts, others = straight/cardinal.
|
||||
- **Firing facing is NOT in the data** (weapon `Properties` empty; `CompartmentsDatabase` has no
|
||||
aim/forward/angle field). The renderer derives facing from the **`Rotation` field** + a
|
||||
user-supplied calibration (CALIBRATION, not pure data): straight rot0 = South, corner rot0 = SW,
|
||||
+90° = clockwise → `face = base − rotation` (base: straight 90°, corner 135°), arc = ±90°.
|
||||
|
||||
## Renderer (`reverse/render_trampler.py`)
|
||||
Per-floor (y −1…3) PNG: real footprints (solid vs faded non-solid), doors/hatches, single-tile guns
|
||||
with ±90° firing wedges, rams, cargo numbered **C1–C8** in JSON order. Outputs
|
||||
`extracted/host_trampler_*.png`. Reactor column above deck 3 and legs below the base are clipped.
|
||||
|
||||
## Other scrape tooling (this session)
|
||||
- `reverse/master_scrape.py` — master-server replay (two-socket `/login` + `/connect`; see
|
||||
[MASTER_SERVER.md](MASTER_SERVER.md)).
|
||||
- `reverse/{capture_hosts,ws_scrape,playfab_scrape,noise_filter,resolve_decomp}.py`.
|
||||
- Ghidra: `analyzeHeadless … -postScript decomp_targets.py|disasm_targets.py` (offset bug fixed:
|
||||
rva = il2cppdumper `Address` directly, **no −0x1000**).
|
||||
Reference in New Issue
Block a user