NEO-93: Preserve LastAdvancedUtc on clock regression and guard zero timings.
Do not move the lazy-tick marker backward when nowUtc regresses; reject non-positive catalog durations and break the phase loop when cursor stalls. Co-authored-by: Cursor <cursoragent@cursor.com>pull/131/head
parent
a22c900655
commit
e4f08b2015
|
|
@ -277,4 +277,43 @@ public sealed class NpcRuntimeOperationsTests
|
||||||
Assert.Equal(T0.AddHours(1).AddSeconds(10), snapshot.PhaseStartedUtc);
|
Assert.Equal(T0.AddHours(1).AddSeconds(10), snapshot.PhaseStartedUtc);
|
||||||
Assert.Equal(T0.AddHours(1).AddSeconds(10), runtime.LastAdvancedUtc);
|
Assert.Equal(T0.AddHours(1).AddSeconds(10), runtime.LastAdvancedUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdvanceAll_ShouldPreserveLastAdvancedUtc_WhenClockRegresses()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (runtime, threat, behavior) = CreateFixture();
|
||||||
|
Advance(T0, runtime, threat, behavior);
|
||||||
|
runtime.LastAdvancedUtc = T0.AddSeconds(10);
|
||||||
|
// Act
|
||||||
|
Advance(T0.AddSeconds(5), runtime, threat, behavior);
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(T0.AddSeconds(10), runtime.LastAdvancedUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HasPositiveTimingDurations_ShouldRejectNonPositiveCatalogTimings()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var invalidCooldown = new NpcBehaviorDefRow(
|
||||||
|
"bad",
|
||||||
|
"Bad",
|
||||||
|
"melee_pressure",
|
||||||
|
100,
|
||||||
|
8.0,
|
||||||
|
16.0,
|
||||||
|
1.5,
|
||||||
|
15,
|
||||||
|
0.0);
|
||||||
|
var invalidWindup = invalidCooldown with { Id = "bad2", TelegraphWindupSeconds = 0.0 };
|
||||||
|
var valid = invalidCooldown with { AttackCooldownSeconds = 3.0, TelegraphWindupSeconds = 1.5 };
|
||||||
|
// Act
|
||||||
|
var cooldownOk = NpcRuntimeOperations.HasPositiveTimingDurations(invalidCooldown);
|
||||||
|
var windupOk = NpcRuntimeOperations.HasPositiveTimingDurations(invalidWindup);
|
||||||
|
var validOk = NpcRuntimeOperations.HasPositiveTimingDurations(valid);
|
||||||
|
// Assert
|
||||||
|
Assert.False(cooldownOk);
|
||||||
|
Assert.False(windupOk);
|
||||||
|
Assert.True(validOk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ public static class NpcRuntimeOperations
|
||||||
behaviorRegistry);
|
behaviorRegistry);
|
||||||
}
|
}
|
||||||
|
|
||||||
runtimeStore.LastAdvancedUtc = nowUtc;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,6 +92,11 @@ public static class NpcRuntimeOperations
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!HasPositiveTimingDurations(behavior))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!runtimeStore.TryGet(npcInstanceId, out var snapshot))
|
if (!runtimeStore.TryGet(npcInstanceId, out var snapshot))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -137,6 +141,7 @@ public static class NpcRuntimeOperations
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var loopCursorBefore = cursor;
|
||||||
switch (snapshot.BehaviorState)
|
switch (snapshot.BehaviorState)
|
||||||
{
|
{
|
||||||
case NpcBehaviorState.Idle:
|
case NpcBehaviorState.Idle:
|
||||||
|
|
@ -159,7 +164,7 @@ public static class NpcRuntimeOperations
|
||||||
cursor,
|
cursor,
|
||||||
telegraph);
|
telegraph);
|
||||||
// telemetry: npc_state_transition (NEO-96)
|
// telemetry: npc_state_transition (NEO-96)
|
||||||
continue;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case NpcBehaviorState.TelegraphWindup:
|
case NpcBehaviorState.TelegraphWindup:
|
||||||
|
|
@ -178,7 +183,7 @@ public static class NpcRuntimeOperations
|
||||||
cursor,
|
cursor,
|
||||||
activeTelegraph: null);
|
activeTelegraph: null);
|
||||||
// telemetry: telegraph_fired / npc_state_transition (NEO-96); logical attack_execute → recover (NEO-95 applies attackDamage)
|
// telemetry: telegraph_fired / npc_state_transition (NEO-96); logical attack_execute → recover (NEO-95 applies attackDamage)
|
||||||
continue;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case NpcBehaviorState.AttackExecute:
|
case NpcBehaviorState.AttackExecute:
|
||||||
|
|
@ -189,7 +194,7 @@ public static class NpcRuntimeOperations
|
||||||
cursor,
|
cursor,
|
||||||
activeTelegraph: null);
|
activeTelegraph: null);
|
||||||
// telemetry: npc_state_transition (NEO-96); NEO-95 applies attackDamage here
|
// telemetry: npc_state_transition (NEO-96); NEO-95 applies attackDamage here
|
||||||
continue;
|
break;
|
||||||
|
|
||||||
case NpcBehaviorState.Recover:
|
case NpcBehaviorState.Recover:
|
||||||
{
|
{
|
||||||
|
|
@ -208,15 +213,24 @@ public static class NpcRuntimeOperations
|
||||||
cursor,
|
cursor,
|
||||||
telegraph);
|
telegraph);
|
||||||
// telemetry: npc_state_transition (NEO-96)
|
// telemetry: npc_state_transition (NEO-96)
|
||||||
continue;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cursor <= loopCursorBefore)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Catalog timings must be positive; CI/content gate enforces for prototype defs.</summary>
|
||||||
|
internal static bool HasPositiveTimingDurations(NpcBehaviorDefRow behavior) =>
|
||||||
|
behavior.AttackCooldownSeconds > 0 && behavior.TelegraphWindupSeconds > 0;
|
||||||
|
|
||||||
private static bool TryHasAggroHolder(
|
private static bool TryHasAggroHolder(
|
||||||
string npcInstanceId,
|
string npcInstanceId,
|
||||||
IThreatStateStore threatStore,
|
IThreatStateStore threatStore,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue