acdream/tests/AcDream.App.Tests/Input/GameplayInputCommandControllerTests.cs
Erik cced83b483 feat(app): FA2 -- fellowship/allegiance command routing for graphical + headless hosts
Both LiveSocialSessionBindings construction sites updated together
(LiveSessionRuntimeFactory.cs, HeadlessSessionHost.cs) so the single
GameEventWiring.WireAll registration site serves both hosts identically
(the K-slice unification). CurrentGameRuntimeCommandAdapter implements
IRuntimeFellowshipCommands/IRuntimeAllegianceCommands over the App
command bus (LiveSessionCommandRouter gains 12 new *RuntimeCmd records
+ registrations + LiveSessionCommandBindings send delegates), mirroring
DirectGameRuntimeCommandAdapter's direct-session shape including the
identical Quit leader-hand-off rule read from RuntimeFellowshipState.
CurrentGameRuntimeAdapter (the graphical IGameRuntimeView/
IGameRuntimeCommands composite) exposes the two new views/command
groups. Headless's DirectGameRuntimeCommandAdapter needed no changes --
it already implements both new interfaces from the Runtime-layer
commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 01:32:18 +02:00

245 lines
8.9 KiB
C#

using AcDream.App.Combat;
using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
using AcDream.UI.Abstractions.Input;
namespace AcDream.App.Tests.Input;
public sealed class GameplayInputCommandControllerTests
{
[Theory]
[InlineData(InputAction.ToggleInventoryPanel, "inventory")]
[InlineData(InputAction.AcdreamToggleFlyMode, "fly-or-chase")]
[InlineData(InputAction.AcdreamTogglePlayerMode, "player-mode")]
// Campaign OP slice OP3 (D1): F11 now opens the retail Options panel
// through IRetainedGameplayWindowCommands, not the OP9-retired
// IDevToolsGameplayCommands.ToggleSettingsPanel() no-op (which was
// never dispatched from this switch in the first place).
[InlineData(InputAction.ToggleOptionsPanel, "options")]
[InlineData(InputAction.CombatToggleCombat, "combat")]
[InlineData(InputAction.ToggleFloatingChatWindow1, "chat-window-1")]
[InlineData(InputAction.ToggleFloatingChatWindow2, "chat-window-2")]
[InlineData(InputAction.ToggleFloatingChatWindow3, "chat-window-3")]
[InlineData(InputAction.ToggleFloatingChatWindow4, "chat-window-4")]
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);
}
// OP9: AcdreamToggleDebugPanel/ToggleChatEntry retired the
// IDevToolsGameplayCommands seam they used to forward to — both
// targets (the ImGui-era DebugPanel/ChatPanel) were already gone
// (Campaign V slice V11), so the seam's own body was an unconditional
// no-op. The action is still consumed (handled == true, matching the
// prior no-op's contract) but claims no typed-owner call.
[Theory]
[InlineData(InputAction.AcdreamToggleDebugPanel)]
[InlineData(InputAction.ToggleChatEntry)]
public void RetiredDevToolsCommand_IsConsumedWithoutClaimingATypedOwner(
InputAction action)
{
var harness = new Harness();
bool handled = harness.Controller.Handle(action);
Assert.True(handled);
Assert.Empty(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);
Diagnostics = new FakeDiagnostics(Calls);
Player = new FakePlayerMode(Calls);
TargetMode = new FakeTargetMode(Calls);
Camera = new FakeCamera(Calls);
Combat = new FakeCombat(Calls);
Runtime = new FakeRuntimeView();
Window = new FakeWindow(Calls);
Controller = new GameplayInputCommandController(
Retained,
Diagnostics,
Player,
TargetMode,
Camera,
Runtime,
Combat,
Window);
}
public List<string> Calls { get; } = [];
public FakeRetained Retained { get; }
public FakeDiagnostics Diagnostics { get; }
public FakePlayerMode Player { get; }
public FakeTargetMode TargetMode { get; }
public FakeCamera Camera { get; }
public FakeCombat Combat { get; }
public FakeRuntimeView Runtime { get; }
public FakeWindow Window { get; }
public GameplayInputCommandController Controller { get; }
}
private sealed class FakeRetained(List<string> calls)
: IRetainedGameplayWindowCommands
{
public void ToggleInventory() => calls.Add("inventory");
public void ToggleFloatingChatWindow(int windowId) =>
calls.Add($"chat-window-{windowId}");
public void ToggleOptionsPanel() => calls.Add("options");
}
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) : IRuntimeCombatCommands
{
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeCombatCommand command)
{
Assert.Equal(RuntimeCombatCommand.ToggleMode, command);
calls.Add("combat");
return new RuntimeCommandResult(
RuntimeCommandStatus.Accepted,
expectedGeneration);
}
public RuntimeCommandResult ExecuteAttack(
RuntimeGenerationToken expectedGeneration,
in RuntimeCombatAttackInput command) =>
new(
RuntimeCommandStatus.Unsupported,
expectedGeneration);
}
private sealed class FakeRuntimeView : IGameRuntimeView
{
public RuntimeGenerationToken Generation => new(7);
public RuntimeLifecycleSnapshot Lifecycle => throw new NotSupportedException();
public IGameRuntimeClock Clock => throw new NotSupportedException();
public IRuntimeEntityView Entities => throw new NotSupportedException();
public IRuntimeInventoryView Inventory => throw new NotSupportedException();
public IRuntimeInventoryStateView InventoryState =>
throw new NotSupportedException();
public IRuntimeCharacterView Character =>
throw new NotSupportedException();
public IRuntimeSocialView Social => throw new NotSupportedException();
public IRuntimeChatView Chat => throw new NotSupportedException();
public IRuntimeFellowshipView Fellowship => throw new NotSupportedException();
public IRuntimeAllegianceView Allegiance => throw new NotSupportedException();
public IRuntimeActionView Actions => throw new NotSupportedException();
public IRuntimeMovementView Movement => throw new NotSupportedException();
public AcDream.Runtime.World.IRuntimeWorldEnvironmentView Environment =>
throw new NotSupportedException();
public IRuntimePortalView Portal => throw new NotSupportedException();
public RuntimeStateCheckpoint CaptureCheckpoint() =>
throw new NotSupportedException();
}
private sealed class FakeWindow(List<string> calls) : IGameplayWindowCommands
{
public void Close() => calls.Add("close");
}
}