diff --git a/CLAUDE.md b/CLAUDE.md index d2f41ca..730e8b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,12 @@ The four data sources, and which tools own them: - IL2CPP source of truth: `il2cpp/dump.cs` (Il2CppDumper output — signatures/RVAs only, no method bodies). `il2cpp/`, `ghidra/`, `snapshots/`, `bundles`, `Walkers`, `reverse/.secrets/` are git-ignored (large/regenerable/secret). Live PlayFab token: `reverse/.secrets/playfab_token.json`. +- **Game runtime data dir** (`%USERPROFILE%\AppData\LocalLow\Hologryph\SAND`, here + `/mnt/c/Users/DownloadPizza/AppData/LocalLow/Hologryph/SAND/`) holds: + - **`Player.log`** — Unity log; check it to see what the client did (walker file reads: + `[FS_STANDALONE] ReadAllFilesAsync … Path: …/Data/Walkers`; master-server handshake: + `[MasterServer] … Login / Connection failed to /connect`). `Player-prev.log` = previous run. + - **`Data/Walkers/*.wbt`** — the live walker saves (the `Walkers/` symlink points here). ## The `.wbt` walker save format (current focus) @@ -71,6 +77,16 @@ offline, `build_wbt.py pack` recomputes the two Compartment* hashes and **copies Definition* hashes from the source — correct as long as the *set of part definitions* is unchanged; it raises if you add a part whose `DefinitionHash` has never been harvested. +**Hash lifecycle (verified live 2026-06-16 — see `docs/TRAMPLER.md`):** the client **recomputes all 5 +hashes on save** from its own database (same build ⇒ byte-identical hashes; no per-walker secret — +plain unsalted MD5, they're integrity/version-staleness markers, **not** security). Wiping any/all +hashes is harmless: a walker with blank hashes still **loads, lists, and opens in the editor**, and +**one in-editor save regenerates everything** (and mints a new file UUID + `UniqueId`). The `VERSION` +flag in `Player.log`'s `CheckValidBlueprint: ERRORS {0}, VERSION:{1}` (=`WalkerBlueprintContainer +.ValidateVersion`, which recomputes against the *current* DB/definitions) tracks only the **structural** +hashes (Compartments/Connections); Definition hashes don't affect client validation. Server-side upsert +validation is untested (blocked by master-server `/connect` errors during the playtest). + Enum tables (from `dump.cs`): `ConnectionSlotType` 0 DOOR,1 HATCH,2 STRUCTURE,3 BALCONY,4 DECK · `ConnectionState` 0 DEFAULT,1 DOOR,2 OPEN · `ConnectionsCount` 0 FULL,1 PARTIAL,2 ERROR. Note the master-server **WS form** serializes these as integers and omits null `EpbId`; the storage/hash form diff --git a/docs/TRAMPLER.md b/docs/TRAMPLER.md index e4775a4..8f9221d 100644 --- a/docs/TRAMPLER.md +++ b/docs/TRAMPLER.md @@ -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, diff --git a/extracted/test_allhashwiped_4020ff89.wbt b/extracted/test_allhashwiped_4020ff89.wbt new file mode 100644 index 0000000..18a3faa Binary files /dev/null and b/extracted/test_allhashwiped_4020ff89.wbt differ diff --git a/extracted/test_nodefhash_6a804acb.wbt b/extracted/test_nodefhash_6a804acb.wbt new file mode 100644 index 0000000..a3c5636 Binary files /dev/null and b/extracted/test_nodefhash_6a804acb.wbt differ diff --git a/extracted/test_nodefhash_baseline.json b/extracted/test_nodefhash_baseline.json new file mode 100644 index 0000000..d44ba57 --- /dev/null +++ b/extracted/test_nodefhash_baseline.json @@ -0,0 +1,87 @@ +{ + "note": "Baseline hash state to diff against after the game loads/re-saves the test walker. Test walker 'Scientific Sentinel' has Definition* hashes wiped to ''. Watch which fields the game fills back in.", + "source_01260e82_Cranky_Third": { + "DefinitionsHash": "A94FBF71596E2E5FC3595F38C310AB68", + "CompartmentsHash": "54319EC1DDFFAF5CAE17A0FA8B0D6750", + "ConnectionsHash": "36777484B669177C2B402DC0830EF1BD", + "per_part": [ + { + "EpbId": "walker_compChassis_SmallLong4_Metal_2x3_epb", + "DefinitionHash": "64677F77ED55788CA579CDC5D5EDC53C", + "CompartmentHash": "D1EBE5DDB86BD898DA22A558F5199922" + }, + { + "EpbId": "walker_compSteering_Square_Open_1x1_epb", + "DefinitionHash": "8AD614A669C43EC06EC49FB2C0C9F56D", + "CompartmentHash": "93901C77B060FA462EB1C415D7304E07" + }, + { + "EpbId": "walker_compReactor_Round_Open_2x1_epb", + "DefinitionHash": "5744B05118A9CF470EEB77DC2FB8C62D", + "CompartmentHash": "BDADD1B66303873E30F8D031EE1F99A1" + }, + { + "EpbId": "walker_compCaptainCrew_Wood_1x1_epb", + "DefinitionHash": "37F659D0A1EDBFEF11138B059BA87630", + "CompartmentHash": "CA0A7339EC20ADAAD07B756BA3206F77" + }, + { + "EpbId": "walker_compDeck_Square_Open_1x1_epb", + "DefinitionHash": "04E304846A06AF8E3209DF1054D1E784", + "CompartmentHash": "0B8CC69BD1392E6EA7EA37BE5DE6C372" + }, + { + "EpbId": "walker_compDeck_Square_Open_1x1_epb", + "DefinitionHash": "04E304846A06AF8E3209DF1054D1E784", + "CompartmentHash": "0B8CC69BD1392E6EA7EA37BE5DE6C372" + }, + { + "EpbId": "walker_compSpecial_Entry_Frame_1x1_epb", + "DefinitionHash": "CB2209E5C634FBC37C242DE4ED91B70C", + "CompartmentHash": "9FB555D20A1BADBBD3AD73DFC989F9B6" + } + ] + }, + "test_6a804acb_Scientific_Sentinel_NODEFHASH": { + "DefinitionsHash": "", + "CompartmentsHash": "B0DC03D36083E9A6A7C86DFC1E87D978", + "ConnectionsHash": "36777484B669177C2B402DC0830EF1BD", + "per_part": [ + { + "EpbId": "walker_compChassis_SmallLong4_Metal_2x3_epb", + "DefinitionHash": "", + "CompartmentHash": "D1EBE5DDB86BD898DA22A558F5199922" + }, + { + "EpbId": "walker_compSteering_Square_Open_1x1_epb", + "DefinitionHash": "", + "CompartmentHash": "93901C77B060FA462EB1C415D7304E07" + }, + { + "EpbId": "walker_compReactor_Round_Open_2x1_epb", + "DefinitionHash": "", + "CompartmentHash": "BDADD1B66303873E30F8D031EE1F99A1" + }, + { + "EpbId": "walker_compCaptainCrew_Wood_1x1_epb", + "DefinitionHash": "", + "CompartmentHash": "CA0A7339EC20ADAAD07B756BA3206F77" + }, + { + "EpbId": "walker_compDeck_Square_Open_1x1_epb", + "DefinitionHash": "", + "CompartmentHash": "0B8CC69BD1392E6EA7EA37BE5DE6C372" + }, + { + "EpbId": "walker_compDeck_Square_Open_1x1_epb", + "DefinitionHash": "", + "CompartmentHash": "0B8CC69BD1392E6EA7EA37BE5DE6C372" + }, + { + "EpbId": "walker_compSpecial_Entry_Frame_1x1_epb", + "DefinitionHash": "", + "CompartmentHash": "9FB555D20A1BADBBD3AD73DFC989F9B6" + } + ] + } +} \ No newline at end of file