acdream/tests/AcDream.App.Tests/Input/GameplayInputCommandControllerTests.cs
Erik 371197a345 refactor(settings): OP9 — retire the dead F11 settings surface + fully-superseded GameplaySettings
Campaign OP slice OP9 code retirement (docs/plans/2026-08-10-options-panel-campaign.md
§OP9). The retail four-tab Options panel (OP1-OP8) is now acdream's one
in-client settings surface (D1) — this commit deletes the pre-retail-UI
surfaces it fully superseded. Pure retirement: no behavior change to
anything live, verified by dispositioning every one of the 15 src files
and 8 test files that referenced the deleted types before touching any
of them.

1. SettingsVM + SettingsPanel (the old F11 IPanel surface, unrendered
   since Campaign V slice V11's ImGui deletion) deleted outright, plus
   their two dedicated test files. IPanel/IPanelRenderer stay — ChatPanel/
   DebugPanel/VitalsPanel still implement IPanel, so the contract does NOT
   become unreferenced.

2. RuntimeSettingsController's SettingsVM binding seam deleted:
   CreateViewModel, CreateViewModelBinding, RuntimeSettingsViewModelBinding,
   the _viewModel field, UnbindViewModel, and every _viewModel? notification
   call (ToggleFrameRate, LoadCharacterContext, RestoreDefaultCharacterContext,
   SyncChatFromServerOptions). CreateViewModelBinding had zero production
   callers (test-only). HasDraftPreview/DisplayPreview/AudioPreview stay on
   IRuntimeSettingsPreviewSource (WorldRenderFrameBuilder and
   SettingsParticleRangeSource still consume it) but now trivially mirror
   the committed Display/Audio snapshot — HasDraftPreview was already
   always false in production. GameWindowLifetime's matching
   "settings view model" shutdown stage is deleted.

3. IDevToolsGameplayCommands + DevToolsGameplayCommands deleted from
   GameplayInputCommandController.cs. All three members were dead:
   ToggleSettingsPanel() had ZERO dispatch sites (ToggleOptionsPanel always
   routed to _retained, never to _devTools); ToggleDebugPanel()/
   FocusChatInput() had dispatch sites (F1/Ctrl+F1, Tab) but empty no-op
   bodies (their ImGui DebugPanel/ChatPanel targets were already gone).
   The two live dispatch sites are kept as inert `return true;` cases
   (still consuming the key, matching the prior no-op's "handled"
   contract) instead of falling through to a lower-priority scope.
   SessionPlayerComposition.cs's `new DevToolsGameplayCommands()`
   construction is removed. No `ToggleSettingsPanel` InputAction exists
   (only `ToggleOptionsPanel`, rebound at OP3) — nothing to remove there
   or from KeyBindings.RetailDefaults()/keymap fixtures.

4. GameplaySettings deleted entirely (the type, SettingsStore's
   LoadGameplay/SaveGameplay/BuildGameplayObject, RuntimeSettingsController's
   Gameplay property/SetAcceptLootPermits). Verified all 13 remaining
   members (ToggleRun, AdvancedCombatUI, ShowTooltips,
   VividTargetingIndicator, SideBySideVitals, CoordinatesOnRadar,
   SpellDuration, AllowGive, ShowHelm, ShowCloak, LockUI, UseMouseTurning,
   AcceptLootPermits — the three combat ones already died at OP4/AP-196)
   were ALREADY bound through CharacterOptionTable/
   CharacterOptionsPageController's server-bit seam at OP4 before deleting
   the client-local mirror — no (c)-case genuinely-client-local member
   was found; disposition (b) covers 100% of the surface. SetUiLocked
   rewritten to compare only against the last value actually pushed to
   _runtimeTargets (MUST-FIX 4's guard), with no second store left to
   read or write. LiveSessionRuntimeFactory's SetAcceptLootPermits binding
   now sends the wire option only (the GameplaySettings write-behind call
   removed as dead output). CharacterSettings/DisplaySettings/
   AudioSettings/ChatSettings and their SettingsStore Load/Save surfaces
   are UNTOUCHED per the campaign contract.

   Per-file disposition (15 src + 8 test files that referenced
   GameplaySettings before this commit):
   - GameplaySettings.cs, SettingsVM.cs, SettingsPanel.cs: the types
     themselves — deleted.
   - SettingsStore.cs, RuntimeSettingsController.cs,
     LiveSessionRuntimeFactory.cs: real usage — API deleted/rewritten.
   - RetailUiRuntime.cs, InteractionRetainedUiComposition.cs,
     SessionPlayerComposition.cs, CombatUiController.cs,
     LiveCombatAttackOperations.cs, LivePresentationComposition.cs,
     FrameRootComposition.cs, RuntimeCharacterState.cs,
     CombatCameraTargetSource.cs: doc-comment-only or interface-name
     substring matches (ICombatGameplaySettingsSource) — left as accurate
     historical record, no forward reference to the deleted type.
   - Tests: RuntimeSettingsControllerTests.cs and SettingsStoreTests.cs
     rewritten (Gameplay-specific tests deleted; SaveDisplay/SaveAudio/
     SaveChat tests re-targeted off the now-public methods instead of the
     retired SettingsVM draft/Save() indirection); GameplaySettingsTests.cs/
     SettingsVMTests.cs/SettingsPanelTests.cs deleted; the remaining three
     (CharacterOptionCombatSettingsSourceTests.cs,
     CombatCameraTargetSourceTests.cs, LiveCombatAttackOperationsTests.cs)
     were comment/interface-name-only, untouched.

5. Register: AP-196 (OP4's partial GameplaySettings retirement, which left
   five fields as write-behind mirrors) is fully retired now that the
   record is gone outright — marked ~~AP-196~~ RETIRED with its retirement
   note, active-row count 143 -> 142. No other row cited the deleted types
   directly (AP-194/AP-193 cite CharacterOptionTable.cs, not
   GameplaySettings.cs).

6. Settings.json migration honesty: SettingsStore no longer reads or
   writes the "gameplay" top-level key, so an existing file carrying one
   from a pre-OP9 build is neither parsed nor dropped — the existing
   SaveSection raw-JSON-text preservation mechanism (unknown top-level
   keys survive every subsequent save) carries it forward untouched.
   Two new targeted tests
   (LeftoverGameplaySection_FromAnOlderSettingsJson_DoesNotBreakOtherLoads,
   LeftoverGameplaySection_SurvivesAnUnrelatedSave) pin this.

InputAction.ToggleOptionsPanel's stale doc comment (still describing the
retired ImGui SettingsPanel) and a handful of other dangling doc
references (DisplaySettings.cs, ChatOpacityLink.cs,
SettingsDevToolsComposition.cs, InputDispatcherCaptureTests.cs) are
reworded to point at the current retail Options panel / OP8
KeyboardConfigController.

Build: dotnet build -c Release green, 0 errors. Tests: dotnet test -c
Release --no-build — 13,075 passed / 4 skipped / 0 failed (13,079 total),
down from the stated baseline of 13,155 passed / 4 skipped / 0 failed
(13,159 total) — the -80 delta is exactly the deleted SettingsVM/
SettingsPanel/GameplaySettings test surface (three whole files plus the
Gameplay-specific cases trimmed from RuntimeSettingsControllerTests.cs/
SettingsStoreTests.cs), with zero regressions elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:18:53 +02:00

243 lines
8.7 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 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");
}
}