StatModifier.Initialize recomputed _targets fresh every (re)activation but
never reset _applied, so after deactivate -> unequip the target -> equip a
replacement -> activate, Update saw `_executed && _applied` (both still true
from the stale attachment) and neither branch of its latch fired again --
the modifier silently stopped reaching anything. Differential evidence: the
same scenario resolved 6 pre-cut and 1 at Cut 2's HEAD.
Chose the ownership fix over the one-line flag reset: Initialize is the
entity's own re-activation hook, called exactly once per (re)activation for
every StatModifier on it, so it is the one place that already knows a
transition is happening. Detaching whatever this modifier was attached to
before recomputing _targets removes the split authority between Initialize
(which decides targets) and Update (which was silently also deciding
attachment state) instead of leaving Update to infer a transition it was
never told about.
F8: Behaviors.cs's `StatOwner` property had no production caller -- it was
added only to make one mutation test's mutant expressible. Deleted; the
mutation test now uses `Entity` (a real symbol) as its wrong-owner stand-in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PerformanceStat kept two Dictionary<Entity, ...> fields for attached modifiers,
grown by one key per entity that ever evaluated the stat and never trimmed
(docs/stats-and-power-cut.md §0.3). StatResolver.cs is the new sole owner of
every resolved (item, stat) value and every attached modifier, one instance
per Entity and reachable only from it. EquippedItem and ConsumableItemEffect
now read and write through it instead of through the catalog object; the two
dictionaries and their four accessors are deleted outright, not hidden.
StatModifier no longer resolves its (type name, field name) reference by
runtime reflection search on every equip: StatValidation.ResolveStatField
does it once, memoized, and StatValidation.ValidateStatModifiers checks it at
catalog load and at Upsert, naming the record instead of failing with an
opaque NullReferenceException the first time something equips it. A modifier
whose own magnitude stat is (directly or transitively, through another
modifier on the same entity) one of the stats it writes is refused at equip:
resolving it would depend on a value computed from itself one tick late.
A resolved value now caches by the generation of the sources its declared
Terms actually read, invalidated per source by EquippedItem.UpdatePerformance
(Heat, Durability) and ConsumableItemEffect.Update (ConsumableProgress), so a
stat recomputes only when a term it declared moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 2 of docs/addressables-cut.md. The asset move (git mv
Resources/{Prefabs,Schematics,Sprites/Icons} -> Content/...) and the
UnityHelpers.cs deletion already landed in 1fdf8151, picked up by a
concurrent session sharing this working tree; this commit is the rest
of Cut 2 landing on top of that shared state, atomically:
- Bump CultLib to cultlib-unity-v1.0.60 / caching-unity-v1.4.0 (both
45c2f40) in Packages/manifest.json, packages-lock.json and
Directory.Build.props, together with the 8 ServerShared
[CultInspectorAssetPath] usages becoming [CultInspectorAssetGuid] -
the .NET and Unity sides move together or nothing compiles.
- Assets/Scripts/EngineAssets.cs replaces UnityHelpers: loads an
Addressables key (bare GUID or guid[subAsset]) synchronously,
loading the GameObject and taking the component for the four sites
that load a component. ActionBarSlot, EntityInstance, ShipInstance
and ZoneRenderer call it instead of Resources.Load/UnityHelpers; the
inline "Assets/Resources/" prefix strips are gone with them.
CultCacheDrawers' schematic preview goes through
AssetDatabase.GUIDToAssetPath first.
- Assets/Scripts/Editor/EngineAssetCheck.cs validates every stored
[CultInspectorAssetGuid] value: 32 lowercase hex, resolves to a file,
is addressable, and satisfies its member's consumer-type contract
(Texture2D for icons/logos/schematics, a GameObject with the right
component for prefabs) - table-driven, so an attributed member with
no row fails loudly instead of being skipped.
- GameData/Aetheria.cc: the scratch migration (Aetheria.Shared + the
pinned CultLib revision, a throwaway console app, never landed)
walked all 138 attributed field slots across 180 documents,
rewriting the 30 non-empty Resources paths to their .meta GUIDs -
108 stayed empty, 0 unresolved, matching the map's census exactly.
Verification: dotnet test tests/Aetheria.Shared.Tests 61/61 green
against CultLib 45c2f40; Unity 6000.3.24f1 batchmode compiles clean;
EngineAssetCheck.Run passes all 30 stored references; the map's
negative greps are clean; tests/mutation_tests.py's 5 mutations (wrong
type, non-addressable, random guid, an old path, and an untabled
attributed member) are all caught and named, the control case and the
post-revert recompile stay clean, and the catalog/source are restored
byte-identical afterward.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Weapon.StanceAllowsFire is the one shared gate: true with no target, else the
shooter's own Entity.IsHostileTo(Target) -- not the target's stance toward the
shooter. It lives on the Weapon base class so every weapon subtype and every
caller (player input, TurretController, AI) goes through the same rule.
Wired at the two places weapons actually start dealing damage:
- InstantWeapon.Trigger (the base for InstantWeapon, AutoWeapon, ChargedWeapon
and LockWeapon/Launcher, all of which route real firing through Trigger).
- ConstantWeapon.Execute's continuous-beam loop, which stops firing (and
raises OnStopFiring) the moment the shooter's stance disallows it.
Also fixes LockWeapon's lock-building condition, which read the wrong
direction (target.IsHostileTo(shooter) instead of shooter.IsHostileTo(target))
-- the same backwards check as the HUD lock indicator, fixed alongside this in
ActionGameManager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator ruling (docs/item-provenance-cut.md Progress header, 2026-09-17):
delete EntityPack.PersistedBehaviors, IPersistentBehavior,
PersistentBehaviorData and MarkerPersistentBehaviorData outright rather than
keep patching around a feature with no production writer. Key(5) is retired
and left unused; other MessagePack keys are untouched.
Removes the pack/unpack code that read and wrote the field, the feature-only
test ReopenedPackKeepsPersistedBehaviors and its BarePack initializer, and the
matching "not yet reached" mutation notes in tests/mutation_tests.py.
ReopenSurvivesAGeneratedShipWithEntities is kept: it pins the reopen defect
class against a real generated ship, independent of this feature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dictionary<int2, PersistentBehaviorData[]> has no hash-resistant comparer
under CultCache's UntrustedData security, so any run store holding an
entity threw MessagePackSerializationException on reopen, even when the
dictionary was empty. EntityPack.PersistedBehaviors (key 5, unchanged)
becomes (int2 position, PersistentBehaviorData[] data)[], matching
Equipment/CargoBays/DockingBays. Pack writes pairs; Unpack builds a local
lookup before restoring behaviors.
PersistentBehaviorData had no concrete leaf and no Union, so nothing could
ever exercise its wire format; added MarkerPersistentBehaviorData as the
one placeholder leaf so ReopenedPackKeepsPersistedBehaviors can round-trip
real data ahead of a real IPersistentBehavior implementer landing.
Tests: RunSaveTests.BarePack uses an empty pairs array.
Added ReopenedPackKeepsPersistedBehaviors (a hand-built pack with two
positions, each with one persisted behavior, round-trips through commit
and reopen) and ReopenSurvivesAGeneratedShipWithEntities (a real ship from
LoadoutGenerator against a catalog fixture, committed into a SavedZone and
reopened, pinning the defect at the layer Soul's probe found it).
MaterializedLotsSurviveSaveAndReload and CommitKeepsOnlyReachableLots go
green; 36 of 36 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AetheriaMath predates the CultMath swap and re-implemented helpers CultMath
now owns. Callers use CultMath directly: decay (Ship, AetherDrive, Entity),
damp (VelocityConversion, FieldTester, FieldDriver), first_order_intercept
(TurretController, Combat, GuidedProjectile), quadratic_bezier (FieldDriver)
and smootherstep (Ship). Parameter order matches in every case, and those
formulas have the same float evaluation order, so behaviour is unchanged.
Galaxy's commented-out segment check names distance_to_segment.
The other overloads had no callers and are dropped without a CultMath
counterpart being needed: the double Damp overloads, FindDistanceToSegment,
GetCatmullRomPosition, GetCubicSplinePosition, Smoothstep(x), and the
Deg2Rad/Rad2Deg constants.
AetherDb census, factions and hardpoint-fit are byte-identical to the
pre-cut captures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every Unity.Mathematics using, alias and qualified name under Assets\Scripts
and tools now names CultMath. The nine `using static ...noise;` lines are
gone: snoise lives on CultMath's math. UnityExtensions loses float4.ToColor
and Color.ToFloat4, which duplicate the bridge's ToColor and ToCultMath; the
float3 colour pair stays because the bridge has none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>