acdream/tests/AcDream.Runtime.Tests/GameRuntimeContractTests.cs
Erik f5f7b4177f refactor(runtime): own interaction transactions
Move use throttling, appraisal identity, queued interactions, and exact post-arrival pickup state beneath RuntimeActionState. Keep App as the picker, movement, transport, and retained-presentation adapter while preserving retail send and UseDone ordering. Add reset, disposal, GUID-reuse, callback-reentrancy, and transport-failure coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-07-26 11:13:09 +02:00

225 lines
7.8 KiB
C#

using System.Reflection;
using AcDream.Runtime.Gameplay;
namespace AcDream.Runtime.Tests;
public sealed class GameRuntimeContractTests
{
[Fact]
public void InstanceClock_NormalizesHostTimeAndCanFreezeSimulation()
{
var first = new GameRuntimeClock();
var second = new GameRuntimeClock();
RuntimeFrameTime frame1 = first.Advance(0.25);
RuntimeFrameTime frozen = first.Advance(0.5, advanceSimulationTime: false);
RuntimeFrameTime invalid = first.Advance(double.NaN);
Assert.Equal(1UL, frame1.FrameNumber);
Assert.Equal(0.25, frame1.DeltaSeconds);
Assert.Equal(0.25, frame1.SimulationTimeSeconds);
Assert.Equal(2UL, frozen.FrameNumber);
Assert.Equal(0.5, frozen.DeltaSeconds);
Assert.Equal(0.25, frozen.SimulationTimeSeconds);
Assert.Equal(3UL, invalid.FrameNumber);
Assert.Equal(0.0, invalid.DeltaSeconds);
Assert.Equal(0.25, invalid.SimulationTimeSeconds);
Assert.Equal(0UL, second.FrameNumber);
Assert.Equal(0.0, second.SimulationTimeSeconds);
}
[Theory]
[InlineData(double.NegativeInfinity)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NaN)]
[InlineData(-1.0)]
[InlineData(0.0)]
public void InstanceClock_InvalidDeltaNormalizesToZero(double input)
{
Assert.Equal(0.0, GameRuntimeClock.NormalizeDeltaSeconds(input));
}
[Fact]
public void GenerationAndTeardownAcknowledgementAreExact()
{
RuntimeGenerationToken first = RuntimeGenerationToken.Initial.Next();
var complete = new RuntimeTeardownAcknowledgement(
first,
first.Next(),
RuntimeCommandStatus.Accepted,
RuntimeTeardownStage.Complete);
var stale = complete with
{
Status = RuntimeCommandStatus.StaleGeneration,
};
var partial = complete with
{
CompletedStages =
RuntimeTeardownStage.CommandsInert
| RuntimeTeardownStage.InboundDetached,
};
Assert.Equal(1UL, first.Value);
Assert.True(complete.IsComplete);
Assert.False(stale.IsComplete);
Assert.False(partial.IsComplete);
}
[Fact]
public void EventSequencersAreMonotonicAndInstanceScoped()
{
var first = new RuntimeEventSequencer();
var second = new RuntimeEventSequencer();
var generation = new RuntimeGenerationToken(7);
RuntimeEventStamp one = first.Next(generation, frameNumber: 11);
RuntimeEventStamp two = first.Next(generation, frameNumber: 12);
RuntimeEventStamp independent = second.Next(generation, frameNumber: 99);
Assert.Equal(1UL, one.Sequence);
Assert.Equal(2UL, two.Sequence);
Assert.Equal(1UL, independent.Sequence);
Assert.Equal(12UL, two.FrameNumber);
Assert.Equal(99UL, independent.FrameNumber);
}
[Fact]
public void EventSequencer_RestartsAtOneForEachSessionGeneration()
{
var sequencer = new RuntimeEventSequencer();
RuntimeEventStamp first = sequencer.Next(new(4), 10);
RuntimeEventStamp second = sequencer.Next(new(4), 11);
RuntimeEventStamp replacement = sequencer.Next(new(5), 12);
Assert.Equal(1UL, first.Sequence);
Assert.Equal(2UL, second.Sequence);
Assert.Equal(1UL, replacement.Sequence);
Assert.Equal(new RuntimeGenerationToken(5), replacement.Generation);
}
[Fact]
public void TraceRecorderNormalizesTypedDeltasWithoutRetainingOwners()
{
var recorder = new RuntimeTraceRecorder();
var stamp = new RuntimeEventStamp(
new RuntimeGenerationToken(3),
Sequence: 1,
FrameNumber: 8);
var command = new RuntimeCommandDelta(
stamp,
RuntimeCommandDomain.Selection,
(int)RuntimeSelectionCommand.SelectClosestHostile,
RuntimeCommandStatus.Accepted,
PrimaryObjectId: 0x50000001u);
var chat = new RuntimeChatDelta(
stamp with { Sequence = 2 },
new RuntimeChatEntry(
Revision: 4,
SenderGuid: 0x50000002u,
Kind: 2,
Sender: "Grey",
Text: "hello",
ChannelName: "General"));
recorder.OnCommand(in command);
recorder.OnChat(in chat);
Assert.Collection(
recorder.Entries,
entry =>
{
Assert.Equal(RuntimeTraceKind.Command, entry.Kind);
Assert.Equal(0x50000001u, entry.PrimaryObjectId);
Assert.Equal((long)RuntimeCommandStatus.Accepted, entry.Value);
},
entry =>
{
Assert.Equal(RuntimeTraceKind.Chat, entry.Kind);
Assert.Equal(4L, entry.Value);
Assert.Equal("General|Grey|hello", entry.Text);
});
}
[Fact]
public void TraceRecorderIncludesJ4GameplayStateInCheckpoint()
{
var recorder = new RuntimeTraceRecorder();
var stamp = new RuntimeEventStamp(new(3), 1, 8);
var checkpoint = new RuntimeStateCheckpoint(
new RuntimeGenerationToken(3),
RuntimeLifecycleState.InWorld,
FrameNumber: 8,
EntityCount: 2,
MaterializedEntityCount: 2,
InventoryObjectCount: 1,
InventoryContainerCount: 1,
new RuntimeInventoryStateSnapshot(
0u, 0u, 0, true, null, 2, 4, 1, 3),
new RuntimeCharacterSnapshot(
CharacterRevision: 5,
SpellbookRevision: 6,
default,
default,
LearnedSpellCount: 7,
ActiveEnchantmentCount: 0,
DesiredComponentCount: 0,
SkillCount: 8,
SpellbookFilters: 0x3FFFu),
new RuntimeSocialSnapshot(9, 10, 11, 0, 0, 0, 0),
ChatRevision: 12,
ChatCount: 13,
new RuntimeActionSnapshot(
SelectionRevision: 14,
SelectedObjectId: 0x50000001u,
PreviousObjectId: 0u,
PreviousValidObjectId: 0u,
CombatRevision: 15,
CombatMode: AcDream.Core.Combat.CombatMode.Missile,
TrackedTargetHealthCount: 1,
InteractionRevision: 16,
InteractionMode: InteractionModeKind.Use,
InteractionSourceObjectId: 0u,
InteractionTransactions: default),
default,
default);
recorder.AddCheckpoint(stamp, checkpoint);
RuntimeTraceEntry entry = Assert.Single(recorder.Entries);
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
Assert.Contains("social=9:10:11", entry.Text);
Assert.Contains(
"actions=14:50000001:15:4:1:16:1:00000000:0:00000000:" +
"00000000:00000000:00000000:0:0",
entry.Text);
}
[Fact]
public void J4GameplayOwnersHaveNoStaticMutableSessionState()
{
Type[] owners =
[
typeof(RuntimeCommunicationState),
typeof(RuntimeInventoryState),
typeof(RuntimeCharacterState),
typeof(RuntimeCharacterOptionsState),
typeof(RuntimeMovementSkillState),
typeof(RuntimeActionState),
typeof(InteractionState),
];
foreach (Type owner in owners)
{
FieldInfo[] mutable = owner
.GetFields(
BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic)
.Where(static field => !field.IsLiteral)
.ToArray();
Assert.Empty(mutable);
}
}
}