Mounts retail's Options panel (LayoutDesc 0x2100002B resolved through host 0x2100006E slot 0x1000018D, gmPanelUI key 10) via the same catalog-import pattern CharacterController already validates, registered through RetailPanelUiController so it shares retail's "one active gmPanelUI child" mutual exclusion with every other sibling panel for free. F11 and the toolbar's options button (0x1000019B, already authoring panel id 10) both now open it; the close button fires the same ToggleOptionsPanel action. OptionPageModel (OptionPage/BoolOptionRow) ports retail's exact Apply/Reset/Defaults/visibility semantics from UIOption_Checkbox/PlayerOptionPage — LED clicks apply live immediately, Apply commits every row unconditionally + flushes the batched blob, Reset reverts only Changed rows, Defaults restores without committing, and tab-switch/window-hide revert uncommitted edits. Wired for all four tabs; this slice registers real rows on none of them (Gameplay authentically has none — a pure button list). UiTabPanel gains an ActivePageChanged event so the page model can hook every tab transition, including the initial default-tab activation. The seven Gameplay-tab buttons: Exit Game reuses the existing graceful window-close path; Exit to Character Selection gets retail's confirmation dialog and byte-verified mid-air refusal but still behaves as Exit Game (AD-74 — no pre-world character-select flow exists); Configure Keyboard and In-Game Help Files are inert this slice (AD-76 for Help — the plugin retail depends on doesn't exist); Urgent Assistance/Report Abuse short-circuit to their own byte-verified failure text through the interface-text seam instead of ShellExecute against a dead URL (AD-75); Use Mouse Turning Settings runs the pure MouseTurningSettingsMacro port, persisting five new CameraTurningSettings preferences and sending PlayerOption.UseMouseTurning — TS-74 records that acdream has no persistent mouse-turning camera mode for the bit to drive yet. Full Release suite: 12,918 passed / 4 skipped / 0 failed (baseline 12,871/4/0 — only new tests added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
235 lines
8.4 KiB
C#
235 lines
8.4 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.AcdreamToggleDebugPanel, "debug")]
|
|
[InlineData(InputAction.AcdreamToggleFlyMode, "fly-or-chase")]
|
|
[InlineData(InputAction.AcdreamTogglePlayerMode, "player-mode")]
|
|
[InlineData(InputAction.ToggleChatEntry, "chat")]
|
|
// Campaign OP slice OP3 (D1): F11 now opens the retail Options panel
|
|
// through IRetainedGameplayWindowCommands, not the retired
|
|
// IDevToolsGameplayCommands.ToggleSettingsPanel() no-op.
|
|
[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);
|
|
}
|
|
|
|
[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);
|
|
Runtime = new FakeRuntimeView();
|
|
Window = new FakeWindow(Calls);
|
|
Controller = new GameplayInputCommandController(
|
|
Retained,
|
|
DevTools,
|
|
Diagnostics,
|
|
Player,
|
|
TargetMode,
|
|
Camera,
|
|
Runtime,
|
|
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 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 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) : 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 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");
|
|
}
|
|
}
|