Five changes:
1. PlayerModeAutoEntry — testable guard class that fires once after
EnterWorld + WorldSession.State.InWorld + player entity present +
PlayerController.State == InWorld. GameWindow arms the entry
after EnterWorld; per-frame Tick checks all four guards and
invokes the same fly-to-player transition the Tab handler runs.
User-initiated fly toggle (DebugPanel button) Cancel()s pending
entry. Skip in offline mode (no ACDREAM_LIVE) — Holtburg orbit
stays default for testing.
2. MouseLookState + KeyBindings.RetailDefaults() binds MMB Hold to
InputAction.CameraInstantMouseLook. GameWindow subscribes:
- Press: hide cursor, capture position, _mouseLookActive = true.
- Release: restore cursor, deactivate.
- WantCaptureMouse=true while held → suspend (release cursor).
- MouseMove while active: combined drive — chase camera yaw +
character heading move together (retail's signature mouse-look
behavior). Camera Y still pitches camera-only.
3. DebugPanel "Toggle Free-Fly Mode" button via DebugVM.ToggleFlyMode
action delegate — replaces the F-key as the primary discovery
path for free-fly. Gated on DevToolsEnabled.
4. ChatPanel.FocusInput() one-shot + IPanelRenderer.SetKeyboardFocusHere
primitive. GameWindow's ToggleChatEntry (Tab) subscriber calls
_chatPanel.FocusInput() so Tab moves focus to the chat input
field. Replaces the K.1c TODO stub.
5. WantCaptureMouse gating reinforcement on surviving mouse handlers
(no new code; verified intact from K.1b).
21 new tests (8 PlayerModeAutoEntry, 10 MouseLookState, 3 ChatPanel
focus). 1183 total green. 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
4.9 KiB
C#
155 lines
4.9 KiB
C#
using AcDream.App.Input;
|
|
|
|
namespace AcDream.Core.Tests.Input;
|
|
|
|
/// <summary>
|
|
/// Phase K.2 — guard logic for auto-entering player mode at login.
|
|
/// The trigger fires exactly once when:
|
|
/// <list type="number">
|
|
/// <item>It has been <see cref="PlayerModeAutoEntry.Arm"/>'d (login
|
|
/// succeeded).</item>
|
|
/// <item>The live session is in <c>InWorld</c>.</item>
|
|
/// <item>The player entity has been streamed into the local
|
|
/// dictionary.</item>
|
|
/// <item>The player movement controller is ready to attach.</item>
|
|
/// </list>
|
|
/// All four predicates are passed as <see cref="Func{bool}"/>; tests
|
|
/// flip plain boolean fields and assert against an entry-counter that
|
|
/// the entry callback bumps.
|
|
/// </summary>
|
|
public sealed class AutoEnterPlayerModeTests
|
|
{
|
|
private sealed class State
|
|
{
|
|
public bool LiveInWorld;
|
|
public bool PlayerEntityPresent;
|
|
public bool PlayerControllerReady;
|
|
public int EnteredCount;
|
|
|
|
public PlayerModeAutoEntry Build() =>
|
|
new(
|
|
isLiveInWorld: () => LiveInWorld,
|
|
isPlayerEntityPresent: () => PlayerEntityPresent,
|
|
isPlayerControllerReady: () => PlayerControllerReady,
|
|
enterPlayerMode: () => EnteredCount++);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryEnter_NotArmed_DoesNotFire()
|
|
{
|
|
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = true };
|
|
var guard = s.Build();
|
|
|
|
// Not armed → must NOT fire even though every precondition is true.
|
|
Assert.False(guard.TryEnter());
|
|
Assert.Equal(0, s.EnteredCount);
|
|
Assert.False(guard.IsArmed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryEnter_Armed_LiveNotInWorld_DoesNotFire()
|
|
{
|
|
var s = new State { LiveInWorld = false, PlayerEntityPresent = true, PlayerControllerReady = true };
|
|
var guard = s.Build();
|
|
guard.Arm();
|
|
|
|
Assert.False(guard.TryEnter());
|
|
Assert.Equal(0, s.EnteredCount);
|
|
Assert.True(guard.IsArmed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryEnter_Armed_PlayerEntityNotPresent_DoesNotFire()
|
|
{
|
|
var s = new State { LiveInWorld = true, PlayerEntityPresent = false, PlayerControllerReady = true };
|
|
var guard = s.Build();
|
|
guard.Arm();
|
|
|
|
Assert.False(guard.TryEnter());
|
|
Assert.Equal(0, s.EnteredCount);
|
|
Assert.True(guard.IsArmed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryEnter_Armed_PlayerControllerNotReady_DoesNotFire()
|
|
{
|
|
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = false };
|
|
var guard = s.Build();
|
|
guard.Arm();
|
|
|
|
Assert.False(guard.TryEnter());
|
|
Assert.Equal(0, s.EnteredCount);
|
|
Assert.True(guard.IsArmed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryEnter_AllConditionsSatisfied_FiresExactlyOnce()
|
|
{
|
|
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = true };
|
|
var guard = s.Build();
|
|
guard.Arm();
|
|
|
|
Assert.True(guard.TryEnter());
|
|
Assert.Equal(1, s.EnteredCount);
|
|
Assert.False(guard.IsArmed);
|
|
|
|
// Subsequent tick must not re-fire — one-shot semantics.
|
|
Assert.False(guard.TryEnter());
|
|
Assert.Equal(1, s.EnteredCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryEnter_FiresOnLaterTickWhenPreconditionsBecomeTrue()
|
|
{
|
|
var s = new State();
|
|
var guard = s.Build();
|
|
guard.Arm();
|
|
|
|
// Tick 1: only LiveInWorld true.
|
|
s.LiveInWorld = true;
|
|
Assert.False(guard.TryEnter());
|
|
|
|
// Tick 2: + PlayerEntityPresent.
|
|
s.PlayerEntityPresent = true;
|
|
Assert.False(guard.TryEnter());
|
|
|
|
// Tick 3: + PlayerControllerReady → fires.
|
|
s.PlayerControllerReady = true;
|
|
Assert.True(guard.TryEnter());
|
|
Assert.Equal(1, s.EnteredCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_BeforeFiring_SuppressesAutoEntry()
|
|
{
|
|
// Manual fly-mode toggle BEFORE the auto-entry fires must
|
|
// disarm the trigger; the user's choice wins.
|
|
var s = new State();
|
|
var guard = s.Build();
|
|
guard.Arm();
|
|
|
|
// User opts out before any precondition is true.
|
|
guard.Cancel();
|
|
Assert.False(guard.IsArmed);
|
|
|
|
// Even when every precondition flips true, the guard stays
|
|
// silent — the user's manual fly-mode choice wins.
|
|
s.LiveInWorld = true;
|
|
s.PlayerEntityPresent = true;
|
|
s.PlayerControllerReady = true;
|
|
Assert.False(guard.TryEnter());
|
|
Assert.Equal(0, s.EnteredCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Arm_WhileAlreadyArmed_IsIdempotent()
|
|
{
|
|
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = true };
|
|
var guard = s.Build();
|
|
guard.Arm();
|
|
guard.Arm(); // second Arm() — no-op.
|
|
|
|
Assert.True(guard.TryEnter());
|
|
Assert.Equal(1, s.EnteredCount);
|
|
}
|
|
}
|