diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/DebugMessageTimingTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/DebugMessageTimingTests.cs new file mode 100644 index 000000000..042628b79 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/DebugMessageTimingTests.cs @@ -0,0 +1,145 @@ +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 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(); + } + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs b/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs index 05630d2f8..03c0f1a1e 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs @@ -56,7 +56,7 @@ public ref struct DebugInterpolatedStringHandler /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. public DebugInterpolatedStringHandler(int literalLength, int formattedCount, Mobiles.BaseAI ai) { - _debugActive = ai.Mobile?.Debug == true && Core.TickCount >= ai.NextDebugMessage; + _debugActive = ai.Mobile?.Debug == true && Core.TickCount - ai.NextDebugMessage >= 0; if (_debugActive) { @@ -79,7 +79,7 @@ public ref struct DebugInterpolatedStringHandler /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. public DebugInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Mobiles.BaseAI ai) { - _debugActive = ai.Mobile?.Debug == true && Core.TickCount >= ai.NextDebugMessage; + _debugActive = ai.Mobile?.Debug == true && Core.TickCount - ai.NextDebugMessage >= 0; if (_debugActive) {