- .gitignore: ignore /extracted/ (regenerable game data) and /tmp/ (scratch) - git rm --cached the 38 extracted/ files: untracked but left on disk, not deleted - master_scrape.py: add GetExpedition=7 to ACTIONS (was missing; pulls ExpeditionDto.Trampler) - docs: mark master-server /connect blocker cleared 2026-06-16 (server back up); server-side upsert hash validation remains untested (live re-test not yet run)
10 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 5 hashes (reverse/trampler_hashes.py, walker/walker_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. A WalkerBlueprintDto carries 3 top-level hashes; each
CompartmentBlueprintDto (chassis + every part) carries 2 more.
| Hash | Scope | What it hashes | Offline-computable? |
|---|---|---|---|
CompartmentsHash |
top-level | MD5(compact-JSON(Compartments list)) |
YES — VERIFIED |
ConnectionsHash |
top-level | MD5(compact-JSON(Connections list)) |
YES — VERIFIED |
DefinitionsHash |
top-level | MD5(JSON(Compartments→CompartmentDefinitionDto)) |
NO — server-sourced |
CompartmentHash |
per-part | placement fingerprint (constant per EpbId+placement) | NO from scratch — harvest |
DefinitionHash |
per-part | MD5(JSON(that part's CompartmentDefinitionDto)) |
NO — server-sourced |
- The two Definition hashes (
DefinitionsHash+ per-partDefinitionHash) hash the rich server-sideCompartmentDefinitionDto— which is NOT in the blueprint and NOT equal to the server'sGetCompartmentDefinitionspricing DTO (brute-forcing every field subset of that DTO reproduced neither). They can only be harvested (every part placed in the in-game editor writes them into the.wbtsave) →extracted/definition_hashes_known.json,walker/harvest_hashes.py. CompartmentsHashis a hash over the Compartments list including each part'sCompartmentHash+DefinitionHashfields — so wiping per-part Definition hashes also changesCompartmentsHash.- Per-part
CompartmentHashis constant per EpbId across placements (verified across 30 harvested EpbIds); per-partDefinitionHashis constant per EpbId except it varies withDecorationsInfocontent (onlywalker_compDeck_Square_Open_1x1_epbseen to differ so far).
Hash lifecycle & purpose — round-trip experiment (2026-06-16, live client)
Test: built copies of a known walker offline with hashes wiped, loaded them in the game, watched
Player.log (CheckValidBlueprint: ERRORS {0}, VERSION:{1}, {2} = haveBuildingErrors,
isVersionValid, UniqueId), then opened/saved in the in-game editor and re-decoded.
| Variant | Comp/Conn hashes | Definition hashes | ERRORS |
VERSION |
|---|---|---|---|---|
| Definition hashes only wiped | valid | wiped | False | True |
| all 5 wiped | wiped | wiped | False | False |
| untouched original (current build) | valid | valid | False | True |
| genuinely old-version walker | valid | valid | False | False |
Findings (data-derived):
- The client recomputes ALL FIVE hashes locally on save from its own
CompartmentsDatabase+CompartmentDefinitionDtocontainer. Saving an all-wiped walker produced hashes byte-identical to the original source (same build ⇒ same hashes). So the hashes carry no per-walker secret and are a pure function of the build. - Open/edit tolerates blank hashes: an all-wiped walker loads, lists, and opens in the editor with no error; opening + closing without saving rewrites nothing (file stayed byte-identical). Hashes are (re)written only on save.
- Every save mints a new file UUID and a new
UniqueId(observed 3×). isVersionValid(theVERSIONflag) depends only on the structural hashes (CompartmentsHash/ConnectionsHash), not on the Definition hashes (def-only-wiped was stillVERSION:True).haveBuildingErrors(ERRORS) is a separate structural check, independent of every hash.
Purpose (mechanism from dump.cs, not inferred): WalkerBlueprintContainer.ValidateVersion( ICompartmentsDatabase, ICompartmentDefinitionDtoContainer) sets isVersionValid/haveBuildingErrors.
It takes the current database + definitions as input ⇒ the stored hashes are a cached fingerprint
re-checked against a fresh recompute: when the game's part data changes across a patch, the recompute
won't match the stored hash and the walker is flagged stale (VERSION:False, → migrate/re-save). They
are integrity / version-staleness markers, not security — plain unsalted MD5 we reproduce offline;
the client just regenerates them. From the client editor's view the Definition hashes appear inert
(no effect on ERRORS or VERSION); whether the server validates them on upsert is still untested
(the master-server /connect blocker is cleared as of 2026-06-16 — server back up — but the live
re-test has not been run yet).
Practical upshot for offline editing: edit the .wbt, then either recompute the two structural hashes
yourself (walker/walker_hashes.py, exact) or just let one in-editor save regenerate all five.
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).
Open questions
- Server-side upsert validation — UNTESTED. We've only confirmed the client tolerates wiped/blank
hashes (loads, opens, edits, and regenerates all 5 on save). Whether the master server validates
the hashes (esp. the server-sourced
DefinitionsHash/ per-partDefinitionHash) when a blueprint isUpsertTrampler'd — i.e. whether it recomputes, rejects, or stores blanks — is not yet known. It was previously blocked by the master server throwingSystem.NullReferenceExceptionand closing the socket on/connect(no successful login); that blocker is cleared as of 2026-06-16 — the master server is back up, so the re-test is now unblocked but has not been run (awaiting explicit go-ahead for the live op). Re-test: upsert a walker with Definition hashes blank, thenGetTramplerit back and diff. See MASTER_SERVER.md.