# 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**).