The click-through passed with the AmmoType refs repointed and the schematic underlay fixed. It also surfaced that the cutover dropped the Database Tools' record grouping and drag-and-drop without recording them; CultLib's studio-grouping cut map owns restoring them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
CultCache Migration Target
Date: 2026-09-12
Status: landed; awaiting operator play smoke. Aetheria runs on CultLib's CultCache and CultMath, cut over on
codex/cultcache-cutover from Cut 8a (4f4beb0f) through Cut 10 (4a545469
to the commit that set this status). The means are in
docs/cultcache-migration-cut.md.
Aetheria moves off its in-tree CultCache, the ancestor of the one in CultLib,
onto modern CultCache in F:\Projects\CultLib. Adopting the infrastructure the
rest of GameCult maintains replaces a private copy with a shared one. This
document states the end state and the invariants that constrain getting there.
This touches the foundation
CultNet is built on CultCache, and CultMesh on CultNet. Every CultLib change below lands under all GameCult infrastructure, not just Aetheria.
- Wire parity is an invariant. CultCache maintains wire parity with the
other runtimes, and only the packages inside CultLib are canonical:
CultLib\packages\cultcache-{py,rs,ts},cultnet-{py,rs,ts}, andcultmesh-{browser,kotlin,py,rs,ts}. The neighboring repositoriesF:\Projects\cultcache-rs,cultcache-py,cultnet-rs, andcultnet-tsare defunct and are not parity targets. A change that alters bytes on disk or on the wire must land in every canonical runtime, or it does not land. - C# is the reference runtime. The ergonomics arose there; the other
runtimes follow its contract. Contract changes are written down in
CultLib\src\GameCult.Caching\Contracts\first. - Existing consumers must survive. Aquarium and Ymir build against CultLib
main; other consumers pin revisions or carry copies. AetheriaEve is not a consumer: it is preserved as a specimen and receives no consideration.
Why the previous attempt failed
The first migration (rolled back by 7006a6b0, plan in
aetheria-perfect-machine-map.md) made the cache move phase 2 of seven,
bundled with an Aetheria.State service layer, CultMesh hosting, Eve UI, and
an Electron RTS client. The cache never finished moving. This migration moves
the cache and nothing else.
Target state: Aetheria
-
All persistent state is CultCache state, split by lifecycle into routed stores inside one cache:
Store Holds Written by Lifetime Catalog GameData/Aetheria.ccItems, factions, products, name files, other authored records CultCache Studio Read-only at runtime Run .ccGalaxy, zones, bodies, orbits, entities, tasks The run save path Deleted on death Player .ccSettings, bindings, account The game Persists across runs -
Documents versus values. A record with identity that something references or that changes independently is a
[CultDocument]. Values without identity (item instances in cargo, behavior lists on a design, weapon groups) live inside their owning document and keep their MessagePack[Union]s. -
References are
CultRecordRef<T>, resolved through an explicit cache.DatabaseLink<T>, the staticDatabaseLinkBase.Cache,DatabaseEntry, its union, andDatabaseEntry.IDare gone. A catalog record's identity is its record key, which carries the legacy Guid string. -
The live simulation owns in-play state.
Entityand its reactive fields are not documents. Quit and zone transition share one save commit (RunSave.CapturethenRunSave.Commit); death deletes the run withRunSave.Clear. -
No legacy reader exists. The catalog records carry legacy Guid keys and slot numbers from the one-time import; the importer and the legacy files are deleted.
-
Existing saves are discarded. No save converter.
-
Deleted from Aetheria:
Assets/Scripts/ServerShared/CultCache/, the vendoredAssets/Plugins/MessagePack, vendored JsonKnownTypes, the legacy Guid/Type resolvers, and the IMGUI Database Tools underAssets/Scripts/CultCache/Editor/. -
Consumption. Unity takes the full
org.gamecult.cultlibUPM package pinned by git tag.Aetheria.Shared/Aetheria.Shared.csprojreferences the same CultLib version, so the headless build still proves the simulation compiles without Unity.
Loadouts
A loadout is an authored ship preset, a variant in the MechWarrior sense. Players
do not see loadouts in the MVP. They are meant for spawners to build ships from; no game spawner calls
Loadouts.Materialize yet.
- Content. A loadout stores item designs only: the hull design, and for each
hull cell a rotation and an
EquippableItemDatadesign. It names no manufacturer and no product, so it works in any galaxy. - Home. Loadouts live in the catalog. They are authored in CultCache Studio,
or captured in play with the editor-only console command
capturepreset "<name>" [replace], and they are only read at runtime. - Capture. The process cache opens the catalog read-only in every build.
Loadouts.Commitopens its own short-lived cache overAetheria.cc, writable, commits the one preset and disposes it, so no catalog instance play holds can reach the file. The running game sees the preset after the catalog reloads.- A preset is keyed by its name. It replaces an existing preset only when
replaceis given, and a preset of the same name under another key (one authored in Studio) refuses the capture. - A missing
Aetheria.ccrefuses the capture; it never creates the catalog. - The commit is conditional on that key, so it writes only this one record onto the file as it is on disk.
- The console refuses a command containing anything but letters, digits, spaces and hyphens, and quotes a multi-word name.
- A CultCache Studio session opened before the capture overwrites captured presets when it saves, since a single-file save is last-writer-wins. Reopen the Studio after capturing.
- A preset is keyed by its name. It replaces an existing preset only when
- Materialize.
Loadouts.Materializeis the only way to build a ship from a loadout, and it charges nothing.- Each design is built by the first available product for that design, in
record-key order, under the availability predicate. The game passes
LoadoutGenerator.IsAvailable. - It is all-or-nothing. On any failure it lists every failure and returns no ship, so a spawner can pick another preset.
- Each design is built by the first available product for that design, in
record-key order, under the availability predicate. The game passes
- Weapon groups. Weapon groups are indices into the slots, and every index
must name a slot whose design is a weapon. A loadout may have at most
WeaponGroupCountgroups; more is a failure. The ship always gets exactlyWeaponGroupCountgroups, and missing groups are added empty. - Future direction. Variants will also differ by hardpoint configuration:
one hull with alternative hardpoint layouts. This is not built yet. Nothing
consumes presets yet either; ship spawning still uses
LoadoutGenerator.
Target state: CultLib
- Store routing replaces replication. Each document type has exactly one home store, chosen by the record's runtime type (most specific assignable route wins). A cache with one store and no routes behaves as today. Overlapping routes, writes of an unrouted type, a record loaded from a store that is not its home, and attaching a store after hydration are errors. Multi-store replication is deleted: no consumer uses it, and transactions already refuse more than one store.
- Loading never writes. Store dirtiness derives only from mutations.
- Read-only stores. Writes routed to a read-only store throw.
- Transactions require every mutation to route to one store, replacing the rule that a transacting cache may attach only one store.
- Runtime type decides schema. The generic
AddAsync<T>/UpsertAsync<T>type parameter stops choosing the schema. - Typed lookups honor inheritance.
Watch<T>,GetByName<T>,GetGlobal<T>,GetByIndex<T>match assignable types, asGetAll<T>andGet<T>already do. - Consumers can serialize their value types (Unity.Mathematics, and whatever
replaces
System.Typefields) without breaking wire parity. - CultCache Studio absorbs Database Tools: dictionaries, polymorphic
[Union]fields, Unity.Mathematics types, a record picker forCultRecordRef<T>, and a custom drawer extension point.
Evidence
Runtime probe, 2026-09-12, CultLib main at c2a9a6e, generator and
reflection builds, write then flush then reopen:
| Case | Result |
|---|---|
Nested [Union] field inside a document |
Pass |
Document inheriting [Key] fields from an unattributed abstract base |
Pass |
GetAll<AbstractBase>(), Get<AbstractBase>(key) |
Pass |
UpsertAsync<Gear>(new Weapon()) |
Fail: stored and reloaded as Gear, Weapon fields lost silently |
Code-read, not probed: exact-type Watch/GetByName/GetGlobal/GetByIndex;
no consumer formatter resolver registration (fixed static options); the
generator never emits a codec for hierarchies and silently falls back;
CultCache Studio lacks dictionaries, unions, math types, and a ref picker.
MessagePack 3's analyzer (MsgPack005) rejects [MessagePackObject] on an
abstract type without [Union]; Aetheria's abstract ItemData,
CraftedItemData, and ItemInstance must change.
Invariants
- Wire parity across runtimes; C# remains the reference.
- CultNet and CultMesh behavior is preserved or deliberately changed with every runtime updated.
- Loading a store never writes it.
- Every document type has one home store; its runtime type decides schema and route.
ServerSharedcompiles headless with no UnityEngine reference.- No code reads a legacy format.
- The migration does not bundle CultMesh hosting, Eve UI, daemons, or a state service layer.
Out of scope
CultMesh and Eve adoption in Aetheria, R3 for gameplay (UniRx stays), removing Newtonsoft attributes from the 82 files that carry them, and a state service layer.
Open questions for the Imagination pass
- Is routing a C# cache behavior only, or a semantic every runtime's cache must match? What do the Rust, Python, and TypeScript caches do with multiple stores today?
- How do Unity.Mathematics values encode so other runtimes can read them? Does CultMath already define portable shapes?
- What replaces
System.Typefields (StatModifierData.RequireBehavior), which have no portable encoding? - Do CultNet or CultMesh depend on multi-store replication, on the generic
UpsertAsync<T>declared-type behavior, or on exact-type watches? - Can existing
.ccstores in other projects hold records written under a declared parent schema, and what does fixingUpsertAsync<T>do to them? - Which consumers pin which CultLib revision, and what is the release and upgrade order?
- Where exactly do Aetheria's world types fall on the document/value line, and where are the save points?
- How do catalog identity comparisons (
DatabaseEntry.Equalson ID,Dictionary<Faction,…>,Hull.Data.LinkID == data.ID) survive withoutDatabaseEntry.ID?
Process
A mini Epiphany loop. Imagination (a Fable agent) maps the cut into
docs/cultcache-migration-cut.md: authority maps, sequenced independently
executable cuts, deletion lines, negative checks, parity verification, and
build budgets. Hands (Opus agents) execute the cuts. Soul (a Fable agent)
verifies each against the map and the invariants above.