ModernUO/Projects/UOContent.Tests/Tests/Mobiles/AI/DebugMessageTimingTests.cs
Robert Dickey 9d9e672a09
fix(ai): make debug-message cooldown comparisons wraparound-safe (#2659)
When an AI debug-message cooldown crosses the signed tick-counter boundary, DebugInterpolatedStringHandler can emit before the deadline or suppress a message after it. Both constructors compare absolute tick values.

Use subtraction-based deadline comparisons in both constructors, following dev-docs/tick-counts.md. This is a two-line production change; message formatting, cooldown duration, and gameplay behavior are unchanged.

Adds 35 regression cases exercising the actual handler and DebugSayFormatted:
- Both constructor overloads, with debugging enabled and disabled.
- Before/at/after deadlines on positive and negative clocks.
- Future and expired deadlines across signed wraparound.
- Cooldown rearming across wraparound and buffer clearing.
- A compiler-generated interpolated call through the public extension method.

Validation on Linux / .NET 10, based on clean upstream 35e3a31b4:
- Release build: 0 warnings, 0 errors.
- New tests against unchanged production code: 28 passed, 7 failed.
- With the fix: 35 passed, including without client map data (no skips).
- Selected AI/pet suites: 134 passed / 7 failed before; 141 passed / 0 failed after.

Tests use synthetic entities; no running shard or world saves. The faulty comparisons were reverified on upstream main before submission. Windows execution remains unverified. The tests exercise explicitly assigned deadlines; deadline initialization is outside this patch.

Addresses only the DebugInterpolatedStringHandler comparison item in #2627; it does not close the other audit items.
2026-09-19 09:59:00 -07:00

145 lines
4.3 KiB
C#

using System.Collections.Generic;
using System.Globalization;
using Server;
using Server.Mobiles;
using Server.Mobiles.AI.BaseAI;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
[Collection("Sequential UOContent Tests")]
public class DebugMessageTimingTests
{
public static IEnumerable<object[]> DeadlineCases()
{
(long Now, long Deadline, bool Due)[] clocks =
[
(99, 100, false), (100, 100, true), (101, 100, true),
(-101, -100, false), (-100, -100, true), (-99, -100, true),
(long.MaxValue - 10, long.MinValue + 10, false),
(long.MinValue + 10, long.MaxValue - 10, true)
];
foreach (var (now, deadline, due) in clocks)
{
foreach (var withProvider in new[] { false, true })
{
foreach (var debug in new[] { false, true })
{
yield return [now, deadline, due, withProvider, debug];
}
}
}
}
[Theory]
[MemberData(nameof(DeadlineCases))]
public void Handler_EmitsOnlyWhenDebugEnabledAndDue(
long now, long deadline, bool due, bool withProvider, bool debug)
{
var previousTick = Core.TickCount;
var pet = new PetTestStub();
var ai = pet.AIObject;
pet.Debug = debug;
ai.NextDebugMessage = deadline;
Core._tickCount = now;
var handler = withProvider
? new DebugInterpolatedStringHandler(6, 1, CultureInfo.InvariantCulture, ai)
: new DebugInterpolatedStringHandler(6, 1, ai);
try
{
handler.AppendLiteral("value ");
handler.AppendFormatted(42);
Assert.Equal(debug && due ? "value 42" : "", handler.Text.ToString());
ai.DebugSayFormatted(ref handler, 50);
Assert.Equal(debug && due ? unchecked(now + 50) : deadline, ai.NextDebugMessage);
Assert.True(handler.Text.IsEmpty);
}
finally
{
handler.Clear();
Core._tickCount = previousTick;
pet.Delete();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Handler_RearmedCooldown_WaitsAcrossWrap(bool withProvider)
{
var previousTick = Core.TickCount;
var pet = new PetTestStub { Debug = true };
var ai = pet.AIObject;
try
{
Core._tickCount = long.MaxValue - 10;
ai.NextDebugMessage = Core.TickCount;
Emit(ai, withProvider);
var deadline = ai.NextDebugMessage;
Assert.Equal(long.MinValue + 39, deadline);
Core._tickCount = long.MaxValue - 9;
Emit(ai, withProvider);
Assert.Equal(deadline, ai.NextDebugMessage);
Core._tickCount = deadline - 1;
Emit(ai, withProvider);
Assert.Equal(deadline, ai.NextDebugMessage);
Core._tickCount = deadline;
Emit(ai, withProvider);
Assert.Equal(deadline + 50, ai.NextDebugMessage);
}
finally
{
Core._tickCount = previousTick;
pet.Delete();
}
}
[Fact]
public void InterpolatedCall_UsesHandlerAcrossWrap()
{
var previousTick = Core.TickCount;
var pet = new PetTestStub { Debug = true };
var ai = pet.AIObject;
try
{
Core._tickCount = long.MaxValue - 10;
ai.NextDebugMessage = long.MinValue + 10;
var value = 42;
ai.DebugSayFormatted($"value {value}", 50);
Assert.Equal(long.MinValue + 10, ai.NextDebugMessage);
Core._tickCount = long.MinValue + 10;
ai.DebugSayFormatted($"value {value}", 50);
Assert.Equal(long.MinValue + 60, ai.NextDebugMessage);
}
finally
{
Core._tickCount = previousTick;
pet.Delete();
}
}
private static void Emit(BaseAI ai, bool withProvider)
{
var handler = withProvider
? new DebugInterpolatedStringHandler(5, 0, CultureInfo.InvariantCulture, ai)
: new DebugInterpolatedStringHandler(5, 0, ai);
try
{
handler.AppendLiteral("debug");
ai.DebugSayFormatted(ref handler, 50);
}
finally
{
handler.Clear();
}
}
}