Aetheria/docs/settings-globals-cut.md
Meta Vens dcf1881db7 Rule Addressables Q1 and sweep the settings map onto asset GUIDs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:36:59 +02:00

40 KiB

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 (2026-09-17):

  • Engine asset references: Addressables, stored as asset GUIDs (operator: "you got it", to that proposal). This supersedes forks I and B: nothing moves under Resources, and the existing [CultInspectorAssetPath] fields (ItemData.Icon/ActionBarIcon, Weapon effect, Thruster/AetherDrive particles, Corporations logo) convert too, so there is one rule. That conversion is its own cut, mapped in docs/addressables-cut.md, and lands before Cut 1; Cut 1's icon and body-preset fields then use it.
  • K (a), D (a), P (a), X (a), M (a): Self's defaults, stated to the operator, who did not object. Reopen any of them on request.
  • Open: none blocking Cut 0. 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

Superseded in part (2026-09-17): wherever this map stores an engine asset as an Assets/Resources/... path or loads it with UnityHelpers.LoadAsset/Resources.Load, read instead: the asset's GUID under [CultInspectorAssetGuid], loaded with EngineAssets.Load<T>, the asset living under Assets/Content (docs/addressables-cut.md, which lands first). That covers ItemIcons/WeaponTypeIcons/... (2.1), the 3.2 import's {fileID, guid} resolution (store the guid directly, no path), the body presets (the 7 CelestialBodySettings assets move to Assets/Content, not Resources), and Cut 1's picker check. Forks I and B are history.

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.

[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<string> logger) sets Settings = itemData.GetGlobal<GameSettings>() ?? 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<GameSettings>(), 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<Sprite> 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=<a0813c6 worktree> -- 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<T>(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<EntitySettings>(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)

Superseded in part (2026-09-17): wherever this map stores an engine asset as an Assets/Resources/... path or loads it with UnityHelpers.LoadAsset/Resources.Load, read instead: the asset's GUID under [CultInspectorAssetGuid], loaded with EngineAssets.Load<T>, the asset living under Assets/Content (docs/addressables-cut.md, which lands first). That covers ItemIcons/WeaponTypeIcons/... (2.1), the 3.2 import's {fileID, guid} resolution (store the guid directly, no path), the body presets (the 7 CelestialBodySettings assets move to Assets/Content, not Resources), and Cut 1's picker check. Forks I and B are history.

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<Sprite>. 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<GameSettings>().…. 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 CultRecordRefs, 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).