From 9e8b5ee2ea37560fac32380e5997465d25652c77 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 21:31:47 +0200 Subject: [PATCH] Make loadouts design-only and settle Cut 10's open rulings Operator rulings: - A loadout stores item designs only, with no manufacturer or product. - Restoring one picks the first available product of each design under the generation rule, charges the sum of design prices, and is all or nothing. Cut 10 starts by pinning the CultLib 1.0.59 correction release. It then generalizes clear-boss-hulls into one dangling-ref command that clears the two DemandProfile refs, leaving the three AmmoType refs for the Studio click-through. Co-Authored-By: Claude Opus 5 --- docs/cultcache-migration-cut.md | 240 ++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 102 deletions(-) diff --git a/docs/cultcache-migration-cut.md b/docs/cultcache-migration-cut.md index 033464ff2..db02a5007 100644 --- a/docs/cultcache-migration-cut.md +++ b/docs/cultcache-migration-cut.md @@ -68,24 +68,41 @@ Recorded, not fixed (the importer is deleted in Cut 10): never fire on this data. - A slot with no matching member is copied raw. -The six dangling refs await the operator. CultLib `b85a828` (tags -`cultlib-unity-v1.0.58` and `caching-unity-v1.3.0`) gives drawers a read-only -`CultInspector.Record` and writes an unset `CultRecordRef` as nil. Cut 10 pins -it. Cut 10 is not started; it waits on Q10-3. +CultLib `b85a828` (tags `cultlib-unity-v1.0.58` and `caching-unity-v1.3.0`) +gives drawers a read-only `CultInspector.Record`. Soul found that it writes an +unset `CultRecordRef` as a nil map key, which Python's msgpack rejects. The +correction release (1.0.59 / Studio 1.3.1, in progress) makes `""` the +canonical unset wire form, and readers still accept nil. Cut 10 pins the +correction release. Cut 10 is not started. Rulings (operator, 2026-09-14): - **Q9-1 A:** `InputLayout` is catalog state, imported by Cut 9. -- **Q10-1:** loadouts stay, as player-store documents holding only blueprints - (design and product refs per hull cell). They are materialized against the - current galaxy at load, which keeps them portable. Q10-3 (materialization - rules) is open. +- **Q10-1:** loadouts stay, as player-store documents holding only item designs: + the hull's design, and a design ref, hull cell and rotation per slot. A design + names no manufacturer, and a ship build does not care who makes the parts. + Loadouts are materialized against the current galaxy at load, which keeps + them portable. - **Q10-2 A:** the local `PlayerSettings.msgpack` is discarded, with no importer. +- **Q10-3, all A.** Materialization: + - uses `LoadoutGenerator.IsAvailable` for availability (the maker is present + and allied with the docked station's faction); + - picks the first available product of each design; + - costs the sum of design prices; + - is all-or-nothing: on any failure, no ship, no charge, and every failing slot listed. +- **Cut 9's six dangling refs, option B:** + - Cut 10 clears the two `CompoundCommodityData.DemandProfile` keys, which + point at the deleted Conscientiousness and Neuroticism + `PersonalityAttribute`s. They are on Mouth Adapting Gummy Molars (2 keys) + and Neural Lace (1 key), and nothing reads `DemandProfile`. + - The three `WeaponData.AmmoType` refs, on DeathCluster, FastBlast+- and + pretty pretty bang bang, point at the deleted Auto and Charged Shotgun Ammo. + The operator fixes them in the Studio click-through. - **CultLib follow-ups decided alongside:** - - Studio 1.3.0 hands drawers the owning record, read-only. - - An unset `CultRecordRef` reads back exactly as written, with sibling-runtime - parity checked. - - Cut 10's schematic-shape drawer restores the texture underlay and hardpoint - tints once Studio 1.3.0 is tagged. + - Studio hands drawers the owning record, read-only (1.3.0, corrected in 1.3.1). + - An unset `CultRecordRef` reads back exactly as written, with + sibling-runtime parity checked. The canonical wire form is `""`, per the + 1.0.59 correction. + - Cut 10's schematic-shape drawer restores the texture underlay and hardpoint tints. CultMath 0.2.1 (`f060536` on `main`, tag `cultmath-unity-v0.2.1`, lightweight) fills Q8-1's gaps: @@ -491,7 +508,7 @@ state. | Entity, Ship, OrbitalEntity, Zone, Galaxy | live simulation | not records | tags 14 and 20 deleted | | PlayerSettings | Player | `[CultGlobal]` | name, tutorial flag, credits | | InputLayout | Catalog | document keyed by layout name | authored keyboard geometry; player rebinds live in `PlayerSettings` (Q9-1 A); imported by Cut 9 from `GameData\KeyboardLayouts` | -| Loadout | Player | document named by `[CultName]` | blueprints only (design and product refs per hull cell); outlives runs (Q10-1) | +| Loadout | Player | document named by `[CultName]` | item designs only (a design ref per hull cell, no manufacturer); outlives runs (Q10-1) | Save points at Aetheria `20db3a93` (`Gameplay\ActionGameManager.cs` unless noted): - **Player settings:** `SavePlayerSettings` (`:70-73`) writes @@ -2182,15 +2199,53 @@ ended at `9bdf6ef2`) and CultLib `0f2c1f0`, the pin in `Directory.Build.props`. ### Cut 10. Runtime cutover, importer removal, docs Refreshed 2026-09-14. `file:line` is at Aetheria `20db3a93`, except the -AetherDb and test edits made by Cut 9. +AetherDb and test edits made by Cut 9 (landed `70fbaca1`, recorded `e3af23fa`). - **Repo/branch:** same, after Cut 9. -- **Rulings (operator, 2026-09-14):** - - **Q10-1: loadouts are kept, as blueprint documents.** The operator: loadouts - are essential; what matters is the blueprint of each equipped item, and - materialization depends on the current galaxy so that loadouts stay portable. +- **Rulings (operator, 2026-09-14):** the loadout ruling and Q10-2 A, Q10-3 all + A, and dangling-ref option B, as recorded in the status header. - **Q10-2 A:** the local `GameData\PlayerSettings.msgpack` is discarded. First launch writes defaults to `player.cc`, and nothing imports the old file. +- **Step 1, the CultLib pin.** This is Cut 10's first commit. + - Move `F:\Projects\CultLib` to the commit tagged `cultlib-unity-v1.0.59` and + `caching-unity-v1.3.1`, and leave it clean. + - Set `Directory.Build.props:5` `CultLibRevision` to that commit's SHA, which + is set when the tag lands. The pin guard refuses anything else. + - `Packages\manifest.json:53-54` become + `…/src/GameCult.Unity/Assets/Caching#caching-unity-v1.3.1` and + `…/unity/org.gamecult.cultlib#cultlib-unity-v1.0.59`. + - Why 1.0.59 and not 1.0.58: 1.0.58 writes an unset `CultRecordRef` as a nil + map key, which Python's msgpack rejects. 1.0.59 writes `""` and reads nil. + - Verify: the headless build, `dotnet test tests\Aetheria.Shared.Tests`, and + the batchmode compile. + - `Aetheria.cc` does not have to be rewritten for the pin, because readers + accept nil. Step 2's commit rewrites it anyway, in the canonical form. +- **Step 2, dangling refs (option B).** This is one catalog `Commit`, landed + before any runtime change. + - **Tool:** `tools\AetherDb\Program.cs:200-226`: `ClearBossHulls` becomes + `ClearDangling(string[] members, bool apply)`, and `case "clear-boss-hulls"` + (`:26`) becomes `case "dangling"`. It stays one command; no second one-off tool. + - It opens `AetherDb.Open(catalogWritable: apply)`. + - It walks every catalog record's `CultRecordRef` members by reflection: + plain refs, list elements, ref-keyed dictionary keys, and nested + `[MessagePackObject]` and `[Union]` values such as + `EquippableItemData.Behaviors`. + - It prints each ref that resolves to nothing as + ` -> `. + - Each `clear ` argument unsets that member's dangling + refs, or removes that dictionary's dangling keys, on the record in hand. + - With `apply`, all changed records land in one `db.Cache.Commit`. Without + it, the command is a dry run. + - `Faction.BossHull` stays a clearable member, so the old command's use is kept. + - **Run:** `dotnet run --project tools\AetherDb -- dangling clear CompoundCommodityData.DemandProfile apply`. + - It clears 3 keys: 2 on Mouth Adapting Gummy Molars and 1 on Neural Lace. + - **Verify:** + - `-- dangling` then lists exactly three refs: `WeaponData.AmmoType` on + DeathCluster, FastBlast+- and pretty pretty bang bang. + - `census`, `factions` and `hardpoint-fit` still equal the Cut 9 results + (same normalization). + - `git diff --stat` shows only `GameData/Aetheria.cc` and `Program.cs`. + - The three AmmoType refs stay for the operator's Studio click-through (below). - **Loadouts today** (all of it is replaced): - "Save Loadout" (`InventoryPanel.cs:170-174`) calls `SaveLoadout` (`ActionGameManager.cs:227-230`). That writes a whole `EntityPack` to a @@ -2207,44 +2262,23 @@ AetherDb and test edits made by Cut 9. - An `EntityPack` is the wrong unit for a portable loadout. It carries rolled units (`Quality`, `Ingredients`, `Durability`, `ItemInstance.cs:36-43, 76`), cargo, children, faction, position and persisted behaviour state. -- **What a blueprint is in Aetheria today.** There is no blueprint type. - - A buildable unit is named by an `EquippableItemData` design, plus, when a - manufacturer built it, the `FactionProductData` product it was built as - (`CraftedItemInstance.Product`, `ItemInstance.cs:43`). +- **What a loadout holds.** A loadout slot is an `EquippableItemData` design. + - A design names no manufacturer. Which manufacturer's product builds it is a + fact of the current galaxy, not of the build. + - The unit's `FactionProductData` (`CraftedItemInstance.Product`, + `ItemInstance.cs:43`) is not captured. - `ItemManager.CreateInstance(FactionProductData)` (`ItemManager.cs:164-187`) - builds a unit from a product. `CreateInstance(CraftedItemData)` - (`:144-160`) builds one from a bare design. - - Units made from a bare design (console `give`, `ActionGameManager.cs:497`) - carry no product. - - A loadout blueprint is therefore the pair (design ref, optional product ref). - Both are catalog records, so the pair is portable across runs and galaxies. -- **Q10-3, operator: what "depends on current galaxy contents" means at load.** - - **Availability:** - - A: the generator's rule. A product is available when its manufacturer is - in the galaxy and allied with the docked station's faction; the prelude - and a null galaxy make everything available - (`LoadoutGenerator.IsAvailable`, `LoadoutGenerator.cs:152-154`). - - B: only units actually stocked in the docked station's cargo - (`LoadoutGenerator.cs:97-105` stocks it). - - C: no availability rule; any catalog product. - - **A blueprint with no product** (design only): - - A: any available product of that design; the first in catalog order that - passes availability. - - B: build from the bare design. - - **Cost:** - - A: the sum of design `Price`, as `EntityPack.Price` charged. - - B: the sum of `ItemManager.GetPrice` of the built units, which includes - the quality roll. - - **Partial success:** - - A: all-or-nothing. Any failing slot means no ship, no charge, and every failure reported. - - B: build what resolves and report the rest. - - **Recommended: A on all four.** - - Availability A reuses the one rule generation already owns. - - It keeps a loadout from materializing gear the galaxy has no maker for. - - B would turn restore into shopping, which the trade menu already owns. - - All-or-nothing keeps restore from half-charging for an unflyable hull. - - The spec below assumes A throughout. Materialization is one function, so a - different ruling changes only `Loadouts.Materialize` and its test. + builds the unit at load. +- **Materialization rules (Q10-3, all A):** + - **Availability:** `LoadoutGenerator.IsAvailable` (`LoadoutGenerator.cs:152-154`). + The product's manufacturer must be in the galaxy and allied with the docked + station's faction. The prelude and a null galaxy make everything available. + - **Picking a product:** the first product, in `GetAll()` + order, whose `Design` is the slot's design and which passes availability. + Slots carry no product, so this always applies. + - **Cost:** the sum of the design `Price`s, as `EntityPack.Price` charged. + - **Failure:** all-or-nothing. Any failing slot means no ship, no charge, and + every failing slot listed. - **Deletes first:** - **`ActionGameManager.cs`:** - `:58-73`: the `PlayerSettings.msgpack` getter, `_playerSettingsFilePath` @@ -2326,24 +2360,17 @@ AetherDb and test edits made by Cut 9. public class Loadout { [CultName, Key(0)] public string Name; - [Key(1)] public LoadoutBlueprint Hull; + [Key(1)] public CultRecordRef Hull; [Key(2)] public List Slots = new List(); [Key(3)] public int[][] WeaponGroups; // indices into Slots } - [MessagePackObject] - public class LoadoutBlueprint - { - [Key(0)] public CultRecordRef Design; - [Key(1)] public CultRecordRef Product; // unset when the unit carried none - } - [MessagePackObject] public class LoadoutSlot { [Key(0)] public int2 Position; // hull cell passed to Entity.TryEquip(item, int2) [Key(1)] public ItemRotation Rotation; - [Key(2)] public LoadoutBlueprint Blueprint; + [Key(2)] public CultRecordRef Design; } public static class Loadouts @@ -2351,7 +2378,7 @@ AetherDb and test edits made by Cut 9. public static Loadout Capture(ItemManager itemManager, Entity entity, string name); // One Commit to the player store; a loadout with the same name is replaced under its existing key. public static void Save(CultCache cache, Loadout loadout); - // All-or-nothing: returns a ship only when every blueprint resolved, was available and fitted. + // All-or-nothing: returns a ship only when every design resolved, had an available product and fitted. public static Ship Materialize(ItemManager itemManager, Galaxy galaxy, GalaxyZone zone, Faction stationFaction, Zone liveZone, Loadout loadout, List failures); public static int Price(ItemManager itemManager, Loadout loadout); @@ -2364,14 +2391,14 @@ AetherDb and test edits made by Cut 9. - The cell and the rotation are properties of the hull design, so they stay valid in any galaxy. - **`Capture`:** - - The hull blueprint comes from `entity.Hull`. + - `Hull` is `cache.RefOf(itemManager.GetData(entity.Hull))`. - There is one slot per distinct `EquippedItem` across `entity.Equipment`, - `CargoBays` and `DockingBays`, in that order. Each blueprint is - `Design = cache.RefOf(itemManager.GetData(item))` - and `Product = item.Product`. + `CargoBays` and `DockingBays`, in that order, with + `Design = cache.RefOf(itemManager.GetData(item))`. - `WeaponGroups` maps each group's items to slot indices. - - It records nothing else: no quality, ingredients, durability, cargo, - children, faction, name of the ship, position, settings or behaviour state. + - It records nothing else: no product or manufacturer, quality, + ingredients, durability, cargo, children, faction, ship name, position, + settings or behaviour state. - **`Save`:** - Looks up the existing record with `cache.GetByName(name)`, then `cache.Commit(b => b.Upsert(typeof(Loadout), loadout, existingKey))`. @@ -2382,15 +2409,12 @@ AetherDb and test edits made by Cut 9. zone, stationFaction, 0)` only to ask `IsAvailable`. That makes `LoadoutGenerator.cs:152` public; there is no second availability rule. - For the hull and then each slot, in slot order: - 1. Resolve `Design`. A missing record reports - `slot : design is not in the catalog`. - 2. If `Product` is set, resolve it. A missing record reports - `product is not in the catalog`. A product whose `Design` is - not this design reports a mismatch. - 3. Otherwise take the first available product of that design. If there - is none, report `no available product of `. - 4. An unavailable set product reports - ` is not made by anyone in this galaxy`. + 1. Resolve the design ref. A missing record reports + `slot : design is not in the catalog`; the hull reports + `hull` in place of the cell. + 2. Take the first available product of that design. If there is none, + report `slot : no available product of `. + - The pass does not stop at the first failure, so every failing slot is listed. - It builds units with `itemManager.CreateInstance(product)` (fresh rolls), sets `Rotation`, and constructs `new Ship(itemManager, liveZone, hull, itemManager.GameplaySettings.DefaultEntitySettings)`. @@ -2400,7 +2424,8 @@ AetherDb and test edits made by Cut 9. - Any failure returns `null`. The caller has changed nothing: no ship, no credits and no cache write. The function never substitutes a design or a slot. - - **`Price`** is the hull's design `Price` plus each slot's design `Price` (Q10-3 A). + - Units are built only after every slot resolved; a fit failure discards them. + - **`Price`** is the hull design's `Price` plus each slot design's `Price`. - **`AetheriaStores.cs:11`** (after Cut 9): `PlayerTypes = { typeof(PlayerSettings), typeof(Loadout) }`. - The player store is the right home. A loadout outlives runs, so the run store is wrong: `RunSave.Clear` would delete it at death. @@ -2421,19 +2446,21 @@ AetherDb and test edits made by Cut 9. `:184-192` (`SetParent`, `IsPlayerShip`, `DockingBay.DockedShip`, `CurrentEntity`, `Display`). - On failure: `Dialog` titled `Loadout cannot be built here`, with one line per failure. - - **Studio 1.3.0 drawer follow-up** (operator, 2026-09-14). It lands in this cut once - `caching-unity` 1.3.0 is tagged; if the tag is not out when the rest of - Cut 10 lands, it is its own follow-up commit. - - Pin `org.gamecult.caching.unity` to the 1.3.0 tag in `Packages\manifest.json`. + - **Schematic-shape drawer** (operator, 2026-09-14), on the Step 1 pin (Studio 1.3.1): - `InspectableSchematicShapeDrawer` (`Editor\CultCacheDrawers.cs:90-114`) - takes the owning record read-only through the 1.3.0 drawer signature. + reads the owning record through `inspector.Record` (read-only). - From that record it restores the Database Tools behaviour: the item's schematic texture under the grid, the height derived from the texture's aspect, and hull hardpoint tints. - Port that from `git show d3db1730:Assets/Scripts/CultCache/Editor/Inspectors/AetheriaInspectors.cs` (the drawer at `:158`) and delete the comment at `:90-92`. - - Verify with the batchmode compile and the operator's Studio click-through - on a `HullData`. + - Verify with the batchmode compile and the operator's Studio click-through. + The click-through checklist: + - a `HullData` shows its texture underlay, aspect-derived height and hardpoint tints; + - set or clear `WeaponData.AmmoType` on DeathCluster, FastBlast+- and + pretty pretty bang bang (each points at the deleted Auto or Charged + Shotgun Ammo); + - afterwards `-- dangling` lists zero refs. - **`ServerShared\Extensions.cs`:** add `public static uint StableHash(this string s)`. - The body folds UTF-8 bytes through CultMath's `pcg`: `uint h = 0x811C9DC5; foreach (var b in Encoding.UTF8.GetBytes(s)) h = pcg(h ^ b); return h;`. @@ -2492,13 +2519,13 @@ AetherDb and test edits made by Cut 9. - **Loadouts:** player store, owned by `Loadouts` in `Loadout.cs`. - **Inputs:** a live `Entity` for capture; the catalog, galaxy, zone and station faction for materialization. - - **Output:** `aetheria.loadout` records holding only design and product refs, hull cells and rotations. + - **Output:** `aetheria.loadout` records holding only design refs, hull cells and rotations. - **Only writer:** `Loadouts.Save`. - **Only builder of a ship from a loadout:** `Loadouts.Materialize`. It judges availability only through `LoadoutGenerator.IsAvailable`. - **Forbidden:** - - `EntityPack`, `ItemInstance`, run-type or `Faction` members on - `Loadout`, `LoadoutSlot` or `LoadoutBlueprint`. + - `EntityPack`, `ItemInstance`, `FactionProductData`, run-type or + `Faction` members on `Loadout` or `LoadoutSlot`. - A `.loadout` file. - A `LoadoutGenerator` fallback to "any manufacturer" at restore. That fallback exists for generation's required items (`:136-141`) and is not @@ -2557,36 +2584,44 @@ AetherDb and test edits made by Cut 9. - **`tests\Aetheria.Shared.Tests\LoadoutTests.cs`,** new. Its fixture catalog adds: - a `HullData` (Ship, 2×2 shape, one 1×1 hardpoint); - a 1×1 `GearData` for that hardpoint and a 1×1 `CargoBayData`; - - a product for each, made by the fixture faction. + - a product for each design, made by the fixture faction (materialization + needs one; the loadout never names it). - Settings are as in `Program.cs:307-312`, and `galaxy` is `null`, so everything is available. - **`LoadoutRoundTripsThroughAFreshCache`:** - Materializes a ship from a hand-built loadout (gear on the hardpoint cell, cargo on an interior cell), then `Capture`s and `Save`s it. - Disposes, reopens a fresh cache over the same files, and reads `GetByName`. - - The hull and slot blueprints, cells, rotations and weapon groups must be + - The hull design, slot designs, cells, rotations and weapon groups must be equal, and `Materialize` must succeed again with the same designs on the same cells. - `Save` under the same name leaves one `aetheria.loadout` record. - **`LoadoutHoldsNoRunOrGalaxyRefs`:** walks the member graph of `Loadout` by reflection. - - Every `CultRecordRef` has `T` assignable to `EquippableItemData` or `FactionProductData`. - - No member type is assignable to `ItemInstance`, `EntityPack`, `Faction` or - any `AetheriaStores.RunTypes` entry. + - Every `CultRecordRef` has `T` assignable to `EquippableItemData`. + - No member type is assignable to `ItemInstance`, `EntityPack`, + `FactionProductData`, `Faction` or any `AetheriaStores.RunTypes` entry. - It also checks, after a save, that `player.cc` holds the loadout and `run.cc` holds nothing new. - - **`MissingBlueprintReports`:** + - **`MissingDesignReports`:** - A loadout whose second slot's design key is absent from the catalog makes `Materialize` return `null`. - `failures` names that slot's cell and the key. - - A loadout whose product names a different design reports the mismatch. + - A loadout whose third slot's design has no product lists both failing + slots, in slot order. - Neither call writes to any store (all three file hashes unchanged). - **Verification:** - **Builds and tests:** `dotnet build Aetheria.Shared\Aetheria.Shared.csproj`, `dotnet build tools\AetherDb`, then `dotnet test tests\Aetheria.Shared.Tests` (6 + 6 + 3 tests). - **Unity:** the batchmode compile as in 8a reports no `error CS`. + - **Pin:** `git -C F:\Projects\CultLib describe --tags` names + `cultlib-unity-v1.0.59`, and the manifest names `v1.0.59` and `v1.3.1`. + The pin guard fails with CultLib at `b85a828`. + - **Dangling refs:** `dotnet run --project tools\AetherDb -- dangling` lists + exactly the three `WeaponData.AmmoType` refs (DeathCluster, FastBlast+-, + pretty pretty bang bang) until the operator's click-through, then zero. - **Negative greps,** over `Assets\Scripts tools tests` unless noted: - - `rg "SavedRun|SaveState|SaveLoadout|SaveZone|_loadoutPath|\.loadout\b|List Loadouts|PlayerSettings\.msgpack|KeyboardLayouts|ParseJson|AssociateInputKeys|SaveLayout|legacy-census|class Import"` is empty. + - `rg "SavedRun|SaveState|SaveLoadout|SaveZone|_loadoutPath|\.loadout\b|List Loadouts|PlayerSettings\.msgpack|KeyboardLayouts|ParseJson|AssociateInputKeys|SaveLayout|legacy-census|class Import|clear-boss-hulls|ClearBossHulls"` is empty. - `rg "class Loadout|Upsert\(typeof\(Loadout\)" Assets\Scripts` shows only `ServerShared\Loadout.cs`. - `rg "File\.(Read|Write)AllBytes" Assets\Scripts` shows only `Zone Display\GradientMapper.cs:116`, an editor texture export. - `rg "MessagePackSerializer\." Assets\Scripts` shows only `NewEntitySettings` and `EntitySerializer.cs:61-62`. @@ -2625,7 +2660,8 @@ AetherDb and test edits made by Cut 9. - The smoke passes, including steps 7 and 9. - Nothing in `Assets\Scripts` writes a game-state file. - Materialization changes nothing on failure. - - Only the operator decides any drift from Q10-3. + - Loadouts name no product. + - Only the operator decides any drift from the Q10-3 rulings. Steps needing Unity: - 6: CultLib's Unity project. @@ -2649,7 +2685,7 @@ estimates otherwise. | 7 | 2 doc lines | 2 doc lines, rebuilt DLLs | 4 tags | | 8 (8a+8b; unsplit estimate, predates the refresh: 8a is a using swap and conversion fixes, near zero net, and 8b carries the deletes and the five Aetheria drawers) | 1,771 − ~305 kept + 30,088 + 1,963 + ~60 ≈ **33,600** | ~40 `AetheriaStores`, ~150 drawers, ~180 tests, ~40 props/targets, ~400 attribute/reference edits, ~120 AetherDb ≈ **930** | −1 vendored MessagePack, −1 JsonKnownTypes, −1 asmdef; +2 UPM packages, +2 ProjectReferences, +1 test project; 20 schemas replace 1 union | | 9 | 0 code (8b already removed `OpenWithNameFiles`); data: `AetherDB.msgpack` (46,150 bytes), 12 name files, `ansi104.msgpack` and `.json`; 30 empty untracked folders | ~250 importer (deleted in Cut 10), 2 routing lines, 1 fixture move, 1 `.cc`, 1 `.gitattributes` and 1 `.gitignore` line | `InputLayout` routes to the catalog instead of the player store (Q9-1 A) | -| 10 | `ActionGameManager` ~35 (settings file I/O, `SaveState`, `SaveLoadout`/`Loadouts`/`_loadoutPath`, `SaveZone`), `InventoryPanel` 26 (rewritten), `InputDisplayLayout` ~120 (`ParseJson` 73, `AssociateInputKeys` 33, `SaveLayout` 6, reader 5), `SavedGame` constructor 34, `MainMenu` ~15, `PlayerSettings` 1, importer ~250, 8 doc lines | `RunSave` ~55, `Loadout.cs` ~130, `InventoryPanel` ~30, `StableHash` ~6, `ActionGameManager`/`MainMenu` ~25, ~250 tests; schematic drawer port ~40 once Studio 1.3.0 is tagged | -4 private file formats (`PlayerSettings.msgpack`, `KeyboardLayouts\*.msgpack`, `.loadout`, `.zone`); +1 schema (`aetheria.loadout`, player store); -1 `PlayerSettings` slot | +| 10 | `ActionGameManager` ~35 (settings file I/O, `SaveState`, `SaveLoadout`/`Loadouts`/`_loadoutPath`, `SaveZone`), `InventoryPanel` 26 (rewritten), `InputDisplayLayout` ~120 (`ParseJson` 73, `AssociateInputKeys` 33, `SaveLayout` 6, reader 5), `SavedGame` constructor 34, `MainMenu` ~15, `PlayerSettings` 1, importer ~250, 8 doc lines | `RunSave` ~55, `Loadout.cs` ~130, `InventoryPanel` ~30, `StableHash` ~6, `ActionGameManager`/`MainMenu` ~25, ~250 tests; schematic drawer port ~40; `clear-boss-hulls` generalized to `dangling` ~+30 | -4 private file formats (`PlayerSettings.msgpack`, `KeyboardLayouts\*.msgpack`, `.loadout`, `.zone`); +1 schema (`aetheria.loadout`, player store); -1 `PlayerSettings` slot | Expected net: CultLib **−1,650** lines of source and −450 of tests in Cut 2, then −325/+370 source and +720 tests in Cut 3 and +80/+110 in Cut 4: the two