refactor(input): own gameplay action routing
Move the sole semantic action-priority graph, combat and diagnostic commands, and retained-root item-drop lifetime behind focused typed owners. Preserve retail toggle behavior, explicit auto-wield cancellation, shutdown quiescence, and symmetric callback cleanup. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
8b8afeefa3
commit
4eae9b4f5a
25 changed files with 2608 additions and 418 deletions
|
|
@ -0,0 +1,190 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.Tests.Combat;
|
||||
|
||||
public sealed class LiveCombatModeCommandControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void OutsideWorld_IsCompleteNoOpBeforeEquipmentQuery()
|
||||
{
|
||||
var harness = new Harness { Authority = { IsInWorldValue = false } };
|
||||
|
||||
harness.Controller.Toggle();
|
||||
|
||||
Assert.Equal(0, harness.Equipment.QueryCount);
|
||||
Assert.Empty(harness.Calls);
|
||||
Assert.Equal(CombatMode.NonCombat, harness.Combat.CurrentMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PeaceWithBow_PreservesIntentSendLocalStateFeedbackOrder()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Equipment.Items.Add(new ClientObject
|
||||
{
|
||||
ObjectId = 1,
|
||||
CurrentlyEquippedLocation = EquipMask.MissileWeapon,
|
||||
Type = ItemType.MissileWeapon,
|
||||
CombatUse = 2,
|
||||
});
|
||||
harness.Combat.CombatModeChanged += mode =>
|
||||
harness.Calls.Add($"state:{mode}");
|
||||
|
||||
harness.Controller.Toggle();
|
||||
|
||||
Assert.Equal(1, harness.Equipment.QueryCount);
|
||||
Assert.Equal(CombatMode.Missile, harness.Combat.CurrentMode);
|
||||
Assert.Equal(
|
||||
[
|
||||
"intent",
|
||||
"send:Missile",
|
||||
"state:Missile",
|
||||
"log:combat: Combat mode Missile",
|
||||
"toast:Combat mode Missile",
|
||||
],
|
||||
harness.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveCombat_TogglesToPeaceRegardlessOfEquipmentDefault()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Combat.SetCombatMode(CombatMode.Magic);
|
||||
|
||||
harness.Controller.Toggle();
|
||||
|
||||
Assert.Equal(0, harness.Equipment.QueryCount);
|
||||
Assert.Equal([CombatMode.NonCombat], harness.Authority.Requests);
|
||||
Assert.Equal(CombatMode.NonCombat, harness.Combat.CurrentMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PeaceWithIncompatibleHeldItem_PrintsRetailNoticeWithoutSending()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Equipment.Items.Add(new ClientObject
|
||||
{
|
||||
ObjectId = 1,
|
||||
Name = "Torch",
|
||||
CurrentlyEquippedLocation = EquipMask.Held,
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
|
||||
harness.Controller.Toggle();
|
||||
|
||||
Assert.Empty(harness.Authority.Requests);
|
||||
Assert.Equal(CombatMode.NonCombat, harness.Combat.CurrentMode);
|
||||
Assert.Equal(
|
||||
[
|
||||
"intent",
|
||||
"log:combat: You can't enter combat mode while wielding the Torch",
|
||||
"system:You can't enter combat mode while wielding the Torch",
|
||||
],
|
||||
harness.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransportFailure_LeavesLocalModeAndFeedbackUnpublished()
|
||||
{
|
||||
var harness = new Harness { Authority = { ThrowOnSend = true } };
|
||||
|
||||
Assert.Throws<InvalidOperationException>(harness.Controller.Toggle);
|
||||
|
||||
Assert.Equal(["intent", "send:Melee"], harness.Calls);
|
||||
Assert.Equal(CombatMode.NonCombat, harness.Combat.CurrentMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeferredSlot_IsNoOpBeforeBindAndForwardsOneOwnerAfterBind()
|
||||
{
|
||||
var slot = new LiveCombatModeCommandSlot();
|
||||
var target = new FakeCommand();
|
||||
|
||||
slot.Toggle();
|
||||
slot.Bind(target);
|
||||
slot.Toggle();
|
||||
slot.Bind(target);
|
||||
|
||||
Assert.Equal(1, target.ToggleCount);
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => slot.Bind(new FakeCommand()));
|
||||
slot.Unbind(target);
|
||||
slot.Toggle();
|
||||
Assert.Equal(1, target.ToggleCount);
|
||||
|
||||
slot.Bind(target);
|
||||
Action copied = slot.Toggle;
|
||||
slot.Deactivate();
|
||||
copied();
|
||||
Assert.Equal(1, target.ToggleCount);
|
||||
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
|
||||
}
|
||||
|
||||
private sealed class Harness
|
||||
{
|
||||
public Harness()
|
||||
{
|
||||
Authority = new FakeAuthority(Calls);
|
||||
Equipment = new FakeEquipment();
|
||||
Intent = new FakeIntent(Calls);
|
||||
Controller = new LiveCombatModeCommandController(
|
||||
Authority,
|
||||
Equipment,
|
||||
Combat,
|
||||
Intent,
|
||||
line => Calls.Add($"log:{line}"),
|
||||
line => Calls.Add($"toast:{line}"),
|
||||
line => Calls.Add($"system:{line}"));
|
||||
}
|
||||
|
||||
public List<string> Calls { get; } = [];
|
||||
public FakeAuthority Authority { get; }
|
||||
public FakeEquipment Equipment { get; }
|
||||
public CombatState Combat { get; } = new();
|
||||
public FakeIntent Intent { get; }
|
||||
public LiveCombatModeCommandController Controller { get; }
|
||||
}
|
||||
|
||||
private sealed class FakeAuthority(List<string> calls)
|
||||
: ILiveCombatModeAuthority
|
||||
{
|
||||
public bool IsInWorldValue { get; set; } = true;
|
||||
public bool IsInWorld => IsInWorldValue;
|
||||
public bool ThrowOnSend { get; set; }
|
||||
public List<CombatMode> Requests { get; } = [];
|
||||
|
||||
public void SendChangeCombatMode(CombatMode mode)
|
||||
{
|
||||
calls.Add($"send:{mode}");
|
||||
if (ThrowOnSend)
|
||||
throw new InvalidOperationException("send");
|
||||
Requests.Add(mode);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeEquipment : ICombatEquipmentSource
|
||||
{
|
||||
public List<ClientObject> Items { get; } = [];
|
||||
public int QueryCount { get; private set; }
|
||||
|
||||
public IReadOnlyList<ClientObject> GetOrderedEquipment()
|
||||
{
|
||||
QueryCount++;
|
||||
return Items;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeIntent(List<string> calls)
|
||||
: IExplicitCombatModeIntentSink
|
||||
{
|
||||
public void NotifyExplicitCombatModeRequest() => calls.Add("intent");
|
||||
}
|
||||
|
||||
private sealed class FakeCommand : ILiveCombatModeCommand
|
||||
{
|
||||
public int ToggleCount { get; private set; }
|
||||
public void Toggle() => ToggleCount++;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Tests.Diagnostics;
|
||||
|
||||
[Collection(AcDream.App.Tests.World.WorldEnvironmentControllerCollection.Name)]
|
||||
public sealed class RuntimeDiagnosticCommandControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Handle_RoutesCanonicalEnvironmentSceneAndDumpOwners()
|
||||
{
|
||||
var source = new FakeNearbySource();
|
||||
var logs = new List<string>();
|
||||
var toasts = new List<string>();
|
||||
var scene = new WorldSceneDebugState();
|
||||
var controller = new RuntimeDiagnosticCommandController(
|
||||
new WorldEnvironmentController(),
|
||||
scene,
|
||||
pointer: null,
|
||||
new NearbyWorldDiagnosticDumper(source, logs.Add),
|
||||
toasts.Add);
|
||||
|
||||
Assert.True(controller.Handle(InputAction.AcdreamToggleCollisionWires));
|
||||
Assert.True(scene.CollisionWireframesVisible);
|
||||
Assert.True(controller.Handle(InputAction.AcdreamCycleTimeOfDay));
|
||||
Assert.True(controller.Handle(InputAction.AcdreamCycleWeather));
|
||||
Assert.True(controller.Handle(InputAction.AcdreamDumpNearby));
|
||||
Assert.True(controller.Handle(InputAction.AcdreamSensitivityDown));
|
||||
Assert.True(controller.Handle(InputAction.AcdreamSensitivityUp));
|
||||
Assert.False(controller.Handle(InputAction.MovementForward));
|
||||
|
||||
Assert.Contains("Collision wireframes ON", toasts);
|
||||
Assert.Contains("Time override = 0.00", toasts);
|
||||
Assert.Contains("Weather = Overcast", toasts);
|
||||
Assert.Equal(3, toasts.Count);
|
||||
Assert.Contains(logs, line => line.StartsWith(
|
||||
"=== F3 DEBUG DUMP ===", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearbyDump_PreservesLandblockCoordinatesRangeAndDistanceOrder()
|
||||
{
|
||||
var source = new FakeNearbySource
|
||||
{
|
||||
ObserverPositionValue = new Vector3(200f, -1f, 3f),
|
||||
LiveCenterXValue = 10,
|
||||
LiveCenterYValue = 20,
|
||||
TotalShadowObjectsValue = 4,
|
||||
};
|
||||
source.Entities.Add(Entity(2, new Vector3(203f, -1f, 3f)));
|
||||
source.Entities.Add(Entity(1, new Vector3(201f, -1f, 3f)));
|
||||
source.Entities.Add(Entity(3, new Vector3(216f, -1f, 3f)));
|
||||
source.Shadows.Add(new ShadowEntry(
|
||||
EntityId: 5,
|
||||
GfxObjId: 6,
|
||||
Position: new Vector3(202f, -1f, 3f),
|
||||
Rotation: Quaternion.Identity,
|
||||
Radius: 0.5f,
|
||||
CollisionType: ShadowCollisionType.Sphere));
|
||||
var lines = new List<string>();
|
||||
|
||||
new NearbyWorldDiagnosticDumper(source, lines.Add).Dump();
|
||||
|
||||
Assert.Contains("landblock=0x0B13FFFF local=(8.00,191.00)", lines[0]);
|
||||
Assert.Contains("total shadow objects: 4", lines[0]);
|
||||
Assert.Equal(" VISIBLE entities within 15m: 2", lines[1]);
|
||||
Assert.Contains("id=0x00000001", lines[2]);
|
||||
Assert.Contains("id=0x00000002", lines[3]);
|
||||
Assert.Equal(" SHADOW objects within 15m: 1", lines[4]);
|
||||
Assert.Contains("id=0x00000005", lines[5]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeferredSlot_ClaimsKnownActionsBeforeBindAndForwardsAfterBind()
|
||||
{
|
||||
var slot = new RuntimeDiagnosticCommandSlot();
|
||||
var target = new FakeCommands();
|
||||
|
||||
Assert.True(slot.Handle(InputAction.AcdreamDumpNearby));
|
||||
Assert.False(slot.Handle(InputAction.MovementForward));
|
||||
slot.CycleTimeOfDay();
|
||||
|
||||
slot.Bind(target);
|
||||
slot.Bind(target);
|
||||
Assert.True(slot.Handle(InputAction.AcdreamDumpNearby));
|
||||
slot.CycleTimeOfDay();
|
||||
slot.CycleWeather();
|
||||
slot.ToggleCollisionWireframes();
|
||||
|
||||
Assert.Equal(4, target.CallCount);
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => slot.Bind(new FakeCommands()));
|
||||
slot.Unbind(target);
|
||||
slot.CycleWeather();
|
||||
Assert.Equal(4, target.CallCount);
|
||||
|
||||
slot.Bind(target);
|
||||
Action copied = slot.CycleWeather;
|
||||
slot.Deactivate();
|
||||
copied();
|
||||
Assert.Equal(4, target.CallCount);
|
||||
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullToast_DoesNotSuppressDiagnosticStateChanges()
|
||||
{
|
||||
var environment = new WorldEnvironmentController();
|
||||
var scene = new WorldSceneDebugState();
|
||||
var sensitivity = new FakeSensitivity();
|
||||
var controller = new RuntimeDiagnosticCommandController(
|
||||
environment,
|
||||
scene,
|
||||
sensitivity,
|
||||
new NearbyWorldDiagnosticDumper(new FakeNearbySource()),
|
||||
toast: null);
|
||||
|
||||
controller.CycleTimeOfDay();
|
||||
controller.CycleWeather();
|
||||
controller.ToggleCollisionWireframes();
|
||||
Assert.True(controller.Handle(InputAction.AcdreamSensitivityDown));
|
||||
Assert.True(controller.Handle(InputAction.AcdreamSensitivityUp));
|
||||
|
||||
Assert.Equal(0f, environment.DayFraction);
|
||||
Assert.Equal(WeatherKind.Overcast, environment.Weather.Kind);
|
||||
Assert.True(scene.CollisionWireframesVisible);
|
||||
Assert.Equal(2, sensitivity.AdjustCount);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(uint id, Vector3 position) => new()
|
||||
{
|
||||
Id = id,
|
||||
SourceGfxObjOrSetupId = id + 100,
|
||||
Position = position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = [],
|
||||
};
|
||||
|
||||
private sealed class FakeNearbySource : INearbyWorldDiagnosticSource
|
||||
{
|
||||
public Vector3 ObserverPositionValue { get; set; }
|
||||
public int LiveCenterXValue { get; set; }
|
||||
public int LiveCenterYValue { get; set; }
|
||||
public int TotalShadowObjectsValue { get; set; }
|
||||
public List<WorldEntity> Entities { get; } = [];
|
||||
public List<ShadowEntry> Shadows { get; } = [];
|
||||
|
||||
public Vector3 ObserverPosition => ObserverPositionValue;
|
||||
public int LiveCenterX => LiveCenterXValue;
|
||||
public int LiveCenterY => LiveCenterYValue;
|
||||
public int TotalShadowObjects => TotalShadowObjectsValue;
|
||||
public IReadOnlyList<WorldEntity> WorldEntities => Entities;
|
||||
public IEnumerable<ShadowEntry> ShadowEntries => Shadows;
|
||||
}
|
||||
|
||||
private sealed class FakeCommands : IRuntimeDiagnosticCommands
|
||||
{
|
||||
public int CallCount { get; private set; }
|
||||
public bool Handle(InputAction action)
|
||||
{
|
||||
CallCount++;
|
||||
return true;
|
||||
}
|
||||
public void CycleTimeOfDay() => CallCount++;
|
||||
public void CycleWeather() => CallCount++;
|
||||
public void ToggleCollisionWireframes() => CallCount++;
|
||||
}
|
||||
|
||||
private sealed class FakeSensitivity : IPointerSensitivityCommands
|
||||
{
|
||||
public int AdjustCount { get; private set; }
|
||||
|
||||
public string AdjustSensitivity(float factor)
|
||||
{
|
||||
AdjustCount++;
|
||||
return $"sensitivity {factor}";
|
||||
}
|
||||
}
|
||||
}
|
||||
328
tests/AcDream.App.Tests/Input/GameplayInputActionRouterTests.cs
Normal file
328
tests/AcDream.App.Tests/Input/GameplayInputActionRouterTests.cs
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
||||
public sealed class GameplayInputActionRouterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("pointer", "pointer")]
|
||||
[InlineData("combat", "pointer,combat")]
|
||||
[InlineData("retained", "pointer,combat,retained")]
|
||||
[InlineData("selection", "pointer,combat,retained,selection")]
|
||||
[InlineData("movement", "pointer,combat,retained,selection,movement")]
|
||||
[InlineData("command", "pointer,combat,retained,selection,movement,command")]
|
||||
public void Press_PreservesFrozenPriorityAndStopsAtConsumer(
|
||||
string consumeAt,
|
||||
string expectedCsv)
|
||||
{
|
||||
var harness = new Harness(consumeAt);
|
||||
harness.Router.Attach();
|
||||
|
||||
harness.Actions.Raise(
|
||||
InputAction.AcdreamToggleDebugPanel,
|
||||
ActivationType.Press);
|
||||
|
||||
Assert.Equal(expectedCsv.Split(','), harness.Targets.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scroll_IsBetweenPointerAndCombat_AndOnlyPressMovesIt()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Router.Attach();
|
||||
|
||||
harness.Actions.Raise(InputAction.ScrollUp, ActivationType.Release);
|
||||
Assert.Equal(["pointer"], harness.Targets.Calls);
|
||||
|
||||
harness.Targets.Calls.Clear();
|
||||
harness.Actions.Raise(InputAction.ScrollDown, ActivationType.Press);
|
||||
Assert.Equal(["pointer", "scroll:ScrollDown"], harness.Targets.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Release_ReachesCombatThenStopsAtPressGate()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Router.Attach();
|
||||
|
||||
harness.Actions.Raise(
|
||||
InputAction.CombatHighAttack,
|
||||
ActivationType.Release);
|
||||
|
||||
Assert.Equal(["pointer", "combat"], harness.Targets.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoubleClick_PassesGateToRetainedSelectionMovementAndCommands()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Router.Attach();
|
||||
|
||||
harness.Actions.Raise(
|
||||
InputAction.SelectDblLeft,
|
||||
ActivationType.DoubleClick);
|
||||
|
||||
Assert.Equal(
|
||||
["pointer", "combat", "retained", "selection", "movement", "command"],
|
||||
harness.Targets.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attach_SeedsAndTracksCombatScopes()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Combat.CurrentModeValue = CombatMode.Missile;
|
||||
harness.Router.Attach();
|
||||
|
||||
harness.Combat.Raise(CombatMode.Magic);
|
||||
harness.Combat.Raise(CombatMode.NonCombat);
|
||||
|
||||
Assert.Equal(
|
||||
[InputScope.MissileCombat, InputScope.MagicCombat, null],
|
||||
harness.Actions.Scopes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "remove-actions")]
|
||||
[InlineData(1, "remove-combat,remove-actions")]
|
||||
public void AttachFailureAfterSideEffect_RollsBackExactPrefixInReverse(
|
||||
int failingEdge,
|
||||
string expectedRemovalCsv)
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var actions = new FakeActionSurface(calls)
|
||||
{
|
||||
ThrowAfterAdd = failingEdge == 0,
|
||||
};
|
||||
var combat = new FakeCombatSurface(calls)
|
||||
{
|
||||
ThrowAfterAdd = failingEdge == 1,
|
||||
};
|
||||
var router = Create(actions, combat, new FakeTargets());
|
||||
|
||||
Assert.Throws<InvalidOperationException>(router.Attach);
|
||||
|
||||
Assert.Equal(expectedRemovalCsv.Split(','), calls.Where(
|
||||
call => call.StartsWith("remove", StringComparison.Ordinal)));
|
||||
Assert.True(router.IsDisposalComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantDeliveryDuringAdd_CannotEnterPartialRouter()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var actions = new FakeActionSurface(calls)
|
||||
{
|
||||
RaiseDuringAdd = true,
|
||||
};
|
||||
var targets = new FakeTargets();
|
||||
var router = Create(actions, new FakeCombatSurface(calls), targets);
|
||||
|
||||
router.Attach();
|
||||
|
||||
Assert.Empty(targets.Calls);
|
||||
actions.Raise(InputAction.AcdreamToggleDebugPanel, ActivationType.Press);
|
||||
Assert.NotEmpty(targets.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttachAndRollbackFailure_RetainsCleanupForLaterDispose()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var actions = new FakeActionSurface(calls)
|
||||
{
|
||||
ThrowAfterAdd = true,
|
||||
RemoveFailures = int.MaxValue,
|
||||
};
|
||||
var router = Create(
|
||||
actions,
|
||||
new FakeCombatSurface(calls),
|
||||
new FakeTargets());
|
||||
|
||||
Assert.Throws<AggregateException>(router.Attach);
|
||||
Assert.False(router.IsDisposalComplete);
|
||||
|
||||
actions.RemoveFailures = 0;
|
||||
router.Dispose();
|
||||
|
||||
Assert.True(router.IsDisposalComplete);
|
||||
Assert.Null(actions.Callback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_RetriesOnlyFailedRemovalAndNeverReplaysSuccess()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var actions = new FakeActionSurface(calls);
|
||||
var combat = new FakeCombatSurface(calls)
|
||||
{
|
||||
RemoveFailures = int.MaxValue,
|
||||
};
|
||||
var router = Create(actions, combat, new FakeTargets());
|
||||
router.Attach();
|
||||
|
||||
Assert.Throws<AggregateException>(router.Dispose);
|
||||
Assert.False(router.IsDisposalComplete);
|
||||
|
||||
combat.RemoveFailures = 0;
|
||||
router.Dispose();
|
||||
|
||||
Assert.True(router.IsDisposalComplete);
|
||||
Assert.Equal(4, calls.Count(call => call == "remove-combat"));
|
||||
Assert.Equal(1, calls.Count(call => call == "remove-actions"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deactivate_MakesCopiedCallbacksSilentBeforePhysicalDetach()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Router.Attach();
|
||||
Action<InputAction, ActivationType> copiedAction =
|
||||
harness.Actions.Callback!;
|
||||
Action<CombatMode> copiedCombat = harness.Combat.Callback!;
|
||||
|
||||
harness.Router.Deactivate();
|
||||
copiedAction(InputAction.AcdreamToggleDebugPanel, ActivationType.Press);
|
||||
copiedCombat(CombatMode.Melee);
|
||||
|
||||
Assert.Empty(harness.Targets.Calls);
|
||||
Assert.Equal([null], harness.Actions.Scopes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeBeforeAttach_IsTerminalAndCannotResurrectCallbacks()
|
||||
{
|
||||
var harness = new Harness();
|
||||
|
||||
harness.Router.Dispose();
|
||||
|
||||
Assert.True(harness.Router.IsDisposalComplete);
|
||||
Assert.Throws<ObjectDisposedException>(harness.Router.Attach);
|
||||
Assert.Null(harness.Actions.Callback);
|
||||
Assert.Null(harness.Combat.Callback);
|
||||
}
|
||||
|
||||
private static GameplayInputActionRouter Create(
|
||||
FakeActionSurface actions,
|
||||
FakeCombatSurface combat,
|
||||
FakeTargets targets) =>
|
||||
new(actions, combat, targets, new HostQuiescenceGate());
|
||||
|
||||
private sealed class Harness
|
||||
{
|
||||
public Harness(string? consumeAt = null)
|
||||
{
|
||||
var calls = new List<string>();
|
||||
Actions = new FakeActionSurface(calls);
|
||||
Combat = new FakeCombatSurface(calls);
|
||||
Targets = new FakeTargets(consumeAt);
|
||||
Router = Create(Actions, Combat, Targets);
|
||||
}
|
||||
|
||||
public FakeActionSurface Actions { get; }
|
||||
public FakeCombatSurface Combat { get; }
|
||||
public FakeTargets Targets { get; }
|
||||
public GameplayInputActionRouter Router { get; }
|
||||
}
|
||||
|
||||
private sealed class FakeActionSurface(List<string> calls)
|
||||
: IGameplayInputActionSurface
|
||||
{
|
||||
public Action<InputAction, ActivationType>? Callback { get; private set; }
|
||||
public bool ThrowAfterAdd { get; set; }
|
||||
public bool RaiseDuringAdd { get; set; }
|
||||
public int RemoveFailures { get; set; }
|
||||
public List<InputScope?> Scopes { get; } = [];
|
||||
|
||||
public void AddFired(Action<InputAction, ActivationType> callback)
|
||||
{
|
||||
calls.Add("add-actions");
|
||||
Callback = callback;
|
||||
if (RaiseDuringAdd)
|
||||
{
|
||||
callback(
|
||||
InputAction.AcdreamToggleDebugPanel,
|
||||
ActivationType.Press);
|
||||
}
|
||||
if (ThrowAfterAdd)
|
||||
throw new InvalidOperationException("actions add");
|
||||
}
|
||||
|
||||
public void RemoveFired(Action<InputAction, ActivationType> callback)
|
||||
{
|
||||
calls.Add("remove-actions");
|
||||
if (RemoveFailures-- > 0)
|
||||
throw new InvalidOperationException("actions remove");
|
||||
if (ReferenceEquals(Callback, callback))
|
||||
Callback = null;
|
||||
}
|
||||
|
||||
public void SetCombatScope(InputScope? scope) => Scopes.Add(scope);
|
||||
|
||||
public void Raise(InputAction action, ActivationType activation) =>
|
||||
Callback?.Invoke(action, activation);
|
||||
}
|
||||
|
||||
private sealed class FakeCombatSurface(List<string> calls)
|
||||
: ICombatModeEventSurface
|
||||
{
|
||||
public CombatMode CurrentModeValue { get; set; } = CombatMode.NonCombat;
|
||||
public CombatMode CurrentMode => CurrentModeValue;
|
||||
public Action<CombatMode>? Callback { get; private set; }
|
||||
public bool ThrowAfterAdd { get; set; }
|
||||
public int RemoveFailures { get; set; }
|
||||
|
||||
public void AddChanged(Action<CombatMode> callback)
|
||||
{
|
||||
calls.Add("add-combat");
|
||||
Callback = callback;
|
||||
if (ThrowAfterAdd)
|
||||
throw new InvalidOperationException("combat add");
|
||||
}
|
||||
|
||||
public void RemoveChanged(Action<CombatMode> callback)
|
||||
{
|
||||
calls.Add("remove-combat");
|
||||
if (RemoveFailures-- > 0)
|
||||
throw new InvalidOperationException("combat remove");
|
||||
if (ReferenceEquals(Callback, callback))
|
||||
Callback = null;
|
||||
}
|
||||
|
||||
public void Raise(CombatMode mode) => Callback?.Invoke(mode);
|
||||
}
|
||||
|
||||
private sealed class FakeTargets(string? consumeAt = null)
|
||||
: IGameplayInputPriorityTargets
|
||||
{
|
||||
public List<string> Calls { get; } = [];
|
||||
|
||||
public bool HandlePointerAction(InputAction action, ActivationType activation) =>
|
||||
Record("pointer");
|
||||
|
||||
public void HandleScroll(InputAction action) => Calls.Add($"scroll:{action}");
|
||||
|
||||
public bool HandleCombatAction(InputAction action, ActivationType activation) =>
|
||||
Record("combat");
|
||||
|
||||
public bool HandleRetainedUiAction(InputAction action) =>
|
||||
Record("retained");
|
||||
|
||||
public bool HandleSelectionAction(InputAction action) =>
|
||||
Record("selection");
|
||||
|
||||
public bool HandlePressedMovementAction(InputAction action) =>
|
||||
Record("movement");
|
||||
|
||||
public void HandleCommand(InputAction action) => Calls.Add("command");
|
||||
|
||||
private bool Record(string name)
|
||||
{
|
||||
Calls.Add(name);
|
||||
return consumeAt == name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
||||
public sealed class GameplayInputCommandControllerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(InputAction.ToggleInventoryPanel, "inventory")]
|
||||
[InlineData(InputAction.AcdreamToggleDebugPanel, "debug")]
|
||||
[InlineData(InputAction.AcdreamToggleFlyMode, "fly-or-chase")]
|
||||
[InlineData(InputAction.AcdreamTogglePlayerMode, "player-mode")]
|
||||
[InlineData(InputAction.ToggleChatEntry, "chat")]
|
||||
[InlineData(InputAction.ToggleOptionsPanel, "settings")]
|
||||
[InlineData(InputAction.CombatToggleCombat, "combat")]
|
||||
public void RecognizedCommand_RoutesToTypedOwner(
|
||||
InputAction action,
|
||||
string expected)
|
||||
{
|
||||
var harness = new Harness();
|
||||
|
||||
bool handled = harness.Controller.Handle(action);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Equal([expected], harness.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiagnosticCommand_PrecedesRemainingCommandSwitch()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Diagnostics.HandledAction = InputAction.AcdreamToggleDebugPanel;
|
||||
|
||||
bool handled = harness.Controller.Handle(
|
||||
InputAction.AcdreamToggleDebugPanel);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Equal(["diagnostic"], harness.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownCommand_IsNotClaimed()
|
||||
{
|
||||
var harness = new Harness();
|
||||
|
||||
bool handled = harness.Controller.Handle(InputAction.MovementForward);
|
||||
|
||||
Assert.False(handled);
|
||||
Assert.Empty(harness.Calls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true, true, "cancel-target")]
|
||||
[InlineData(false, true, true, "exit-fly")]
|
||||
[InlineData(false, false, true, "exit-player")]
|
||||
[InlineData(false, false, false, "close")]
|
||||
public void Escape_PreservesTargetFlyPlayerWindowPriority(
|
||||
bool targetMode,
|
||||
bool flyMode,
|
||||
bool playerMode,
|
||||
string expected)
|
||||
{
|
||||
var harness = new Harness
|
||||
{
|
||||
TargetMode = { IsActive = targetMode },
|
||||
Camera = { IsFly = flyMode },
|
||||
Player = { IsPlayer = playerMode },
|
||||
};
|
||||
|
||||
bool handled = harness.Controller.Handle(InputAction.EscapeKey);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Equal([expected], harness.Calls);
|
||||
}
|
||||
|
||||
private sealed class Harness
|
||||
{
|
||||
public Harness()
|
||||
{
|
||||
Retained = new FakeRetained(Calls);
|
||||
DevTools = new FakeDevTools(Calls);
|
||||
Diagnostics = new FakeDiagnostics(Calls);
|
||||
Player = new FakePlayerMode(Calls);
|
||||
TargetMode = new FakeTargetMode(Calls);
|
||||
Camera = new FakeCamera(Calls);
|
||||
Combat = new FakeCombat(Calls);
|
||||
Window = new FakeWindow(Calls);
|
||||
Controller = new GameplayInputCommandController(
|
||||
Retained,
|
||||
DevTools,
|
||||
Diagnostics,
|
||||
Player,
|
||||
TargetMode,
|
||||
Camera,
|
||||
Combat,
|
||||
Window);
|
||||
}
|
||||
|
||||
public List<string> Calls { get; } = [];
|
||||
public FakeRetained Retained { get; }
|
||||
public FakeDevTools DevTools { get; }
|
||||
public FakeDiagnostics Diagnostics { get; }
|
||||
public FakePlayerMode Player { get; }
|
||||
public FakeTargetMode TargetMode { get; }
|
||||
public FakeCamera Camera { get; }
|
||||
public FakeCombat Combat { get; }
|
||||
public FakeWindow Window { get; }
|
||||
public GameplayInputCommandController Controller { get; }
|
||||
}
|
||||
|
||||
private sealed class FakeRetained(List<string> calls)
|
||||
: IRetainedGameplayWindowCommands
|
||||
{
|
||||
public void ToggleInventory() => calls.Add("inventory");
|
||||
}
|
||||
|
||||
private sealed class FakeDevTools(List<string> calls)
|
||||
: IDevToolsGameplayCommands
|
||||
{
|
||||
public void ToggleDebugPanel() => calls.Add("debug");
|
||||
public void FocusChatInput() => calls.Add("chat");
|
||||
public void ToggleSettingsPanel() => calls.Add("settings");
|
||||
}
|
||||
|
||||
private sealed class FakeDiagnostics(List<string> calls)
|
||||
: IRuntimeDiagnosticCommands
|
||||
{
|
||||
public InputAction? HandledAction { get; set; }
|
||||
|
||||
public bool Handle(InputAction action)
|
||||
{
|
||||
if (action != HandledAction)
|
||||
return false;
|
||||
calls.Add("diagnostic");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CycleTimeOfDay() => calls.Add("time");
|
||||
public void CycleWeather() => calls.Add("weather");
|
||||
public void ToggleCollisionWireframes() => calls.Add("collision");
|
||||
}
|
||||
|
||||
private sealed class FakePlayerMode(List<string> calls)
|
||||
: IPlayerModeGameplayCommands
|
||||
{
|
||||
public bool IsPlayer { get; set; }
|
||||
public bool IsPlayerMode => IsPlayer;
|
||||
public void ToggleFlyOrChase() => calls.Add("fly-or-chase");
|
||||
public void TogglePlayerMode() => calls.Add("player-mode");
|
||||
public void ExitPlayerMode() => calls.Add("exit-player");
|
||||
}
|
||||
|
||||
private sealed class FakeTargetMode(List<string> calls)
|
||||
: IItemTargetModeCommands
|
||||
{
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsAnyTargetModeActive => IsActive;
|
||||
public void CancelTargetMode() => calls.Add("cancel-target");
|
||||
}
|
||||
|
||||
private sealed class FakeCamera(List<string> calls)
|
||||
: IGameplayCameraModeCommands
|
||||
{
|
||||
public bool IsFly { get; set; }
|
||||
public bool IsFlyMode => IsFly;
|
||||
public void ExitFlyMode() => calls.Add("exit-fly");
|
||||
}
|
||||
|
||||
private sealed class FakeCombat(List<string> calls) : ILiveCombatModeCommand
|
||||
{
|
||||
public void Toggle() => calls.Add("combat");
|
||||
}
|
||||
|
||||
private sealed class FakeWindow(List<string> calls) : IGameplayWindowCommands
|
||||
{
|
||||
public void Close() => calls.Add("close");
|
||||
}
|
||||
}
|
||||
158
tests/AcDream.App.Tests/Input/RetainedUiGameplayBindingTests.cs
Normal file
158
tests/AcDream.App.Tests/Input/RetainedUiGameplayBindingTests.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
||||
public sealed class RetainedUiGameplayBindingTests
|
||||
{
|
||||
[Fact]
|
||||
public void AttachedBinding_RoutesOnlyItemPayloads()
|
||||
{
|
||||
var surface = new Surface();
|
||||
var calls = new List<(uint Id, int X, int Y)>();
|
||||
var binding = Create(surface, calls);
|
||||
binding.Attach();
|
||||
|
||||
surface.Raise("not an item", 1, 2);
|
||||
surface.Raise(Item(42), 7, 9);
|
||||
|
||||
Assert.Equal([(42u, 7, 9)], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAfterSideEffectFailure_RollsBackExactCallback()
|
||||
{
|
||||
var surface = new Surface { ThrowAfterAdd = true };
|
||||
var binding = Create(surface, []);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(binding.Attach);
|
||||
|
||||
Assert.True(binding.IsDisposalComplete);
|
||||
Assert.Null(surface.Callback);
|
||||
Assert.Equal(1, surface.RemoveCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantDeliveryDuringAdd_CannotEnterPartialBinding()
|
||||
{
|
||||
var surface = new Surface { RaiseDuringAdd = true };
|
||||
var calls = new List<(uint Id, int X, int Y)>();
|
||||
var binding = Create(surface, calls);
|
||||
|
||||
binding.Attach();
|
||||
Assert.Empty(calls);
|
||||
surface.Raise(Item(2), 3, 4);
|
||||
Assert.Equal([(2u, 3, 4)], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttachAndRollbackFailure_RetainsCleanupForLaterDispose()
|
||||
{
|
||||
var surface = new Surface
|
||||
{
|
||||
ThrowAfterAdd = true,
|
||||
RemoveFailures = int.MaxValue,
|
||||
};
|
||||
var binding = Create(surface, []);
|
||||
|
||||
Assert.Throws<AggregateException>(binding.Attach);
|
||||
Assert.False(binding.IsDisposalComplete);
|
||||
|
||||
surface.RemoveFailures = 0;
|
||||
binding.Dispose();
|
||||
|
||||
Assert.True(binding.IsDisposalComplete);
|
||||
Assert.Null(surface.Callback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deactivate_MakesCopiedCallbackSilentBeforePhysicalDetach()
|
||||
{
|
||||
var surface = new Surface();
|
||||
var calls = new List<(uint Id, int X, int Y)>();
|
||||
var binding = Create(surface, calls);
|
||||
binding.Attach();
|
||||
Action<object, int, int> copied = surface.Callback!;
|
||||
|
||||
binding.Deactivate();
|
||||
copied(Item(8), 1, 1);
|
||||
|
||||
Assert.Empty(calls);
|
||||
Assert.False(binding.IsDisposalComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_RetriesFailedRemovalWithoutReattaching()
|
||||
{
|
||||
var surface = new Surface { RemoveFailures = int.MaxValue };
|
||||
var binding = Create(surface, []);
|
||||
binding.Attach();
|
||||
|
||||
Assert.Throws<AggregateException>(binding.Dispose);
|
||||
Assert.False(binding.IsDisposalComplete);
|
||||
|
||||
surface.RemoveFailures = 0;
|
||||
binding.Dispose();
|
||||
|
||||
Assert.True(binding.IsDisposalComplete);
|
||||
Assert.Equal(1, surface.AddCalls);
|
||||
Assert.Equal(3, surface.RemoveCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeBeforeAttach_IsTerminal()
|
||||
{
|
||||
var surface = new Surface();
|
||||
var binding = Create(surface, []);
|
||||
|
||||
binding.Dispose();
|
||||
|
||||
Assert.True(binding.IsDisposalComplete);
|
||||
Assert.Throws<ObjectDisposedException>(binding.Attach);
|
||||
Assert.Null(surface.Callback);
|
||||
}
|
||||
|
||||
private static RetainedUiGameplayBinding Create(
|
||||
Surface surface,
|
||||
List<(uint Id, int X, int Y)> calls) =>
|
||||
new(
|
||||
surface,
|
||||
(item, x, y) => calls.Add((item.ObjId, x, y)),
|
||||
new HostQuiescenceGate());
|
||||
|
||||
private static ItemDragPayload Item(uint id) =>
|
||||
new(id, ItemDragSource.Inventory, 0, SourceCell: null!);
|
||||
|
||||
private sealed class Surface : IRetainedUiDragReleaseSurface
|
||||
{
|
||||
public Action<object, int, int>? Callback { get; private set; }
|
||||
public bool ThrowAfterAdd { get; set; }
|
||||
public bool RaiseDuringAdd { get; set; }
|
||||
public int RemoveFailures { get; set; }
|
||||
public int AddCalls { get; private set; }
|
||||
public int RemoveCalls { get; private set; }
|
||||
|
||||
public void AddReleasedOutside(Action<object, int, int> callback)
|
||||
{
|
||||
AddCalls++;
|
||||
Callback = callback;
|
||||
if (RaiseDuringAdd)
|
||||
callback(Item(1), 1, 1);
|
||||
if (ThrowAfterAdd)
|
||||
throw new InvalidOperationException("add");
|
||||
}
|
||||
|
||||
public void RemoveReleasedOutside(Action<object, int, int> callback)
|
||||
{
|
||||
RemoveCalls++;
|
||||
if (RemoveFailures-- > 0)
|
||||
throw new InvalidOperationException("remove");
|
||||
if (ReferenceEquals(Callback, callback))
|
||||
Callback = null;
|
||||
}
|
||||
|
||||
public void Raise(object payload, int x, int y) =>
|
||||
Callback?.Invoke(payload, x, y);
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,6 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"_inputDispatcher.Attach();",
|
||||
"_movementInput.Bind(_inputDispatcher);",
|
||||
"_cameraInput.Bind(_inputDispatcher);",
|
||||
"_inputDispatcher.Fired += OnInputAction;",
|
||||
"_cameraController = new CameraController(orbit, fly);",
|
||||
"_cameraPointerInput = AcDream.App.Input.CameraPointerInputController.Create(",
|
||||
"_cameraPointerInput.AttachRaw();",
|
||||
|
|
@ -73,9 +72,17 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"_uiHost.WireKeyboard(kb)",
|
||||
"_retailUiRuntime = AcDream.App.UI.RetailUiRuntime.Mount(",
|
||||
"_liveEntities = new AcDream.App.World.LiveEntityRuntime(",
|
||||
"_selectionInteractions ??= new AcDream.App.Interaction.SelectionInteractionController(",
|
||||
"_retainedUiGameplayBinding =",
|
||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create(",
|
||||
"_retainedUiGameplayBinding.Attach();",
|
||||
"_renderFrameOrchestrator =",
|
||||
"_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
||||
"_liveSessionHost = CreateLiveSessionHost();",
|
||||
"_liveCombatModeCommands.Bind(combatCommand);",
|
||||
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);",
|
||||
"_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(",
|
||||
"_gameplayInputActions.Attach();",
|
||||
"_liveSessionHost.Start(_options)");
|
||||
Assert.Equal(1, CountOccurrences(body, "_liveSessionHost.Start(_options)"));
|
||||
Assert.Contains("if (firstKb is not null)", body, StringComparison.Ordinal);
|
||||
|
|
@ -128,33 +135,53 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("private void OnEnvironChanged(", source, StringComparison.Ordinal);
|
||||
|
||||
string cycleTime = MethodBody(
|
||||
"private void CycleTimeOfDay()",
|
||||
"private void CycleWeather()");
|
||||
Assert.DoesNotContain("private void CycleTimeOfDay()", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("private void CycleWeather()", source, StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
cycleTime,
|
||||
"_worldEnvironment.CycleTimeOfDay();",
|
||||
"_debugVm?.AddToast(message);");
|
||||
load,
|
||||
"_debugVm.CycleTimeOfDay =",
|
||||
"_runtimeDiagnosticCommands.CycleTimeOfDay;",
|
||||
"new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(",
|
||||
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InputAction_PreservesRetailAcceptedPriority()
|
||||
public void InputAction_IsOneTypedOwnerHandoff()
|
||||
{
|
||||
string body = MethodBody(
|
||||
"private void OnInputAction(",
|
||||
"private void ToggleLiveCombatMode()");
|
||||
string source = GameWindowSource();
|
||||
string load = MethodBody(
|
||||
"private void OnLoad()",
|
||||
"private AcDream.App.Net.LiveSessionHost");
|
||||
string shutdown = MethodBody(
|
||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||
"private void OnFocusChanged(bool focused)");
|
||||
|
||||
Assert.DoesNotContain("private void OnInputAction(", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("private void SetInputCombatScope(", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("private void ToggleLiveCombatMode()", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal);
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
load,
|
||||
"AcDream.App.Input.GameplayInputActionRouter.Create("));
|
||||
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();"));
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
load,
|
||||
"AcDream.App.Input.RetainedUiGameplayBinding.Create("));
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
load,
|
||||
"_retainedUiGameplayBinding.Attach();"));
|
||||
AssertAppearsInOrder(
|
||||
body,
|
||||
"HandlePointerAction(action, activation)",
|
||||
"action == AcDream.UI.Abstractions.Input.InputAction.ScrollUp",
|
||||
"HandleScrollAction(action);",
|
||||
"HandleCombatAction(action, activation)",
|
||||
"activation != AcDream.UI.Abstractions.Input.ActivationType.Press",
|
||||
"_retailUiRuntime?.HandleInputAction(action)",
|
||||
"_selectionInteractions?.HandleInputAction(action)",
|
||||
"HandlePressedMovementAction(action)",
|
||||
"switch (action)");
|
||||
shutdown,
|
||||
"_liveCombatModeCommands.Deactivate",
|
||||
"_runtimeDiagnosticCommands.Deactivate",
|
||||
"_retainedUiGameplayBinding?.Deactivate()",
|
||||
"_gameplayInputActions?.Deactivate()",
|
||||
"new ResourceShutdownStage(\"session lifetime\"",
|
||||
"binding.Dispose();",
|
||||
"_retainedUiGameplayBinding = null;",
|
||||
"actions.Dispose();",
|
||||
"_gameplayInputActions = null;");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -265,10 +292,16 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"_windowCallbacks = null;");
|
||||
AssertAppearsInOrder(
|
||||
manifest,
|
||||
"new(\"combat command slot\", _liveCombatModeCommands.Deactivate)",
|
||||
"new(\"diagnostic command slot\", _runtimeDiagnosticCommands.Deactivate)",
|
||||
"new(\"retained gameplay\", () => _retainedUiGameplayBinding?.Deactivate())",
|
||||
"new(\"gameplay actions\", () => _gameplayInputActions?.Deactivate())",
|
||||
"new(\"camera pointer\", () => _cameraPointerInput?.Deactivate())",
|
||||
"new ResourceShutdownStage(\"session lifetime\"",
|
||||
"controller.Dispose();",
|
||||
"new ResourceShutdownStage(\"input callback detach\"",
|
||||
"binding.Dispose();",
|
||||
"actions.Dispose();",
|
||||
"pointer.Dispose();",
|
||||
"new ResourceShutdownStage(\"session dependents\"",
|
||||
"pointer.ReleaseMouseLookAfterSessionRetirement();",
|
||||
|
|
|
|||
|
|
@ -44,9 +44,11 @@ public sealed class CombatInputPlannerTests
|
|||
{
|
||||
var held = Equipped(EquipMask.Held, ItemType.Misc, combatUse: 0);
|
||||
|
||||
Assert.Equal(
|
||||
CombatMode.NonCombat,
|
||||
CombatInputPlanner.GetDefaultCombatMode([held]));
|
||||
DefaultCombatModeDecision decision =
|
||||
CombatInputPlanner.GetDefaultCombatModeDecision([held]);
|
||||
|
||||
Assert.Equal(CombatMode.NonCombat, decision.Mode);
|
||||
Assert.Same(held, decision.IncompatibleHeldItem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -64,6 +66,9 @@ public sealed class CombatInputPlannerTests
|
|||
Assert.Equal(
|
||||
CombatMode.Missile,
|
||||
CombatInputPlanner.ToggleMode(CombatMode.NonCombat, CombatMode.Missile));
|
||||
Assert.Equal(
|
||||
CombatMode.NonCombat,
|
||||
CombatInputPlanner.ToggleMode(CombatMode.NonCombat, CombatMode.NonCombat));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -71,6 +76,7 @@ public sealed class CombatInputPlannerTests
|
|||
{
|
||||
Assert.Equal(CombatMode.NonCombat, CombatInputPlanner.ToggleMode(CombatMode.Melee));
|
||||
Assert.Equal(CombatMode.NonCombat, CombatInputPlanner.ToggleMode(CombatMode.Magic));
|
||||
Assert.Equal(CombatMode.NonCombat, CombatInputPlanner.ToggleMode(CombatMode.Undef));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue