Files
SandTools/docs/TRAMPLER.md
DownloadPizza fc6b270fa8 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)
2026-06-16 00:35:17 +02:00

5.4 KiB
Raw Blame History

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 C1C8 matched in-game). Same as the Id field; arrays preserve it.

WalkerDtoWalkerBlueprintDto 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 CompartmentDefinitionDtos — PROVISIONAL (hashes the definitions, not blueprint fields; verify live vs a current GetTrampler + GetCompartmentDefinitions).

Footprints (extracted/CompartmentsDatabase.jsoncompartments[].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 C1C8 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 = il2cppdumper Address directly, no 0x1000).