From db420981da234979821d930a57b8331eccd2d4d3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 17 Sep 2026 17:36:07 +0200 Subject: [PATCH] Map settings out of Unity serialization into CultCache globals Operator ruling: all settings live in CultCache globals. Unity-serialized CultMath fields load as zero, which cost mouse sensitivity and the rarity tier colours. Marks the playground map's fork S as superseded. Co-Authored-By: Claude Opus 5 --- docs/headless-playground-cut.md | 1 + docs/settings-globals-cut.md | 581 ++++++++++++++++++++++++++++++++ 2 files changed, 582 insertions(+) create mode 100644 docs/settings-globals-cut.md diff --git a/docs/headless-playground-cut.md b/docs/headless-playground-cut.md index 2af522d4e..b90621050 100644 --- a/docs/headless-playground-cut.md +++ b/docs/headless-playground-cut.md @@ -1149,6 +1149,7 @@ Options as mapped before the ruling: playground cannot kill anything by the rules the game uses. **S. The two Unity-only lifecycle settings.** +- **Superseded 2026-09-17** by `docs/settings-globals-cut.md` (operator: "All settings should be in globals"). Settings move to a catalog global read through CultCache; `AuthoredSettings` and the YAML reader are deleted, so the options below, the `Run.New` settings arguments and the `AuthoredSettings` uses above are history. Re-map this cut against that one before Hands. - (a) Move `StartingHullName`, `WormholeDistanceRatio` and (Cut 2) `LootDrop*` into `GameplaySettings`, with a text edit of `Settings.asset` while Unity is closed. - (b) Pass them as `Run` constructor arguments read from `GameSettings`; the playground diff --git a/docs/settings-globals-cut.md b/docs/settings-globals-cut.md new file mode 100644 index 000000000..9fa4704fd --- /dev/null +++ b/docs/settings-globals-cut.md @@ -0,0 +1,581 @@ +# Settings Into CultCache Globals: Cut Map + +Date: 2026-09-17 + +Status: Imagination pass, cut map. Nothing here has landed. Anchors are against +`codex/item-provenance` HEAD `00ebe46a` (which carries the separate mouse-sensitivity +move). Claims marked **(probe)** were run, not read off names: + +- a Python walk of every `MonoBehaviour`/`ScriptableObject` field graph in + `Assets/Scripts` (attribute-prefixed fields included), and of the scene, prefab and + asset YAML; +- a Python parse of `Settings.asset` mapped key by key onto the C# classes; +- a headless build of `tools/AetherDb` in a sparse scratch worktree of `00ebe46a`, with + CultLib at `a0813c6` as a detached scratch worktree. Both were removed afterwards, and + the main tree got no `bin`/`obj` writes. The probe sources were scratch-only and are + not kept; each result below says what it ran. + +Operator ruling, 2026-09-17: "We don't care for Unity's serializer, we have CultCache +sitting right there. All settings should be in globals." + +Rulings: none yet. Open: forks K, I, B, D, P (these block Cut 1), then M and X. +Doc sweep for Self: this map supersedes the mechanism in `docs/headless-playground-cut.md` +fork S ("a text edit of `Settings.asset`", `:1151-1159`, `:404-410`, `:693-694`). It also +changes the `Run.New(CultCache, GameplaySettings, ZoneGenerationSettings, PlanetSettings, ...)` +signature (`:415`), and that map's use of `AuthoredSettings` (`:176`, `:224`) goes away. +Once fork P is ruled, those settings come from the cache. + +## 0. Target + +Ends: +- Every authored setting lives in a CultCache global. The catalog holds design data + and the player store holds preferences. Unity's serializer holds no setting. +- One owner per setting. The Unity game, the headless sim, the tests and `AetherDb` + read the same record through the same port. No reader rebuilds the values by hand, + and none parses them out of YAML. +- Nothing authored is lost, and no authored value loads silently as zero. + +Invariants: +- Catalog globals are authored, never invented. `AetheriaStores.Open` keeps refusing a + populated catalog that is missing one, however it is opened (`AetheriaStores.cs:14-15`, + ruling behind `ab4ced0f`). +- A catalog global is never written at runtime. Runtime variations are copies owned by + the run or the entity. +- One home store per type, and a commit lands in one store. + +Not settings (the census below gives the evidence): +- Engine configuration: `ProjectSettings/*`, `Assets/Resources/InputSystem.inputsettings.asset`. +- Per-component presentation tuning on scene and prefab instances (fork M). + +## 1. Census + +### 1.1 `GameSettings` / `Settings.asset`, field by field + +`GameSettings` (`Assets/Scripts/Gameplay/GameSettings.cs:11`) is the only +`ScriptableObject` settings type. It is referenced from 15 YAML fields: +`Assets/Scenes/ARPG.unity` x12, `Assets/Scenes/Main Menu.unity` x2 and +`Assets/Prefabs/UI/Main Menu Canvas.prefab` x1. The asset guid is +`cd2344e2001a37544aab0960c164d4ff`. + +**(probe)** YAML-to-class mapping: every key under the `MonoBehaviour` maps to a field, +and no key is unplaced. The asset holds 266 scalar leaves, 15 `float3` components, 16 +`Color` components, 58 `Gradient` keys and 53 object references. One field is missing +from the YAML: `ZoneGenerationSettings.NeutralWandererCount`, which Unity loads as its +initializer, 2. The existing `AetherDb settings` command agrees: 0 unplaced, 0 missing. + +Classes: **C** is a catalog global (design data). **CP** is a catalog global holding +presentation data: authored and not a preference, but only Unity reads it. **E** is an +engine asset reference (forks I and B). **Dead** means no code reads it (checked with +`rg` over `Assets/Scripts`, `tools` and `tests`). + +| Field (`GameSettings.cs` line) | Type | Authored (`Settings.asset` line) | Readers (HEAD) | Class | +|---|---|---|---|---| +| `DefaultOverworldSoundbank`, `DefaultCombatSoundbank`, `DefaultBossSoundbank`, `AmbienceSoundBank` (13-16) | string | `:15-18` | none | Dead (fork X) | +| `StartingHullName` (17) | string | `LonginusX` (the code default is `Longinus`; `LonginusX` is in the catalog) | `ActionGameManager.cs:776` | C | +| `PickupLifetime` (18) | float | 30 | `ZoneRenderer.cs:572` | C | +| `LootDropProbability`, `LootDropVelocity` (19-20) | float | .25, 25 | `EntityInstance.cs:413,417,428` | C | +| `HeatstrokePhasingFloor`, `HeatstrokePhasingFrequency` (21-22) | float | 0, 5 | `ActionGameManager.cs:1236` | CP | +| `WormholeDistanceRatio` (23) | float | .75 | `ZoneRenderer.cs:230` (a game rule, per the headless map) | C | +| `DefaultViewDistance`, `MinimapZoneGravityRange` (24-25) | float | 4096, .45 | `ZoneRenderer.cs:178,526` | CP | +| `MinimapZoomLevels`, `DefaultMinimapZoom` (26-27) | float[], int | [250,500,1000,2000,4000], 3 | `ActionGameManager.cs:280-284`, `ZoneRenderer.cs:179` | CP | +| `IconSize` (28) | `ExponentialCurve` | .15/10/25 | `ZoneRenderer.cs:412` | CP | +| `AsteroidMeshCount`, `MinimapAsteroidSize`, `MinimapIconSize`, `PlanetRotationSpeed` (29-32) | int, float | 4, 3, .125, .1 | `ZoneRenderer.cs:342,361,147,497` | CP | +| `NameGeneratorSettings` (33) | ServerShared | `:43-46` | `MainMenu.cs:132,158` -> `Galaxy` | C | +| `TutorialGenerationSettings` (34) | ServerShared | `:47-56` | `MainMenu.cs:156`, `ActionGameManager.cs:772` | C | +| `TutorialBackgroundSettings`, `SectorBackgroundSettings` (35-36) | ServerShared | `:57-74` (`NoisePosition` at `:63,72`) | `MainMenu.cs:125-157` (**writes**, see 1.4) | C | +| `SectorGenerationSettings` (37) | ServerShared | `:75-79` | `MainMenu.cs:130` | C | +| `BodySettingsCollections` (38) | `{float MinimumMass; CelestialBodySettings[]}` | `:80-99`: 6 collections, 7 refs, all under `Assets/Plugins/Celestial Body/Solar System/` | `ZoneRenderer.cs:397` | E (fork B) | +| `PlanetSettings` (39) | ServerShared | `:100-155` | `ActionGameManager.cs:690` -> `Zone`, `ZoneRenderer.cs:391-536` | C | +| `ZoneSettings` (40) | `ZoneGenerationSettings` | `:156-217` | `ActionGameManager.cs:685`, `SectorRenderer.cs:71-72` | C | +| `GameplaySettings` (41) | ServerShared | `:218-326` | `ItemManager` (ctor at `ActionGameManager.cs:263`), plus Unity readers listed in Cut 1 | C | +| `HullHitColor` (42) | Color | `:327` | none | Dead (fork X) | +| `ArmorHitColor`, `HardpointHitColor`, `GearHitColor` (43-45) | Color | `:328-330`, every a=1 | `InventoryPanel.cs:497-499` | CP -> `float3` | +| `ArmorGradient`, `DurabilityGradient` (46-47) | Gradient | `:331-390` | `InventoryPanel.cs:790,792` | CP -> `float4[]` | +| `DefaultEnvironment` (48) | `ZoneEnvironment` | `:391-425` | `ActionGameManager.cs:203`, `VolumeSampling.cs:54` | C | +| `ItemIcons`, `WeaponTypeIcons`, `WeaponCaliberIcons`, `WeaponRangeIcons`, `WeaponFireTypeIcons`, `WeaponModifierIcons` (49-54) | Sprite[] | `:426-477` | `GetIcon`/`GetIcons` (`:56-84`) from `ActionBarSlot.cs:155-156`, `SchematicListElement.cs:27-44`; edited by `Editor/GameplaySettingsEditor.cs` | E (fork I) | + +**(probe)** Icon references, resolved guid to path through every `.meta` under +`Assets`: +- 33 resolve under `Assets/Resources/Sprites/Icons{,/Weapons,/Tech}`. +- 3 are intentional `{fileID: 0}` slots: `ItemIcons[13]`, `WeaponFireTypeIcons[0]` and + `WeaponModifierIcons[0]` (the None slot). +- **9 point at assets that do not exist**: `WeaponTypeIcons[1]` (`dc8c6f7a…`), + `WeaponTypeIcons[9]` (`9ae7f8b4…`) and every real `WeaponModifierIcons[1..7]`. This bug + predates the cut. It is recorded here, not fixed, and not invented over. + +Gradients convert without loss **(read, not built)**. Both gradients are `m_Mode: 0` +(Blend) with `m_NumAlphaKeys: 2`, and alpha is 1 at 0 and 1 at 65535. That is exactly +what the existing `UnityExtensions.ToGradient(float4[], sharp: false)` +(`CultCache/UnityExtensions.cs:29-36`) rebuilds. Color key time is a `ushort`, so `w = +ctime/65535f` goes back to the same `ushort`. The values are: +- `ArmorGradient`: `(.2244,.66,.2244, 0)`, `(1,1,1, 1)`. +- `DurabilityGradient`: `(.7264151,0,0, 0)`, `(1,1,.19999999, 29613/65535)`, + `(.2244,.66,.2244, 1)`. + +### 1.2 Other `Assets/Resources/*.asset` + +**(probe)** None of these five has a live script. For each, the `m_Script` guid matches +no `.meta` in `Assets` or `Library/PackageCache`, and no scene, prefab, material, asset +or `.cs` references the asset's own guid. `git log -S` on the script guids finds where +each script was deleted: + +| Asset | Script deleted | What it held | Class | +|---|---|---|---| +| `Atmosphere Settings.asset` | `AtmosphereConfig.cs`, `d61d2f19` "Raymarching Cleanup" (2022-07-13) | atmosphere scattering constants | dead presentation | +| `Volume Settings.asset` | the clouds renderer, `d61d2f19` | cloud texture refs and tiling | dead presentation | +| `Elysium.asset`, `Test Galaxy.asset` | `Galaxy.cs` and `GalaxyEditor.cs`, `0f33e541` "Sector Constellations" (2021-03-11) | the old galaxy generator's `MapData` (arms, twist, masses) | dead content (fork X) | +| `Icons.asset` | `UI/Icons.cs`, `81acaf2a` (2021-05-08) | plus and minus textures | dead presentation | + +`InputSystem.inputsettings.asset` is engine configuration and stays. + +### 1.3 CultMath fields Unity serializes (these load as zero) + +**(probe)** A walk of the field graph from every `MonoBehaviour`/`ScriptableObject` +through `[Serializable]` types, looking for CultMath `floatN`/`intN`/`boolN`/`quaternion` +leaves, finds these. A `Vector3` or `Vector2` field is not affected: the inline +`{x: …}` YAML values belong to those. + +| Field | Stored data | Loss | +|---|---|---| +| `ActionGameManager.Sensitivity` (float2, cf60fd07 `:101`) | `ARPG.unity:16548-16550` `(-.001, .001)` | Real. Fixed separately in `00ebe46a` (`PlayerInputSettings.Sensitivity`, `PlayerSettings.cs:76`). The scene key is now stale. | +| `GameSettings.GameplaySettings.Tiers[].Color` (`RarityTier.Color`, `Settings.cs:226`) | `Settings.asset:221-250`: 5 tiers | **Real.** Rarity colours render black in `TradeMenu.cs:193`, `PropertiesPanel.cs:436` and `ZoneRenderer.cs:567`. Cut 1 recovers them. | +| `MapRenderer.Position` (float2, `MapRenderer.cs:30`) | `ARPG.unity:10236-10238` `(0, 0)` | None. The stored value was zero, and the field is runtime pan state. | +| `FieldDriver.Push` (float2, `FieldDriver.cs:17`) | no key in `FieldShieldTest.unity` | None. It is runtime state, set at `:179`. | + +`FieldShieldTest.unity:1047-1049` also holds a stale `Throttle: (0,0)` for a field that +was deleted. It is harmless. **(probe)** No `[Serializable]` type on a Unity-reachable +path lost the attribute during the migration: the YAML map above placed every key. + +### 1.4 Writers into settings (forbidden once they are globals) + +- `MainMenu.cs:125` and `:148` assign `Settings.{Sector,Tutorial}BackgroundSettings.NoisePosition = Random.value * 1000` + on the shared authored object. The authored values `91.29439` and `536.5106` + (`Settings.asset:72,63`) look like play-mode residue from this writer. Run state already + has a home: `Galaxy.Background` is saved as `SavedGame.Background` (`SavedGame.cs:37`). +- **Entity settings aliasing.** `Loadout.cs:131` and `LoadoutGenerator.cs:46,61,76` pass + `GameplaySettings.DefaultEntitySettings` itself into `Entity`, and `Entity.cs:340` + stores it without copying. `InventoryMenu.cs:175` then writes + `CurrentEntity.Settings.ShutdownPerformance`. That write changes the default for every + generated entity, and once settings are a global it changes the cached catalog record. + Two other places copy: `ActionGameManager.cs:228-232` (`NewEntitySettings`, used by + `TradeMenu.cs:377`) and `EntitySerializer.cs:51-52`. Three copies and four aliases make + split authority. + +### 1.5 Component tuning on scene and prefab instances + +**(probe)** This lists every scalar, non-reference serialized field stored in `.unity` +and `.prefab` files for scripts in `Assets/Scripts`, grouped by script: +- **Presentation timing, layout and VFX:** 45 scripts. Examples: `ShieldAnimation` + curves, `ConstantLaser`/`Laser`/`HitscanEffect` curves, `InputDisplayLayout` colours, + `SectorMap`, `ZoneRenderer` tour, `TextButton`, `PropertiesList`, `MenuPanel` tab + colours, `ItemPickup` label fades. +- **Behaviour on prefabs that affects play:** `Projectile` (`Gravity`, `Drag`, + `DirectHitDamageMultiplier`), `GuidedProjectile` (`Thrust`, `TopSpeed`, split and + airburst), `Mine` (`Lifetime`, `BlastRange`, `ActivationDelay`), `TractorBeam`, + `GridObject`. These are per-prefab instance data. `docs/headless-playground-cut.md` + already moves simulation ownership of hits, loot and fields. +- **Run state misfiled as a component field:** `ActionGameManager.Credits`, 100000000 + in `ARPG.unity` against a code default of 15000000. It is the player's wallet + (`TradeMenu.cs:344-385`). It is not saved, so it resets every scene load. + +Fork M decides whether any of this counts as "settings". + +## 2. Global shape + +### 2.1 Document + +This is fork D, recommended (a). One catalog global in a new file, +`Assets/Scripts/ServerShared/GameSettings.cs`. The name is freed by deleting the Unity +`ScriptableObject`. Both classes cannot coexist: Assembly-CSharp would see two global +`GameSettings` types (CS0433). So the type change and the switch of every reader must be +one commit. + +```csharp +[CultDocument("aetheria.gamesettings", "1"), CultGlobal, MessagePackObject] +public class GameSettings +{ + // Simulation and generation + [Key(0)] public GameplaySettings GameplaySettings; + [Key(1)] public PlanetSettings PlanetSettings; + [Key(2)] public ZoneGenerationSettings ZoneSettings; + [Key(3)] public SectorGenerationSettings SectorGenerationSettings; + [Key(4)] public TutorialGenerationSettings TutorialGenerationSettings; + [Key(5)] public NameGeneratorSettings NameGeneratorSettings; + [Key(6)] public SectorBackgroundSettings SectorBackgroundSettings; + [Key(7)] public SectorBackgroundSettings TutorialBackgroundSettings; + [Key(8)] public ZoneEnvironment DefaultEnvironment; + [Key(9)] public string StartingHullName; + [Key(10)] public float PickupLifetime; + [Key(11)] public float LootDropProbability; + [Key(12)] public float LootDropVelocity; + [Key(13)] public float WormholeDistanceRatio; + // Presentation (authored; read only by Unity) + [Key(14)] public float HeatstrokePhasingFloor; + [Key(15)] public float HeatstrokePhasingFrequency; + [Key(16)] public float DefaultViewDistance; + [Key(17)] public float MinimapZoneGravityRange; + [Key(18)] public float[] MinimapZoomLevels; + [Key(19)] public int DefaultMinimapZoom; + [Key(20)] public ExponentialCurve IconSize; + [Key(21)] public int AsteroidMeshCount; + [Key(22)] public float MinimapAsteroidSize; + [Key(23)] public float MinimapIconSize; + [Key(24)] public float PlanetRotationSpeed; + [Key(25)] public float3 ArmorHitColor; + [Key(26)] public float3 HardpointHitColor; + [Key(27)] public float3 GearHitColor; + [Key(28)] public float4[] ArmorGradient; // xyz colour, w time; lowered by ToGradient() + [Key(29)] public float4[] DurabilityGradient; + // Engine asset paths (forks I and B, recommended (a)): "Assets/Resources/..." like ItemData.Icon + [Key(30)] public string[] ItemIcons; // [CultInspectorAssetPath] on each, indexed by enum as today + [Key(31)] public string[] WeaponTypeIcons; + [Key(32)] public string[] WeaponCaliberIcons; + [Key(33)] public string[] WeaponRangeIcons; + [Key(34)] public string[] WeaponFireTypeIcons; + [Key(35)] public string[] WeaponModifierIcons; + [Key(36)] public BodySettingsCollection[] BodySettingsCollections; // { [Key(0)] float MinimumMass; [Key(1)] string[] BodySettings } +} +``` + +**(probe)** A top-level Cult document must put an integer `[Key]` on every public member. +The CultLib contract check threw `…member GameplaySettings has no [Key]` on a +`keyAsPropertyName` probe document. Nested `[MessagePackObject(keyAsPropertyName: true)]` +classes are accepted as they are. A probe global with `[Key(0)] GameplaySettings`, filled +from the asset, wrote to a catalog and read back read-only with all 5 tiers. Legendary +came back `(1, .4, 0)`, and a re-serialize was byte-identical (1841 bytes). The existing +settings classes are kept unchanged. + +`StartingHullName`, `LootDrop*` and `WormholeDistanceRatio` stay top-level. Headless +fork S wanted them inside `GameplaySettings` so there would be one reader. Once +everything reads one global, the nesting buys nothing. + +### 2.2 Readers + +This is fork P, recommended (a). +- **Simulation.** `ItemManager` already holds the catalog cache, so it owns the read. + `ItemManager(CultCache itemData, ProvenanceLedger lots, Action logger)` sets + `Settings = itemData.GetGlobal() ?? throw …`. It exposes + `GameSettings Settings` and `GameplaySettings GameplaySettings => Settings.GameplaySettings`, + so the 55 `GameplaySettings` reads in ServerShared do not change. + `Zone` drops its `PlanetSettings` parameter (`Zone.cs:46`) and reads + `itemManager.Settings.PlanetSettings`. +- **Unity before a run** (`MainMenu`, `VolumeSampling`): one static, + `ActionGameManager.GameSettings => CultCache.GetGlobal()`, on the same + composition as `ActionGameManager.PlayerSettings` (`:66`). Inside a run the same + object is also reachable as `ItemManager.Settings`. Both paths read the one record, so + they cannot disagree. +- **Icons.** `GetIcon`/`GetIcons` move to Unity-side extension methods on `GameSettings` + in `CultCache/UnityExtensions.cs`. They take the path through + `UnityHelpers.LoadAsset` and cache the sprite by path. An empty path returns + null, the same as today's missing sprite. +- **Tests and `AetherDb`.** No `new GameplaySettings` remains outside one test seeding + helper. `AetherDb loadout` reads the real catalog's global. Test fixtures seed a + `GameSettings` into their temp catalogs, which they must do anyway: `AetheriaStores` + refuses a populated catalog without one, writable or not **(probe)**. + +## 3. Data migration + +### 3.1 Bootstrap of the first catalog global + +**(probe)** The catalog cannot gain its first global through `AetheriaStores`: + +- Open `GameData/Aetheria.cc` (a copy) with a build whose `GameSettings` type is + `[CultGlobal]` and routed to the catalog. The **writable** open threw `…has no + aetheria.probe.settings record; catalog globals are authored, never invented.`, and so + did the read-only one. This is the deliberate `ab4ced0f` rule. +- With `[CultGlobal]` removed from the type, a scratch build opened the same copy + writable. It upserted with an explicit key, `UpsertAsync(typeof(T), doc, new + CultRecordKey("global:aetheria.probe.settings"))`, and flushed. +- The build with `[CultGlobal]` restored then opened that file **read-only** and returned + the record from `GetGlobal`, under key `global:aetheria.probe.settings`, with the + colours intact. CultCache classifies a global by descriptor when it hydrates + (`CultCache.cs:1854`), not by key. The explicit key matches the one a global write + mints (`:1540`). +- Record diff between the original and the written copy: 180 records before, 181 after; + 0 removed, 0 changed (a SHA-256 of each record's MessagePack), and 1 added. The file + grew by 2501 bytes. + +This is fork K, recommended (a): the authoring build exists only in scratch, and the +landed type differs from it by the one attribute. + +### 3.2 Steps (Cut 1, **First**) + +1. Close Unity; `GameData/Aetheria.cc.lock` must be free. Capture the "before" state: + - `dotnet run --project tools/AetherDb -p:CultLibRoot= -- settings > before-settings.txt` + (the method: `dotnet run` stdout, redirected by Git Bash, no BOM); + - a per-record SHA-256 dump of `Aetheria.cc` through `AetheriaStores.Open` (read-only + works before the type exists). +2. Create a scratch worktree of the Cut 0 landing commit and a CultLib worktree at + `a0813c6`. Add `GameSettings.cs` as in 2.1 **without** `[CultGlobal]`, and add it to + `CatalogTypes`. Add a scratch `import-settings` command over `AuthoredSettings` that + fills the document through the catalog's own types: + - existing classes by the current `Read(subtree)`; + - `Color` `{r,g,b,a}` to `float3`, refusing when `a != 1`; + - `Gradient` to `float4[]` from `key{i}`/`ctime{i}` for `i < m_NumColorKeys`, + refusing unless `m_Mode == 0` and every alpha key is 1; + - `{fileID, guid}` to an `Assets/...` path through a `.meta` guid index. `{fileID: 0}` + and unresolved guids become `""`. Every unresolved one is printed, and the run fails + unless the printed set equals the 9 in 1.1. + - `BodySettingsCollections` per fork B. + - `float.Parse(…, CultureInfo.InvariantCulture)`, **throwing** on failure. Today's + `TryParse(...) ? f : 0f` (`AuthoredSettings.cs:131`) is the silent-zero shape this + cut exists to kill. The probe host culture was `en-150`. + - Write with key `global:aetheria.gamesettings` into the real `GameData/Aetheria.cc`. +3. Value check, in the same scratch build: + - Reopen the file and walk the YAML again. Every scalar leaf (266, less the 4 + soundbank strings Cut 0 deletes), every `float3` component (15) and every live + colour component (9) must equal the document value by path (`==` on the invariant + parse). Every gradient key and time must match, and every icon and body path must + match its guid's `.meta` path. + - Print the counts compared. Exit non-zero on any mismatch or unplaced key. + - `NeutralWandererCount == 2` (absent from YAML, carried by the initializer). +4. Record diff (`before` against `after`): exactly one record added, + `global:aetheria.gamesettings`; 0 changed; 0 removed. +5. Copy the written `Aetheria.cc` into the main tree. It lands in the Cut 1 commit + together with the type (with `[CultGlobal]`). Remove the scratch worktrees. + +## 4. Cuts + +### Cut 0. Cut the settings writers and the dead settings + +- **Repo/branch:** Aetheria `codex/item-provenance` from `00ebe46a`. No dependencies. + Unity is closed for the asset edits. +- **First:** `rg -n "Settings\.\w*BackgroundSettings\.NoisePosition" Assets/Scripts` + returns 2 hits at HEAD. Run `dotnet test tests/Aetheria.Shared.Tests` for a baseline. +- **Deletes first:** + - `GameSettings.cs:13-16` (soundbanks) and `:42` (`HullHitColor`); `Settings.asset:15-18,327`. + - `ActionGameManager.cs:228-232` (`NewEntitySettings`). + - `EntitySerializer.cs:51-52` (the copy; `pack.Settings ??= itemManager.GameplaySettings.DefaultEntitySettings`). + - Fork X (a): `Assets/Resources/{Atmosphere Settings,Volume Settings,Elysium,Test Galaxy,Icons}.asset` and their `.meta` files. +- **Per-file changes:** + - `Entity.cs:340`: `Settings = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(settings))`. + The entity owns its copy, so every construction path is safe, including + `Loadout.cs:131`, `LoadoutGenerator.cs:46,61,76`, `EntitySerializer.Unpack` and + `TradeMenu.cs:377`. + - `TradeMenu.cs:377`: pass `GameManager.ItemManager.GameplaySettings.DefaultEntitySettings`. + - `MainMenu.cs:125-157`: take a run-owned copy of the background (the same MessagePack + round trip), set `NoisePosition` on the copy, search the copy (`:146-150`), and pass + the copy to `Galaxy`. The authored object is never assigned. +- **Authority map:** + - Owner: `Entity` owns its `EntitySettings` instance. `Galaxy` (and so `SavedGame`) + owns the run's background position. + - Inputs: the authored defaults, read only. + - Outputs: per-entity and per-run copies. + - Derived state: `DefaultEntitySettings` and `*BackgroundSettings` are template-only. + - Forbidden writers: `InventoryMenu.cs:175` may write only the entity's own copy. + `MainMenu` never writes the settings. + - Shared paths: new ship (`LoadoutGenerator`), preset (`Loadout.Build`), bought ship + (`TradeMenu`) and Continue (`EntitySerializer.Unpack`) all go through the `Entity` ctor. + - Deletion line: `NewEntitySettings` and the serializer copy are gone before the ctor copy lands. +- **Verification:** + - builds: `Aetheria.Shared`, `tests/Aetheria.Shared.Tests` (`-p:CultLibRoot=`); Unity batchmode compile. + - tests: `EntitiesDoNotShareDefaultEntitySettings` (LoadoutTests fixture) pins "no entity + aliases the template". Generate two ships, write `ShutdownPerformance` on one, then + assert that the other and `GameplaySettings.DefaultEntitySettings` are unchanged. It + fails when `Entity.cs:340` goes back to `Settings = settings`. + - negative: + - `rg -n "Settings\.\w*BackgroundSettings\.NoisePosition" Assets/Scripts` returns 0. + - `rg -n "NewEntitySettings|Soundbank|SoundBank|HullHitColor" Assets/Scripts` returns 0 + (at HEAD it hits only the lines deleted here). + - `git ls-files "Assets/Resources/*.asset"` lists only `Settings.asset` and + `InputSystem.inputsettings.asset`. + - operator: open the Inventory ship settings and change Shutdown Threshold. A + generated NPC ship's threshold must not move. + +### Cut 1. Move the settings into the catalog global + +- **Repo/branch:** Aetheria `codex/item-provenance` from the Cut 0 landing. Depends on + forks K, I, B, D and P. It is one commit, because the type rename forces the reader + switch (2.1). +- **First:** section 3.2, steps 1-4. The written `Aetheria.cc` is in hand before any + main-tree edit. +- **Deletes first:** + - `Assets/Scripts/Gameplay/GameSettings.cs` (+`.meta`; `BodySettingsCollection` moves per fork B). + - `Assets/Resources/Settings.asset` (+`.meta`). + - `Assets/Scripts/Editor/GameplaySettingsEditor.cs` (+`.meta`; 100 lines; Studio edits the record). + - The 7 serialized `public GameSettings Settings;` fields: `ActionGameManager.cs:99`, + `SchematicDisplay.cs:14`, `SchematicListElement.cs:9`, `MainMenu.cs:20`, + `InventoryPanel.cs:26`, `VolumeSampling.cs:22`, `ZoneRenderer.cs:27`. Also their 15 + YAML reference lines (`rg -n "cd2344e2001a37544aab0960c164d4ff" Assets -g "*.unity" -g "*.prefab"`), + text-edited with Unity closed, and the stale `Sensitivity` block at + `ARPG.unity:16548-16550`. + - `tools/AetherDb/AuthoredSettings.cs` (174 lines); `Program.cs:30-31,33` (the + `settings` and `settings-dump` commands and help text) and `:174-228`; the `YamlDotNet` + reference (`AetherDb.csproj:16`); `Program.cs:437-442` (the hand-built settings). + - `ItemManager.cs:28,32` (the settings parameter); `Zone.cs:46` (the `PlanetSettings` parameter). + - Tests: `RunSaveTests.cs:313-319` and `IffAndCombatTests.cs:62-70` (`TestSettings` + copies); `AetheriaStoresTests.cs:11-14` (`TestCatalogGlobal`, the stand-in for + "the game has none yet"). +- **Adds:** + - `ServerShared/GameSettings.cs` (2.1). `AetheriaStores.cs:9`: add `typeof(GameSettings)` to `CatalogTypes`. + - The icon extensions in `CultCache/UnityExtensions.cs` (2.2). + - One test seeding helper. It builds `GameSettings { GameplaySettings = {DefaultEntitySettings, one Common tier, QualityPriceModifier}, PlanetSettings = new() }` + with an optional tweak, and each fixture upserts it where it seeds its temp catalog: + `LoadoutTests.cs:27`, `IffAndCombatTests.cs:23-24`, `AetheriaStoresTests.cs:27-28`, + and `RunSaveTests.cs:23`. + - `LoadoutTests.StatsReadTheLot` (`:495-503`) seeds its two tiers through the helper + instead of mutating a live settings object. + - `IffAndCombatTests` seeds `TargetDetectionInfoThreshold = .5f` the same way. + - `GameData/Aetheria.cc`: +1 record. +- **Per-file changes** (reader switch; the member paths after `Settings.` do not change): + - `ActionGameManager.cs:203,280-284,313,671,685,772,776,847,1208-1292` use `ItemManager.Settings` + inside the run. `:230` goes with Cut 0. `:263` becomes + `new ItemManager(CultCache, RunSave.Lots(CultCache), Debug.Log)`. `:690` becomes + `new Zone(ItemManager, galaxyZone.PackedContents, …)`. Add the static + `GameSettings` accessor next to `PlayerSettings` (`:66`). + - `ZoneRenderer.cs:147-572` (22 lines) use `ItemManager.Settings` (`ZoneRenderer.ItemManager`, `:95`). + `:397` follows fork B. `:497` and `:412` are unchanged apart from the source. + - `EntityInstance.cs:413,417,428`: `ZoneRenderer.Settings.LootDrop*` becomes `Entity.ItemManager.Settings.LootDrop*`. + - `MainMenu.cs:125-158` and `VolumeSampling.cs:54` use `ActionGameManager.GameSettings`. + - `InventoryPanel.cs:497-499`: `.ToColor()` on the `float3`. `:790,792`: build + `ToGradient()` once per panel and keep it, instead of one gradient per call. + - `SchematicDisplay.cs:169`, `SectorRenderer.cs:71-72`, `WeaponGroupAssignment.cs:25`, + `ActionBarSlot.cs:155-156` and `SchematicListElement.cs:27-44` read through `ActionGameManager.Instance.ItemManager.Settings`. + - `Zone.cs:46-…`: `Settings = itemManager.Settings.PlanetSettings`. The callers are + `IffAndCombatTests.cs:58` and `RunSaveTests.cs:93`. + - `tools/AetherDb/Program.cs:444`: `new ItemManager(db.Cache, new ProvenanceLedger(), log.Add)`. + The loadout command now runs on the authored tiers and price curve. + - `AetheriaStoresTests`: the refusal tests (including `WritablePopulatedCatalogMissingGlobalIsLoud`) + remove and expect `GameSettings` instead of `TestCatalogGlobal`. +- **Authority map:** + - Owner: the catalog record `global:aetheria.gamesettings`, authored in CultCache Studio. + - Inputs: none at runtime. It is authored data. + - Outputs: `ItemManager.Settings` (sim, in-run Unity) and `ActionGameManager.GameSettings` (pre-run Unity). + - Derived state: Unity `Gradient`, `Color` and `Sprite` objects are lowering-only, built + from the record. Entity and run copies come from Cut 0. + - Forbidden writers: any runtime assignment into the record. The Unity serializer + (asset deleted). `GameSettingsEditor`. `AuthoredSettings` and YAML. Hand-built + `GameplaySettings` in `AetherDb`. + - Shared paths: new game, Continue, the tutorial, the headless tests and `AetherDb` all + construct `ItemManager` over a cache holding the record, so they all read the same one. + - Deletion line: the `ScriptableObject`, the asset, its editor, the YAML reader and the + settings constructor parameters are gone in the same commit that adds the global. +- **Verification:** + - builds: `Aetheria.Shared`; `tools/AetherDb`; `tests/Aetheria.Shared.Tests` (`-p:CultLibRoot=` + at `a0813c6`, `--artifacts-path` in scratch); Unity batchmode compile (Assembly-CSharp and Editor). + - data: the 3.2 value check prints its compared counts with 0 mismatches, and the record diff is +1/0/0. + - tests: + - `ItemManagerReadsTiersFromTheCatalogGlobal`: two temp catalogs whose `GameSettings` + differ only in `Tiers` give different `GetTier` for the same lot. Pins one owner. + It cannot be written against a constructor that takes settings. + - `ItemManagerRefusesACacheWithoutGameSettings`: a bare `CultCache` makes the + constructor throw. Pins loud absence, never defaults. + - `MissingCatalogGlobalIsLoud`, `WritablePopulatedCatalogMissingGlobalIsLoud` and + `GameShapedOpenMissingGlobalIsLoud` (`AetheriaStoresTests.cs:116-143`, retargeted). Pins "authored, never invented". + - `ShippedCatalogSettingsAreNotZero`: opens `GameData/Aetheria.cc` read-only (root + found as in `AetherDb.FindRoot`) and asserts every `RarityTier.Color` has a + nonzero component, the hit colours are nonzero, each gradient has at least 2 keys, + `MinimapZoomLevels` is non-empty, and `PlanetSettings.GravityStrength > 0`. Pins + "no authored value silently loads as zero" (this cut's triggering defect). Exact + values belong to the one-time 3.2 check, not to a test that future authoring would break. + - negative (hit counts at `00ebe46a` in brackets; each must be 0 after): + - `rg -n "\bGameSettings Settings\b" Assets/Scripts` [7] + - `rg -l "0d957fc3cdc9310499a3a494a317fe0a|cd2344e2001a37544aab0960c164d4ff" Assets -g "*.unity" -g "*.prefab" -g "*.asset"` [4] + - `rg -n "AuthoredSettings|YamlDotNet" tools -g "!**/obj/**" -g "!**/bin/**"` [9] + - `rg -n "Settings\.asset" -g "*.cs" .` [2] + - `rg -n "TestCatalogGlobal" tests` [7] + - `rg -n "new GameplaySettings\b" tools Assets/Scripts` [1]. In `tests`, exactly 1 + (the helper) [2 now]. + - `rg -n "ScriptableObject" Assets/Scripts/ServerShared` must stay 0. + - operator: + - Play smoke: main menu, then New Game (tutorial). The tutorial galaxy generates, + and the starting ship is LonginusX. + - In a station trade menu and the properties panel, item names show tier colours: + Legendary orange, Uncommon green. None is black. + - The inventory shows armour and durability gradients, and hit flashes use the + authored colours: armour white, hardpoint pale blue, gear pale red. + - The minimap cycles 5 zoom levels. Planets render with body presets. Loot drops and + pickups expire. + - Action bar and schematic weapon icons appear. Modifier icons stay blank, as they + are today (1.1). + - Continue after save. + - Studio click-through: the `aetheria.gamesettings` record opens, nested curves and + tiers edit, icon fields show the asset-path picker, and the edit persists. +- **Operator questions:** forks K, I, B, D and P (section 6). + +## 5. Subtraction ledger (estimate) + +| Cut | Removed | Added | Dependencies, formats, targets | +|---|---|---|---| +| 0 | ~12 C# lines; 5 dead fields; 5 orphan assets (~18 KB YAML); 1 dead copy path | ~12 C# lines; 1 test | none | +| 1 | `GameSettings.cs` 88, `GameplaySettingsEditor.cs` 100, `AuthoredSettings.cs` 174, `Program.cs` ~65, `Settings.asset` 477 YAML lines, 7 serialized fields plus 16 scene YAML lines, 2 `TestSettings` copies ~15, `TestCatalogGlobal` 4, 2 settings ctor parameters | `ServerShared/GameSettings.cs` ~45, icon extensions ~30, test helper ~15, tests ~70, +1 catalog record (~2.5 KB) | removes the `YamlDotNet` dependency and one Unity asset format (settings YAML); no targets added | + +## 6. Operator forks (ordered by how much they block) + +**K. How the first catalog global gets written.** Blocks Cut 1. +- (a) A scratch-only authoring build without `[CultGlobal]` writes the record under the + explicit `global:` key, and the landed type adds the attribute (probed, 3.1). No landed + code bypasses the refusal. +- (b) Loosen `AetheriaStores` so writable opens skip the refusal. This reverses the + `ab4ced0f` reasoning ("a missing global went silent in play mode"). +- (c) Land a one-shot `import-settings` command with a bypass flag on `Open`, then + delete both. A guardrail hole ships, even if briefly. +- **Recommend (a).** + +**I. Icon sprites (engine references) in the global.** Blocks Cut 1. +- (a) Asset-path strings with `[CultInspectorAssetPath]`, loaded through + `UnityHelpers.LoadAsset`. This is what `ItemData.Icon`/`ActionBarIcon`, + `Weapon`, `Thruster` and `AetherDrive` particles already do, and all 33 live sprites + already sit under `Assets/Resources`. The 9 dangling refs become empty strings. +- (b) Keep a slim Unity `ScriptableObject` holding only the sprite arrays. This keeps + Unity serialization for this data, against the ruling's direction. +- (c) Put the sprite arrays on each consuming component. Several prefab instances would + each duplicate them. +- **Recommend (a).** Separately, the operator may want to know that modifier icons are + already broken (9 missing assets). + +**B. `BodySettingsCollections` (mass thresholds plus `CelestialBodySettings` plugin assets).** Blocks Cut 1. +- (a) Thresholds and asset paths go in the global, like fork I. The 7 referenced assets + move, with their `.meta` files so guids are kept, from `Assets/Plugins/Celestial Body/Solar System/…` + to `Assets/Resources/Celestial Bodies/`, because `Resources.Load` needs them there. + Their dependencies follow by guid. +- (b) Serialized field on `ZoneRenderer`, the only reader. Thresholds and references stay + in `ARPG.unity`, with no asset moves. Unity keeps authoring a setting. +- **Recommend (a).** One rule ("engine assets are referenced from globals by path") is + cheaper to explain than an exception. The 7 moves are a one-time cost. + +**D. Document shape.** Blocks Cut 1. +- (a) One catalog global `GameSettings` (2.1). It keeps today's single authoring + surface, every existing settings class, and every `Settings.X` member path. +- (b) Split by consumer: `GameplaySettings`, `GenerationSettings` and `PresentationSettings` + globals. It gives narrower ports, but nothing needs isolating yet, and it means three + records and a wider reader diff. +- **Recommend (a).** A later split is a mechanical re-key if a consumer ever needs one. + +**P. Simulation port.** Blocks Cut 1. +- (a) `ItemManager` and `Zone` read the global from the cache they already hold, and + their settings constructor parameters go (2.2). +- (b) Keep the parameters, with callers passing `cache.GetGlobal().…`. The + sim could then run on settings the catalog does not hold, and tests and `AetherDb` + could keep hand-building them. That is the duplication this cut exists to delete. +- **Recommend (a).** The headless-playground `Run.New` signature shrinks with it. + +**M. Is component tuning on scene and prefab instances "settings"?** Does not block Cut 0 or Cut 1. +- (a) No. It is instance data of presentation components (1.5) and stays Unity-serialized. + Gameplay-affecting prefab values (`Projectile`, `Mine`, `GuidedProjectile`) move when + the headless-playground map moves hit and loot ownership into the sim. + `ActionGameManager.Credits` is run state, not a setting, so it goes to follow-ups. +- (b) Yes. Each component's tuning becomes a catalog global or a per-prefab catalog + record. That is a second authoring surface per component and a much larger cut. +- **Recommend (a).** + +**X. Dead authored data.** Low blocking; affects Cut 0 only. +- (a) Delete the 4 soundbank names, `HullHitColor` and the 5 orphan `Resources` assets. + Their bytes stay in git history (1.2 names the commits), and `Elysium`/`Test Galaxy` + hold parameters of a generator deleted in 2021. +- (b) Migrate them into the global or a catalog record anyway. +- **Recommend (a).** Nothing reads them. They are not pre-breach content in the sense of + `GameData`; they are Unity assets whose scripts are gone. + +## Follow-ups outside this migration + +- `ActionGameManager.Credits` is not saved and resets per scene load (`ActionGameManager.cs:101`, + `ARPG.unity`: 100000000). It belongs in the run store with `SavedGame`. +- `NoisePosition` is run state inside a settings class (`Settings.cs:62`). After Cut 0 the + authored value is only an unused seed. It could move to `SavedGame`/`Galaxy`. +- `StartingHullName` and the tutorial faction names are name strings, not `CultRecordRef`s, + so `AetherDb dangling` cannot see them. +- `GameplaySettings` mixes presentation (`LockIndicator*`, `MessageDuration`) with sim rules. +- `TargetArmorInfoThreshold` and `TargetGearInfoThreshold` are authored and unread + (recorded in the headless map). +- 9 dangling icon references (1.1).