trampler: hash lifecycle RE'd via live round-trip (client recomputes all 5 on save)

- All 5 walker hashes are recomputed client-side on save from the local
  CompartmentsDatabase + CompartmentDefinitionDto (same build => byte-identical
  hashes; no per-walker secret). Wiping any/all hashes loads/opens/edits fine;
  one in-editor save regenerates everything (+ new file UUID & UniqueId).
- VERSION flag (WalkerBlueprintContainer.ValidateVersion) depends only on the
  structural Compartments/Connections hashes, not the Definition hashes; ERRORS
  is a separate structural check. Hashes = integrity/version-staleness markers,
  not security. Server upsert validation still untested.
- Document the 5-hash table (3 top-level + 2 per-part) and offline-computability
  in docs/TRAMPLER.md + CLAUDE.md; include experiment artifacts + baseline.
This commit is contained in:
DownloadPizza
2026-06-16 09:48:50 +02:00
parent aa7425221f
commit bd01d6753a
5 changed files with 163 additions and 6 deletions

View File

@@ -37,14 +37,68 @@ and `ConnectionDto={Id, Blueprint}`.
`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`)
## 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**.
- **`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`).
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-part `DefinitionHash`) hash the rich
server-side `CompartmentDefinitionDto` — which is **NOT** in the blueprint and **NOT** equal to the
server's `GetCompartmentDefinitions` *pricing* 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 `.wbt` save) → `extracted/definition_hashes_known.json`, `walker/harvest_hashes.py`.
- `CompartmentsHash` is a hash **over the Compartments list including each part's `CompartmentHash` +
`DefinitionHash` fields** — so wiping per-part Definition hashes also changes `CompartmentsHash`.
- Per-part `CompartmentHash` is **constant per EpbId** across placements (verified across 30 harvested
EpbIds); per-part `DefinitionHash` is constant per EpbId **except** it varies with `DecorationsInfo`
content (only `walker_compDeck_Square_Open_1x1_epb` seen 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` +
`CompartmentDefinitionDto` container. 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` (the `VERSION` flag) depends only on the structural hashes** (`CompartmentsHash`/
`ConnectionsHash`), **not** on the Definition hashes (def-only-wiped was still `VERSION: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
(blocked by master-server `/connect` NREs during this playtest).
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,