Quality, Ingredients and Product leave CraftedItemInstance; a unit now
carries only a LotId into the run-scoped ProvenanceLedger. ItemManager
gains the one lot-minting path (CreateLot/CreateInstance(int)), GetLot,
and Brand(), which derives a maker and product from provenance instead
of a stored field. RunSave persists the ledger pruned to what the saved
zones' entities still reach; the live ledger is never pruned, so a
floor pickup collected after a save still resolves. CopyBuild is gone:
matching hardpoints now share the same lot instead of copying values.
PropertiesPanel shows the derived brand where the design's manufacturer
used to be. AetherDb's census flags duplicate (maker, design) products
and its save command checks every crafted instance's lot against a real
run store.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loadouts are ship presets (variants) for spawners, not a player feature.
- Route Loadout to the catalog; the player store no longer holds it.
- Materialize takes no credits: affordability, the charge and Loadouts.Price
are deleted, since only the player menu priced loadouts. Availability,
key-order product choice, all-or-nothing failures and the weapon-group
rules stay.
- Delete the InventoryPanel Save Loadout and Restore Loadout entries and
RestoreLoadout, which also removes its docking-bay overwrite.
- The editor opens the catalog writable in the one process cache, and the
editor-only console command capturepreset "<name>" [replace] captures the
piloted ship. Loadouts.Commit keys a preset by name, refuses a same-name
replace unless asked, and commits conditionally on that key. The single-file
store then lands only that record onto the file as it is on disk. An
unconditional commit would re-serialize every live catalog instance,
including any that play has mutated.
- Tests: capture -> commit -> fresh reopen round-trip, explicit replace with
no duplicate, and a preset commit that does not persist a mutated live
Faction. The money-only tests are removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Materialize now reports a weapon group index whose slot design has no
WeaponData, and a loadout with more groups than WeaponGroupCount. The ship
always gets exactly WeaponGroupCount groups, and a shorter loadout is padded
with empty groups. Both checks run before anything is built.
Price is summed as long, so the hull add can no longer wrap to a negative
price that pays the player. A price beyond int credits is simply unaffordable.
Deleted Capture's index >= 0 filter and its test. Every production writer of
WeaponGroups items (WeaponGroupAssignment, GenerateWeaponGroups, Unpack replay
of Pack, Materialize) groups only weapons, and TryUnequip removes weapons from
groups, so nothing produces an ungrouped index any more.
The Materialize comment now says a failed fit may already have advanced
itemManager.Random.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Materialize, which already charges, now also owns affordability: when credits
are below Price it lists "cannot afford <price> credits" and builds nothing.
The restore menu's separate price < Credits check is deleted; the menu still
shows Loadouts.Price and lists Materialize's failures.
Tests: one credit short builds nothing, leaves credits unchanged and lists the
failure. A fixture gun with an InstantWeapon behavior proves TryUnequip removes
the weapon and its item from every weapon group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Entity.TryUnequip does not clear WeaponGroups, so Capture saved -1 for a
grouped item that was no longer equipped, and Materialize later threw
IndexOutOfRangeException after every other check passed. Capture now skips
group items that are not equipped. Materialize reports every weapon-group index
outside the loadout's slots as a failure before building anything, so a stale
or hand-edited record builds nothing and charges nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Materialize takes isAvailable as a Predicate<FactionProductData>; the restore
menu passes LoadoutGenerator.IsAvailable for the docked station, so generation
and loadouts still share one availability rule. Candidate products are ordered
by record key, so "first available product" no longer depends on dictionary
order. Materialize charges Price through ref credits only when it returns a
ship; RestoreLoadout no longer subtracts on its own.
Tests: a slot that resolves but does not fit builds nothing, charges nothing and
is listed; Price is the sum of design prices and is charged on success; the
first available product in key order builds a slot, a later one is used when
the first is unavailable, and none available fails the slot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>