Commit graph Aetheria/Assets/Scripts/ServerShared/StatResolver.cs
Author SHA1 Message Date
Meta Vens
b0d3354357 Gate 1: give the resolver a Forget(owner); correct the recompute-cost comment
StatResolver's _cache, _modifiers and _generations were keyed by owner and
nothing ever removed an entry, contradicting Sec 0b's promise that a resolver
entry is "destroyed at unequip / entity teardown." A ConsumableItemEffect that
expired still had a generations entry, and an unequipped EquippedItem's entries
lived on for the rest of the process -- the same shape of leak the catalog
object had before Cut 2, one level down.

Forget(owner) drops every entry for that owner across all three dictionaries;
Entity.TryUnequip and the consumable-expiry branch in Entity.Update now call it.
Also adds GenerationOwnerCount/CacheEntryCount/ModifierEntryCount, a test-only
surface a mutation test pins the retained-set claim against.

F3: InvalidateSource's comment claimed a stat "recomputes only when a term's
source changes." Entity.cs's UpdatePerformance calls it unconditionally every
tick for Heat and Durability, regardless of whether the value moved, so the
true invariant is "at most once per tick per (item, stat)." Corrected, and
noted Quality as the one source this scheme still meaningfully defends
against a real no-op tick (see F6 in ItemData.cs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:41:42 +02:00
Meta Vens
dbb8be39d9 Cut 2: give every entity its own StatResolver, delete the catalog leak
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>
2026-09-18 19:53:59 +02:00