- 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)
5.4 KiB
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 PlayFabTitleData(TramplerBlueprint3, a default). Shape =WalkerBlueprintDto. - Another crew member's / the host's:
GetExpedition(Action 7) →ExpeditionDto.Trampler(aWalkerDto), orGetExpeditionWalker(characterId).WalkerDtois the same build in a slightly different wrapper (see conversion below). - Local saved walkers:
.wbtfiles 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}—CellCoordinateis 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
Idfield; 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, addEpbId:null. Enum maps (from dump.cs):ConnectionSlotTypeDOOR0/HATCH1/STRUCTURE2/BALCONY3/DECK4;ConnectionStateDEFAULT0/DOOR1/OPEN2;ConnectionsCountFULL0/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 actualCompartmentDefinitionDtos — PROVISIONAL (hashes the definitions, not blueprint fields; verify live vs a currentGetTrampler+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_2x1is 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):StateDOOR/OPEN = real door, DEFAULT = wall. - HATCH (vertical,
dir.y==±1):StateDOOR = 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
Propertiesempty;CompartmentsDatabasehas no aim/forward/angle field). The renderer derives facing from theRotationfield + 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).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 = il2cppdumperAddressdirectly, no −0x1000).