From 5581911fb3fdc6ab3fce007560df25dff710fda5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 18 Sep 2026 20:41:54 +0200 Subject: [PATCH] Gate 2: Initialize owns detach/reattach; delete dead StatOwner (F8) 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 --- Assets/Scripts/ServerShared/Behaviors/Behaviors.cs | 4 ---- .../Scripts/ServerShared/Behaviors/StatModifier.cs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 934a9fe6f..4013d5c93 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -21,10 +21,6 @@ public abstract class Behavior protected Entity Entity => Item?.Entity ?? Consumable.Entity; public float Temperature => Item?.Temperature ?? Consumable.Entity.MaxTemp; - // The StatResolver key for whichever of Item/Consumable this behaviour has -- the two owners a resolved stat - // value or an attached modifier can belong to (StatResolver.cs, docs/stats-and-power-cut.md Cut 2). - protected object StatOwner => (object) Item ?? Consumable; - public float3 Direction { get diff --git a/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs b/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs index 40ef13eb5..71164bfcb 100644 --- a/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs +++ b/Assets/Scripts/ServerShared/Behaviors/StatModifier.cs @@ -69,8 +69,20 @@ public class StatModifier : Behavior, IInitializableBehavior, IDisposable, IAlwa // fresh and silently at every equip, now goes through StatValidation.ResolveStatField -- resolved once, // memoized, and refused loudly (at catalog load, or here if a caller built one without going through the // catalog) instead of leaving `_stats` null for ApplyModifier to throw an opaque NullReferenceException on. + // + // Gate 2 fix (Soul pass over Cut 2): Initialize is the entity's own re-activation hook, called exactly once + // per (re)activation for every StatModifier on it -- it is the one place that already knows "this behaviour's + // targets are about to change." Detaching an old attachment, if any, belongs here, not in Update's _executed/ + // _applied latch: that latch only tracks per-tick execution and was never told a re-Initialize had happened, + // so after deactivate -> unequip the target -> equip a replacement -> activate, _applied stayed true from the + // stale attachment and Update's `_executed && !_applied` guard never fired again -- the modifier silently + // stopped reaching anything. Making Initialize own the detach/reattach transition removes the split + // authority instead of just resetting the flag: whatever this modifier was attached to before is explicitly + // let go before new targets are computed, so the next Execute+Update reliably reapplies against them. public void Initialize() { + if (_applied) + RemoveModifier(); _targets = TargetsOf(Entity, _data); ValidateNoCycle(Entity, _data, _targets); }