refactor(runtime): own canonical action state
Move selection, combat, and interaction target mode under one Runtime owner; make plugins, retained UI, session routing, and typed runtime views borrow its exact children; and add failure-safe reset, instance isolation, source ownership, and normalized checkpoint coverage without changing retail ordering. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
bb45afef33
commit
b298f99f91
38 changed files with 711 additions and 93 deletions
|
|
@ -169,6 +169,17 @@ public sealed class GameRuntimeContractTests
|
|||
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),
|
||||
default,
|
||||
default);
|
||||
|
||||
|
|
@ -178,6 +189,9 @@ public sealed class GameRuntimeContractTests
|
|||
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",
|
||||
entry.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -190,6 +204,8 @@ public sealed class GameRuntimeContractTests
|
|||
typeof(RuntimeCharacterState),
|
||||
typeof(RuntimeCharacterOptionsState),
|
||||
typeof(RuntimeMovementSkillState),
|
||||
typeof(RuntimeActionState),
|
||||
typeof(InteractionState),
|
||||
];
|
||||
|
||||
foreach (Type owner in owners)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
public sealed class InteractionStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void Modes_HaveOneOwnerAndOneDeduplicatedTransitionStream()
|
||||
{
|
||||
var state = new InteractionState();
|
||||
var changes = new List<InteractionModeTransition>();
|
||||
state.Changed += changes.Add;
|
||||
|
||||
Assert.True(state.EnterUse());
|
||||
Assert.False(state.EnterUse());
|
||||
Assert.True(state.EnterExamine());
|
||||
Assert.True(state.EnterUseItemOnTarget(0x100u));
|
||||
Assert.True(state.Clear());
|
||||
|
||||
Assert.Equal(4, changes.Count);
|
||||
Assert.Equal(InteractionModeKind.None, state.Current.Kind);
|
||||
Assert.Equal(0x100u, changes[2].Current.SourceObjectId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseItemOnTarget_RejectsMissingSource()
|
||||
{
|
||||
var state = new InteractionState();
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => state.EnterUseItemOnTarget(0));
|
||||
Assert.Equal(InteractionMode.None, state.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetSession_RetryRepublishesAndOneObserverCannotStarveAnother()
|
||||
{
|
||||
var state = new InteractionState();
|
||||
state.EnterExamine();
|
||||
bool fail = true;
|
||||
int delivered = 0;
|
||||
state.Changed += _ =>
|
||||
{
|
||||
if (fail)
|
||||
{
|
||||
fail = false;
|
||||
throw new InvalidOperationException("transient");
|
||||
}
|
||||
};
|
||||
state.Changed += transition =>
|
||||
{
|
||||
Assert.Equal(InteractionMode.None, transition.Current);
|
||||
delivered++;
|
||||
};
|
||||
|
||||
Assert.Throws<AggregateException>(state.ResetSession);
|
||||
Assert.Equal(1, delivered);
|
||||
state.ResetSession();
|
||||
Assert.Equal(2, delivered);
|
||||
}
|
||||
}
|
||||
120
tests/AcDream.Runtime.Tests/Gameplay/RuntimeActionStateTests.cs
Normal file
120
tests/AcDream.Runtime.Tests/Gameplay/RuntimeActionStateTests.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
public sealed class RuntimeActionStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void ViewProjectsTheExactCanonicalChildrenAndRevisions()
|
||||
{
|
||||
using var actions = new RuntimeActionState();
|
||||
|
||||
actions.Selection.Select(
|
||||
0x50000001u,
|
||||
SelectionChangeSource.World);
|
||||
actions.Combat.SetCombatMode(CombatMode.Missile);
|
||||
actions.Combat.OnUpdateHealth(0x50000001u, 0.625f);
|
||||
actions.Interaction.EnterUseItemOnTarget(0x70000001u);
|
||||
|
||||
RuntimeActionSnapshot snapshot = actions.View.Snapshot;
|
||||
|
||||
Assert.Equal(1, snapshot.SelectionRevision);
|
||||
Assert.Equal(0x50000001u, snapshot.SelectedObjectId);
|
||||
Assert.Equal(2, snapshot.CombatRevision);
|
||||
Assert.Equal(CombatMode.Missile, snapshot.CombatMode);
|
||||
Assert.Equal(1, snapshot.TrackedTargetHealthCount);
|
||||
Assert.Equal(1, snapshot.InteractionRevision);
|
||||
Assert.Equal(
|
||||
InteractionModeKind.UseItemOnTarget,
|
||||
snapshot.InteractionMode);
|
||||
Assert.Equal(0x70000001u, snapshot.InteractionSourceObjectId);
|
||||
Assert.True(
|
||||
actions.View.TryGetHealth(0x50000001u, out float health));
|
||||
Assert.Equal(0.625f, health);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetSessionConvergesEveryChildAfterObserverFailures()
|
||||
{
|
||||
var actions = new RuntimeActionState();
|
||||
actions.Selection.Select(
|
||||
0x50000001u,
|
||||
SelectionChangeSource.World);
|
||||
actions.Combat.SetCombatMode(CombatMode.Melee);
|
||||
actions.Combat.OnUpdateHealth(0x50000001u, 0.25f);
|
||||
actions.Interaction.EnterExamine();
|
||||
|
||||
actions.Selection.Changed += static _ =>
|
||||
throw new InvalidOperationException("selection observer");
|
||||
actions.Combat.CombatModeChanged += static _ =>
|
||||
throw new InvalidOperationException("combat observer");
|
||||
actions.Interaction.Changed += static _ =>
|
||||
throw new InvalidOperationException("interaction observer");
|
||||
|
||||
Assert.Throws<AggregateException>(actions.ResetSession);
|
||||
|
||||
RuntimeActionOwnershipSnapshot snapshot =
|
||||
actions.CaptureOwnership();
|
||||
Assert.Equal(0u, snapshot.SelectedObjectId);
|
||||
Assert.Equal(0u, snapshot.PreviousObjectId);
|
||||
Assert.Equal(0u, snapshot.PreviousValidObjectId);
|
||||
Assert.Equal(CombatMode.NonCombat, snapshot.CombatMode);
|
||||
Assert.Equal(0, snapshot.TrackedTargetHealthCount);
|
||||
Assert.Equal(InteractionMode.None, snapshot.InteractionMode);
|
||||
Assert.Throws<AggregateException>(actions.Dispose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeIsTerminalAndConvergedAfterObserverFailures()
|
||||
{
|
||||
var actions = new RuntimeActionState();
|
||||
actions.Selection.Select(
|
||||
0x50000001u,
|
||||
SelectionChangeSource.World);
|
||||
actions.Combat.SetCombatMode(CombatMode.Magic);
|
||||
actions.Combat.OnUpdateHealth(0x50000001u, 0.5f);
|
||||
actions.Interaction.EnterUse();
|
||||
actions.Selection.Changed += static _ =>
|
||||
throw new InvalidOperationException("selection observer");
|
||||
actions.Combat.CombatModeChanged += static _ =>
|
||||
throw new InvalidOperationException("combat observer");
|
||||
actions.Interaction.Changed += static _ =>
|
||||
throw new InvalidOperationException("interaction observer");
|
||||
|
||||
Assert.Throws<AggregateException>(actions.Dispose);
|
||||
|
||||
RuntimeActionOwnershipSnapshot snapshot =
|
||||
actions.CaptureOwnership();
|
||||
Assert.True(snapshot.IsConverged);
|
||||
actions.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstancesAreFullyIsolated()
|
||||
{
|
||||
using var first = new RuntimeActionState();
|
||||
using var second = new RuntimeActionState();
|
||||
|
||||
first.Selection.Select(
|
||||
0x50000001u,
|
||||
SelectionChangeSource.World);
|
||||
first.Combat.SetCombatMode(CombatMode.Missile);
|
||||
first.Interaction.EnterUse();
|
||||
|
||||
Assert.Equal(
|
||||
new RuntimeActionSnapshot(
|
||||
0,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0,
|
||||
CombatMode.NonCombat,
|
||||
0,
|
||||
0,
|
||||
InteractionModeKind.None,
|
||||
0u),
|
||||
second.View.Snapshot);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
var inventory = new RuntimeInventoryState(entities);
|
||||
var character = new RuntimeCharacterState();
|
||||
var communication = new RuntimeCommunicationState();
|
||||
var actions = new RuntimeActionState();
|
||||
inventory.Shortcuts.Changed += static () => { };
|
||||
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(2u, 0.5f, true);
|
||||
|
|
@ -60,12 +61,19 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
new HashSet<uint> { 4u })));
|
||||
IDisposable subscription =
|
||||
communication.Events.Subscribe(new NoopObserver());
|
||||
actions.Selection.Select(
|
||||
0x50000002u,
|
||||
AcDream.Core.Selection.SelectionChangeSource.World);
|
||||
actions.Combat.SetCombatMode(AcDream.Core.Combat.CombatMode.Melee);
|
||||
actions.Combat.OnUpdateHealth(0x50000002u, 0.75f);
|
||||
actions.Interaction.EnterUse();
|
||||
|
||||
RuntimeGameplayOwnershipSnapshot populated =
|
||||
RuntimeGameplayOwnership.Capture(
|
||||
inventory,
|
||||
character,
|
||||
communication);
|
||||
communication,
|
||||
actions);
|
||||
|
||||
Assert.False(populated.IsConverged);
|
||||
Assert.Equal(1, populated.Inventory.ShortcutCount);
|
||||
|
|
@ -73,7 +81,13 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
Assert.Equal(1, populated.Communication.StreamSubscriberCount);
|
||||
Assert.True(populated.Communication.HasReplyTarget);
|
||||
Assert.True(populated.Communication.HasRetellTarget);
|
||||
Assert.Equal(0x50000002u, populated.Actions.SelectedObjectId);
|
||||
Assert.Equal(
|
||||
AcDream.Core.Combat.CombatMode.Melee,
|
||||
populated.Actions.CombatMode);
|
||||
Assert.Equal(1, populated.Actions.TrackedTargetHealthCount);
|
||||
|
||||
actions.Dispose();
|
||||
communication.Dispose();
|
||||
character.Dispose();
|
||||
inventory.Dispose();
|
||||
|
|
@ -83,7 +97,8 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
RuntimeGameplayOwnership.Capture(
|
||||
inventory,
|
||||
character,
|
||||
communication);
|
||||
communication,
|
||||
actions);
|
||||
|
||||
Assert.True(retired.IsConverged);
|
||||
Assert.Equal(0, retired.Inventory.ShortcutSubscriberCount);
|
||||
|
|
@ -99,6 +114,7 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
var inventory = new RuntimeInventoryState(entities);
|
||||
var character = new RuntimeCharacterState();
|
||||
var communication = new RuntimeCommunicationState();
|
||||
var actions = new RuntimeActionState();
|
||||
|
||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
|
|
@ -119,12 +135,14 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
Assert.Throws<AggregateException>(inventory.Dispose);
|
||||
Assert.Throws<AggregateException>(character.Dispose);
|
||||
communication.Dispose();
|
||||
actions.Dispose();
|
||||
|
||||
RuntimeGameplayOwnershipSnapshot retired =
|
||||
RuntimeGameplayOwnership.Capture(
|
||||
inventory,
|
||||
character,
|
||||
communication);
|
||||
communication,
|
||||
actions);
|
||||
|
||||
Assert.True(retired.IsConverged);
|
||||
Assert.Equal(1, retired.Communication.DispatchFailureCount);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue