Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using CultMath;
|
|
|
|
|
using GameCult.Caching;
|
|
|
|
|
using GameCult.Caching.MessagePack;
|
|
|
|
|
using Xunit;
|
2026-09-17 04:17:13 +02:00
|
|
|
using Random = CultMath.Random;
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
// Loadouts are ship presets: item designs only, authored into the catalog, materialized all-or-nothing against it.
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
// The fixture hull is 3x3: InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior cell a cargo bay could use.
|
|
|
|
|
public sealed class LoadoutTests : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-loadout-" + Guid.NewGuid().ToString("N"));
|
|
|
|
|
private string Catalog => Path.Combine(_root, "Aetheria.cc");
|
|
|
|
|
private string Run => Path.Combine(_root, "run.cc");
|
|
|
|
|
private string Player => Path.Combine(_root, "player.cc");
|
|
|
|
|
|
|
|
|
|
private static readonly int2 HardpointCell = new int2(0, 0);
|
|
|
|
|
private static readonly int2 InteriorCell = new int2(1, 1);
|
|
|
|
|
|
|
|
|
|
public LoadoutTests()
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(_root);
|
|
|
|
|
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
|
|
|
|
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
|
|
|
|
var maker = cache.Upsert(new Faction { Name = "Maker", ShortName = "MKR" });
|
|
|
|
|
|
|
|
|
|
var hullShape = new Shape(3, 3);
|
|
|
|
|
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
|
|
|
|
var hull = cache.Upsert(new HullData
|
|
|
|
|
{
|
|
|
|
|
Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Price = 100,
|
|
|
|
|
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = HardpointCell, Shape = new Shape() } }
|
|
|
|
|
});
|
|
|
|
|
var gear = cache.Upsert(new GearData { Name = "Lamp", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 10 });
|
|
|
|
|
var cargo = cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = new Shape(), Price = 5 });
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
var gun = cache.Upsert(new GearData { Name = "Gun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1, Behaviors = { new InstantWeaponData() } });
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
cache.Upsert(new GearData { Name = "Orphan", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1 });
|
|
|
|
|
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
foreach (var (name, design) in new[] { ("Skiff by Maker", hull.Key), ("Lamp by Maker", gear.Key), ("Crate by Maker", cargo.Key), ("Gun by Maker", gun.Key) })
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
cache.Upsert(new FactionProductData { Name = name, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = maker });
|
|
|
|
|
cache.FlushAsync().Wait();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose() => Directory.Delete(_root, true);
|
|
|
|
|
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
// The game's shape in every build, the editor included: a read-only catalog with the run and player stores.
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
private CultCache Open() => AetheriaStores.Open(Catalog, Run, Player);
|
|
|
|
|
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
// Gear on the hull's hardpoint cell, cargo on an interior cell, and two empty weapon groups (fewer than the game's six).
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
private static Loadout HandBuilt(CultCache cache, string name = "Skiff build") => new Loadout
|
|
|
|
|
{
|
|
|
|
|
Name = name,
|
|
|
|
|
Hull = cache.RefOf(cache.GetByName<HullData>("Skiff")),
|
|
|
|
|
Slots =
|
|
|
|
|
{
|
|
|
|
|
new LoadoutSlot { Position = HardpointCell, Design = cache.RefOf<EquippableItemData>(cache.GetByName<GearData>("Lamp")) },
|
|
|
|
|
new LoadoutSlot { Position = InteriorCell, Design = cache.RefOf<EquippableItemData>(cache.GetByName<CargoBayData>("Crate")) }
|
|
|
|
|
},
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
WeaponGroups = new[] { new int[0], new int[0] }
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
};
|
|
|
|
|
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
// HandBuilt with the Gun on the hardpoint, alone in the first of the game's six weapon groups.
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
private static Loadout Armed(CultCache cache, string name = "Armed build")
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
{
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
var armed = HandBuilt(cache, name);
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
armed.Slots[0].Design = cache.RefOf<EquippableItemData>(cache.GetByName<GearData>("Gun"));
|
|
|
|
|
armed.WeaponGroups = Enumerable.Range(0, 6).Select(g => g == 0 ? new[] { 0 } : new int[0]).ToArray();
|
|
|
|
|
return armed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Loadout WithDesign(Loadout loadout, int slot, CultCache cache, string gear)
|
|
|
|
|
{
|
|
|
|
|
loadout.Slots[slot].Design = cache.RefOf<EquippableItemData>(cache.GetByName<GearData>(gear));
|
|
|
|
|
return loadout;
|
|
|
|
|
}
|
|
|
|
|
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
// Capture from a built ship, commit to the catalog, reopen fresh read-only: the preset and the ship it builds match.
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
[Fact]
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
public void CapturedPresetRoundTripsThroughTheCatalog()
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
{
|
|
|
|
|
Loadout hand;
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
using (var cache = Open())
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
{
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
hand = Armed(cache);
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
var failures = new List<string>();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
var ship = Build(items, hand, failures);
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
Assert.Empty(failures);
|
|
|
|
|
Assert.NotNull(ship);
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
Assert.True(Loadouts.Commit(Catalog, Loadouts.Capture(items, ship, hand.Name), replace: false));
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
{
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
var loaded = Assert.Single(cache.GetAll<Loadout>());
|
|
|
|
|
Assert.Equal(Loadouts.KeyOf(hand.Name), cache.RefOf(loaded).Key);
|
|
|
|
|
Assert.Equal(hand.Name, loaded.Name);
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
Assert.Equal(hand.Hull.Key, loaded.Hull.Key);
|
|
|
|
|
Assert.Equal(hand.Slots.Select(Describe), loaded.Slots.Select(Describe));
|
|
|
|
|
Assert.Equal(hand.WeaponGroups, loaded.WeaponGroups);
|
|
|
|
|
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
var failures = new List<string>();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
var ship = Build(items, loaded, failures);
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
Assert.Empty(failures);
|
|
|
|
|
Assert.Equal(hand.Slots.Select(slot => (slot.Position, slot.Design.Key)),
|
|
|
|
|
ship.Equipment.Concat<EquippedItem>(ship.CargoBays)
|
|
|
|
|
.Where(item => item != ship.EquippedHull)
|
|
|
|
|
.Select(item => (item.Position, cache.RefOf(items.GetData(item.EquippableItem)).Key)));
|
|
|
|
|
}
|
|
|
|
|
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Single(SchemaNames(Catalog), name => name == "aetheria.loadout");
|
|
|
|
|
Assert.True(!File.Exists(Player) || !SchemaNames(Player).Contains("aetheria.loadout"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recapturing a name refuses without replace and leaves the file untouched; with replace it replaces, never duplicates.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void SameNameCaptureReplacesOnlyWhenAsked()
|
|
|
|
|
{
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
using (var cache = Open())
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
{
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
Assert.True(Loadouts.Commit(Catalog, HandBuilt(cache), replace: false));
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
var before = Hash(Catalog);
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
var error = Assert.Throws<InvalidOperationException>(() => Loadouts.Commit(Catalog, Armed(cache, "Skiff build"), replace: false));
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Contains("Skiff build", error.Message);
|
|
|
|
|
Assert.Equal(before, Hash(Catalog));
|
|
|
|
|
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
Assert.True(Loadouts.Commit(Catalog, Armed(cache, "Skiff build"), replace: true));
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
Assert.Equal(6, Assert.Single(cache.GetAll<Loadout>()).WeaponGroups.Length);
|
|
|
|
|
}
|
|
|
|
|
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
// Play mutates live catalog instances; a preset commit must land only itself.
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
[Fact]
|
|
|
|
|
public void PresetCommitWritesNoOtherInMemoryCatalogState()
|
|
|
|
|
{
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
using (var cache = Open())
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
{
|
|
|
|
|
cache.GetByName<Faction>("Maker").ShortName = "XXX";
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
Assert.True(Loadouts.Commit(Catalog, HandBuilt(cache), replace: false));
|
|
|
|
|
cache.FlushAsync().Wait();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
{
|
|
|
|
|
Assert.Equal("MKR", cache.GetByName<Faction>("Maker").ShortName);
|
|
|
|
|
Assert.Single(cache.GetAll<Loadout>());
|
|
|
|
|
}
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
}
|
|
|
|
|
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
// The process cache holds the catalog read-only while capture writes through its own cache: the capture lands, and
|
|
|
|
|
// the process cache keeps its instances, gains nothing and stays clean until it reloads.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void CaptureWhileTheCatalogIsHeldReadOnlyLeavesTheProcessCacheUnchanged()
|
|
|
|
|
{
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
{
|
|
|
|
|
var maker = cache.GetByName<Faction>("Maker");
|
|
|
|
|
Assert.True(Loadouts.Commit(Catalog, HandBuilt(cache), replace: false));
|
|
|
|
|
Assert.Empty(cache.GetAll<Loadout>());
|
|
|
|
|
Assert.Same(maker, cache.GetByName<Faction>("Maker"));
|
|
|
|
|
Assert.False(cache.IsDirty);
|
|
|
|
|
Assert.Throws<InvalidOperationException>(() => cache.Upsert(new Faction { Name = "Usurper" }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
Assert.Equal("Skiff build", Assert.Single(cache.GetAll<Loadout>()).Name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Capture never creates a catalog: a missing file refuses, and no file or directory appears.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void CaptureWithAMissingCatalogRefusesAndCreatesNothing()
|
|
|
|
|
{
|
|
|
|
|
var absent = Path.Combine(_root, "absent", "Aetheria.cc");
|
|
|
|
|
using var cache = Open();
|
|
|
|
|
var error = Assert.Throws<InvalidOperationException>(() => Loadouts.Commit(absent, HandBuilt(cache), replace: false));
|
|
|
|
|
Assert.Contains("does not exist", error.Message);
|
|
|
|
|
Assert.False(Directory.Exists(Path.GetDirectoryName(absent)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A preset authored in Studio has a minted key; capturing its name refuses, replace or not, and names that key.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void SameNameUnderAnotherKeyRefuses()
|
|
|
|
|
{
|
|
|
|
|
using (var writer = AetheriaStores.Open(Catalog, catalogWritable: true))
|
|
|
|
|
{
|
|
|
|
|
writer.Upsert(HandBuilt(writer));
|
|
|
|
|
writer.FlushAsync().Wait();
|
|
|
|
|
}
|
|
|
|
|
var before = Hash(Catalog);
|
|
|
|
|
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
{
|
|
|
|
|
var studioKey = cache.RefOf(Assert.Single(cache.GetAll<Loadout>())).Key;
|
|
|
|
|
Assert.NotEqual(Loadouts.KeyOf("Skiff build"), studioKey);
|
|
|
|
|
foreach (var replace in new[] { false, true })
|
|
|
|
|
{
|
|
|
|
|
var error = Assert.Throws<InvalidOperationException>(() => Loadouts.Commit(Catalog, Armed(cache, "Skiff build"), replace));
|
|
|
|
|
Assert.Contains(studioKey.Value, error.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Assert.Equal(before, Hash(Catalog));
|
|
|
|
|
}
|
|
|
|
|
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
[Fact]
|
|
|
|
|
public void LoadoutHoldsNoRunOrGalaxyRefs()
|
|
|
|
|
{
|
|
|
|
|
var forbidden = new[] { typeof(ItemInstance), typeof(EntityPack), typeof(FactionProductData), typeof(Faction) }
|
|
|
|
|
.Concat(AetheriaStores.RunTypes)
|
|
|
|
|
.ToArray();
|
|
|
|
|
var seen = new HashSet<Type>();
|
|
|
|
|
var pending = new Queue<Type>(new[] { typeof(Loadout) });
|
|
|
|
|
while (pending.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
var type = pending.Dequeue();
|
|
|
|
|
if (!seen.Add(type)) continue;
|
|
|
|
|
Assert.DoesNotContain(forbidden, bad => bad.IsAssignableFrom(type));
|
|
|
|
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(CultRecordRef<>))
|
|
|
|
|
{
|
|
|
|
|
Assert.True(typeof(EquippableItemData).IsAssignableFrom(type.GetGenericArguments()[0]), $"{type} names a non-design record");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (type.IsArray) pending.Enqueue(type.GetElementType());
|
|
|
|
|
foreach (var argument in type.IsGenericType ? type.GetGenericArguments() : Type.EmptyTypes) pending.Enqueue(argument);
|
|
|
|
|
if (type.Assembly == typeof(Loadout).Assembly)
|
|
|
|
|
foreach (var field in type.GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic))
|
|
|
|
|
pending.Enqueue(field.FieldType);
|
|
|
|
|
}
|
|
|
|
|
|
Capture presets through their own cache; keep the catalog read-only
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>
2026-09-14 23:32:52 +02:00
|
|
|
using (var cache = Open())
|
|
|
|
|
Assert.True(Loadouts.Commit(Catalog, HandBuilt(cache), replace: false));
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Contains("aetheria.loadout", SchemaNames(Catalog));
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
Assert.True(!File.Exists(Run) || SchemaNames(Run).Length == 0);
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.True(!File.Exists(Player) || SchemaNames(Player).Length == 0);
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void MissingDesignReports()
|
|
|
|
|
{
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
cache.Commit(batch => batch.Upsert(new PlayerSettings()));
|
|
|
|
|
var before = new[] { Catalog, Run, Player }.Select(Hash).ToArray();
|
|
|
|
|
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
{
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
var missing = HandBuilt(cache);
|
|
|
|
|
missing.Slots[1].Design = new CultRecordRef<EquippableItemData>(new CultRecordKey("absent-design"));
|
|
|
|
|
var failures = new List<string>();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Null(Build(items, missing, failures));
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
var failure = Assert.Single(failures);
|
|
|
|
|
Assert.Contains("1,1", failure);
|
|
|
|
|
Assert.Contains("absent-design", failure);
|
|
|
|
|
|
|
|
|
|
missing.Slots.Add(new LoadoutSlot { Position = new int2(2, 2), Design = cache.RefOf<EquippableItemData>(cache.GetByName<GearData>("Orphan")) });
|
|
|
|
|
failures.Clear();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Null(Build(items, missing, failures));
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
Assert.Equal(2, failures.Count);
|
|
|
|
|
Assert.Contains("slot 1,1", failures[0]);
|
|
|
|
|
Assert.Contains("slot 2,2", failures[1]);
|
|
|
|
|
Assert.Contains("no available product of Orphan", failures[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Assert.Equal(before, new[] { Catalog, Run, Player }.Select(Hash).ToArray());
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:27:03 +02:00
|
|
|
[Fact]
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
public void FailedFitBuildsNothing()
|
2026-09-14 22:27:03 +02:00
|
|
|
{
|
|
|
|
|
using var cache = Open();
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
2026-09-14 22:27:03 +02:00
|
|
|
var clash = HandBuilt(cache);
|
|
|
|
|
clash.Slots[1].Position = HardpointCell;
|
|
|
|
|
var failures = new List<string>();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Null(Build(items, clash, failures));
|
2026-09-14 22:27:03 +02:00
|
|
|
var failure = Assert.Single(failures);
|
|
|
|
|
Assert.Contains("slot 0,0: Crate does not fit", failure);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 04:17:13 +02:00
|
|
|
// Two new makers, each with one Lamp product, upserted into the live cache so its enumeration order (z before a)
|
|
|
|
|
// disagrees with record-key order; a reopened snapshot could already hand them back sorted. Brand() requires at
|
|
|
|
|
// most one product per (maker, design), so each gets its own maker rather than sharing "Maker".
|
2026-09-14 22:27:03 +02:00
|
|
|
[Fact]
|
|
|
|
|
public void FirstAvailableProductInKeyOrderBuildsTheSlot()
|
|
|
|
|
{
|
2026-09-14 22:35:16 +02:00
|
|
|
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
|
|
|
|
var lamp = new CultRecordRef<CraftedItemData>(cache.RefOf(cache.GetByName<GearData>("Lamp")).Key);
|
2026-09-17 04:17:13 +02:00
|
|
|
var zMaker = cache.Upsert(new Faction { Name = "Zeta", ShortName = "ZET" });
|
|
|
|
|
var aMaker = cache.Upsert(new Faction { Name = "Alpha", ShortName = "ALP" });
|
|
|
|
|
cache.Commit(batch => batch.Upsert(typeof(FactionProductData), new FactionProductData { Name = "lamp-z", Design = lamp, Manufacturer = zMaker }, new CultRecordKey("lamp-z")));
|
|
|
|
|
cache.Commit(batch => batch.Upsert(typeof(FactionProductData), new FactionProductData { Name = "lamp-a", Design = lamp, Manufacturer = aMaker }, new CultRecordKey("lamp-a")));
|
2026-09-14 22:35:16 +02:00
|
|
|
Assert.Equal(new[] { "lamp-z", "lamp-a" }, cache.GetAll<FactionProductData>().Select(p => p.Name).Where(name => name.StartsWith("lamp-")));
|
2026-09-14 22:27:03 +02:00
|
|
|
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
2026-09-14 22:27:03 +02:00
|
|
|
var hand = HandBuilt(cache);
|
|
|
|
|
string LampProduct(Predicate<FactionProductData> available, List<string> failures)
|
|
|
|
|
{
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
var ship = Loadouts.Materialize(items, null, hand, available, failures);
|
2026-09-17 04:17:13 +02:00
|
|
|
if (ship == null) return null;
|
|
|
|
|
var unit = ship.Equipment.Single(item => item.EquippableItem != ship.Hull && item.Position.Equals(HardpointCell)).EquippableItem;
|
|
|
|
|
return items.Brand(unit).Product.Name;
|
2026-09-14 22:27:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var failures = new List<string>();
|
|
|
|
|
Assert.Equal("lamp-a", LampProduct(p => p.Name != "Lamp by Maker", failures));
|
|
|
|
|
Assert.Equal("lamp-z", LampProduct(p => p.Name != "Lamp by Maker" && p.Name != "lamp-a", failures));
|
|
|
|
|
Assert.Empty(failures);
|
|
|
|
|
Assert.Null(LampProduct(p => !p.Name.StartsWith("Lamp") && !p.Name.StartsWith("lamp"), failures));
|
|
|
|
|
Assert.Contains("slot 0,0: no available product of Lamp", Assert.Single(failures));
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 04:17:13 +02:00
|
|
|
// Materialize a hand-built ship, pack it into a zone, commit through Open() and reopen: every crafted instance
|
|
|
|
|
// still resolves in RunSave.Lots, its Data still equals the lot's Design, the origin is Attributed with the
|
|
|
|
|
// product's maker, and Quality is unchanged from the lot minted before the save.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void MaterializedLotsSurviveSaveAndReload()
|
|
|
|
|
{
|
|
|
|
|
Dictionary<int, float> preSaveQuality;
|
|
|
|
|
CultRecordKey maker;
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
{
|
|
|
|
|
maker = cache.RefOf(cache.GetByName<Faction>("Maker")).Key;
|
|
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
|
|
|
|
var hand = HandBuilt(cache);
|
|
|
|
|
var failures = new List<string>();
|
|
|
|
|
var ship = Build(items, hand, failures);
|
|
|
|
|
Assert.Empty(failures);
|
|
|
|
|
|
|
|
|
|
preSaveQuality = items.Lots.Lots.ToDictionary(kv => kv.Key, kv => kv.Value.Quality);
|
|
|
|
|
|
|
|
|
|
var pack = EntitySerializer.Pack(ship);
|
|
|
|
|
var game = new SavedGame
|
|
|
|
|
{
|
|
|
|
|
Factions = Array.Empty<CultRecordRef<Faction>>(),
|
|
|
|
|
Relationships = Array.Empty<FactionRelationship>(),
|
|
|
|
|
HomeZones = new Dictionary<int, int>(),
|
|
|
|
|
BossZones = new Dictionary<int, int>(),
|
|
|
|
|
DiscoveredZones = new[] { 0 },
|
|
|
|
|
ActionBarBindings = new SavedActionBarBinding[0],
|
|
|
|
|
Exit = -1
|
|
|
|
|
};
|
|
|
|
|
var zones = new[]
|
|
|
|
|
{
|
|
|
|
|
new SavedZone
|
|
|
|
|
{
|
|
|
|
|
Name = "Zone 0", AdjacentZones = Array.Empty<int>(), Factions = Array.Empty<int>(), Owner = -1,
|
|
|
|
|
Contents = new ZonePack { Entities = new List<EntityPack> { pack } }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
RunSave.Commit(cache, game, zones, items.Lots);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var cache = Open())
|
|
|
|
|
{
|
|
|
|
|
var lots = RunSave.Lots(cache);
|
|
|
|
|
var run = cache.GetGlobal<SavedGame>();
|
|
|
|
|
var zone = cache.Get(run.Zones[0]);
|
|
|
|
|
var crafted = EntitySerializer.Items(zone.Contents.Entities[0]).OfType<CraftedItemInstance>().ToArray();
|
|
|
|
|
Assert.NotEmpty(crafted);
|
|
|
|
|
foreach (var instance in crafted)
|
|
|
|
|
{
|
|
|
|
|
var lot = lots[instance.Lot];
|
|
|
|
|
Assert.Equal(instance.Data.Key, lot.Design.Key);
|
|
|
|
|
var attributed = Assert.IsType<Attributed>(lot.Origin);
|
|
|
|
|
Assert.Equal(maker, attributed.Faction.Key);
|
|
|
|
|
Assert.Equal(preSaveQuality[instance.Lot], lot.Quality);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FillInterior requires a capacitor product; this fixture hull adds one alongside a 4x4 hull with two identical
|
|
|
|
|
// Sensors hardpoints.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void MatchingHardpointsShareALot()
|
|
|
|
|
{
|
|
|
|
|
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
|
|
|
|
var maker = cache.RefOf(cache.GetByName<Faction>("Maker"));
|
|
|
|
|
var hullShape = new Shape(4, 4);
|
|
|
|
|
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
|
|
|
|
var hull = cache.Upsert(new HullData
|
|
|
|
|
{
|
|
|
|
|
Name = "Twin", HullType = HullType.Ship, Shape = hullShape, Price = 100,
|
|
|
|
|
Hardpoints =
|
|
|
|
|
{
|
|
|
|
|
new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() },
|
|
|
|
|
new HardpointData { Type = HardpointType.Sensors, Position = new int2(1, 0), Shape = new Shape() }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
var capacitor = cache.Upsert(new GearData { Name = "Cap", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new CapacitorData() } });
|
|
|
|
|
foreach (var (name, design) in new[] { ("Twin by Maker", hull.Key), ("Cap by Maker", capacitor.Key) })
|
|
|
|
|
cache.Upsert(new FactionProductData { Name = name, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = maker });
|
|
|
|
|
cache.FlushAsync().Wait();
|
|
|
|
|
|
|
|
|
|
var random = new Random(1);
|
|
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
|
|
|
|
var generator = new LoadoutGenerator(ref random, items, null, null, null, .5f);
|
|
|
|
|
var pack = generator.GenerateShipLoadout(candidate => candidate.Name == "Twin");
|
|
|
|
|
Assert.NotNull(pack);
|
|
|
|
|
|
|
|
|
|
var hardpointItems = pack.Equipment
|
|
|
|
|
.Where(e => (cache.Get(e.item.Data) as GearData)?.Hardpoint == HardpointType.Sensors)
|
|
|
|
|
.ToArray();
|
|
|
|
|
Assert.Equal(2, hardpointItems.Length);
|
|
|
|
|
Assert.Equal(hardpointItems[0].item.Lot, hardpointItems[1].item.Lot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A Lamp lot with Quality .2 and one role fill (lens .9): a stat reading that role gets .9, one reading none
|
|
|
|
|
// gets .2, and GetTier reads .2.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void StatsReadTheLot()
|
|
|
|
|
{
|
|
|
|
|
using var cache = Open();
|
|
|
|
|
var settings = RunSaveTests.TestSettings();
|
|
|
|
|
settings.Tiers = new[] { new RarityTier { Name = "Common", Quality = .2f, Rarity = 0, Color = new float3(1, 1, 1) } };
|
|
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), settings, _ => { });
|
|
|
|
|
var lamp = cache.GetByName<GearData>("Lamp");
|
|
|
|
|
var lotId = items.Lots.Add(new Lot
|
|
|
|
|
{
|
|
|
|
|
Design = cache.RefOf<ItemData>(lamp),
|
|
|
|
|
Origin = new Attributed(),
|
|
|
|
|
Quality = .2f,
|
|
|
|
|
Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .9f } }
|
|
|
|
|
});
|
|
|
|
|
var instance = (EquippableItem) items.CreateInstance(lotId);
|
|
|
|
|
|
|
|
|
|
var withRole = new PerformanceStat { FromRole = "lens", Min = 0, Max = 1, QualityExponent = 1 };
|
|
|
|
|
var noRole = new PerformanceStat { Min = 0, Max = 1, QualityExponent = 1 };
|
|
|
|
|
Assert.Equal(.9f, items.Evaluate(withRole, instance), 3);
|
|
|
|
|
Assert.Equal(.2f, items.Evaluate(noRole, instance), 3);
|
|
|
|
|
Assert.Equal(.2f, items.GetTier(instance).tier.Quality, 3);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:28:24 +02:00
|
|
|
[Fact]
|
|
|
|
|
public void OutOfRangeWeaponGroupIndexBuildsNothing()
|
|
|
|
|
{
|
|
|
|
|
using var cache = Open();
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
var stale = Armed(cache);
|
2026-09-14 22:28:24 +02:00
|
|
|
stale.WeaponGroups = new[] { new[] { 0, 2 }, new[] { -1 } };
|
|
|
|
|
var failures = new List<string>();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Null(Build(items, stale, failures));
|
2026-09-14 22:28:24 +02:00
|
|
|
Assert.Equal(new[] { "weapon group: no slot 2", "weapon group: no slot -1" }, failures);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:38:16 +02:00
|
|
|
[Fact]
|
|
|
|
|
public void UnequippingAGroupedWeaponRemovesItFromItsGroups()
|
|
|
|
|
{
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
using var cache = Open();
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
var ship = Build(items, Armed(cache), new List<string>());
|
2026-09-14 22:38:16 +02:00
|
|
|
var item = Assert.Single(ship.WeaponGroups[0].items);
|
|
|
|
|
Assert.NotNull(item.GetBehavior<Weapon>());
|
|
|
|
|
|
|
|
|
|
Assert.NotNull(ship.TryUnequip(item));
|
|
|
|
|
Assert.All(ship.WeaponGroups, group => Assert.Empty(group.items));
|
|
|
|
|
Assert.All(ship.WeaponGroups, group => Assert.Empty(group.weapons));
|
|
|
|
|
}
|
|
|
|
|
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
// A group index on a non-weapon slot would put a null weapon in the group; it is refused before anything is built.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void NonWeaponGroupIndexBuildsNothing()
|
|
|
|
|
{
|
|
|
|
|
using var cache = Open();
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
var lampGrouped = WithDesign(Armed(cache), 0, cache, "Lamp");
|
|
|
|
|
var failures = new List<string>();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Null(Build(items, lampGrouped, failures));
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
Assert.Equal("weapon group: slot 0,0: Lamp is not a weapon", Assert.Single(failures));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void MoreWeaponGroupsThanTheGameHasBuildsNothing()
|
|
|
|
|
{
|
|
|
|
|
using var cache = Open();
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
var wide = Armed(cache);
|
|
|
|
|
wide.WeaponGroups = wide.WeaponGroups.Append(new[] { 0 }).ToArray();
|
|
|
|
|
var failures = new List<string>();
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
Assert.Null(Build(items, wide, failures));
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
Assert.Equal("weapon groups: 7 groups, at most 6", Assert.Single(failures));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fewer groups, or none, still give the ship exactly WeaponGroupCount groups, so GenerateWeaponGroups and G1-G6
|
|
|
|
|
// bindings can index every one.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void FewerWeaponGroupsArePaddedToTheGameCount()
|
|
|
|
|
{
|
|
|
|
|
using var cache = Open();
|
2026-09-17 04:17:13 +02:00
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
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>
2026-09-14 22:51:37 +02:00
|
|
|
var armed = Armed(cache);
|
|
|
|
|
armed.WeaponGroups = new[] { new[] { 0 } };
|
|
|
|
|
foreach (var groups in new[] { armed.WeaponGroups, null })
|
|
|
|
|
{
|
|
|
|
|
armed.WeaponGroups = groups;
|
|
|
|
|
var failures = new List<string>();
|
|
|
|
|
var ship = Build(items, armed, failures);
|
|
|
|
|
Assert.Empty(failures);
|
|
|
|
|
Assert.Equal(6, ship.WeaponGroups.Length);
|
|
|
|
|
Assert.All(ship.WeaponGroups.Skip(1), group => Assert.Empty(group.items));
|
|
|
|
|
Assert.Equal(groups == null ? 0 : 1, ship.WeaponGroups[0].items.Count);
|
|
|
|
|
ship.GenerateWeaponGroups();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Make loadouts authored catalog presets with an editor capture command
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>
2026-09-14 23:09:43 +02:00
|
|
|
// Everything available: for tests about placement and failure lists.
|
|
|
|
|
private static Ship Build(ItemManager items, Loadout loadout, List<string> failures) =>
|
|
|
|
|
Loadouts.Materialize(items, null, loadout, _ => true, failures);
|
2026-09-14 22:27:03 +02:00
|
|
|
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
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>
2026-09-14 22:06:06 +02:00
|
|
|
private static (int2, ItemRotation, CultRecordKey) Describe(LoadoutSlot slot) => (slot.Position, slot.Rotation, slot.Design.Key);
|
|
|
|
|
|
|
|
|
|
private static string Hash(string path) => File.Exists(path) ? Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))) : "absent";
|
|
|
|
|
|
|
|
|
|
private static string[] SchemaNames(string path)
|
|
|
|
|
{
|
|
|
|
|
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
|
|
|
|
|
return snapshot.Records
|
|
|
|
|
.Select(record => snapshot.SchemaCatalog.Single(entry => entry.SchemaId == record.SchemaId).SchemaName)
|
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
}
|