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
|
|
@ -447,12 +447,9 @@ public sealed class GameWindow : IDisposable
|
|||
// the bool here.
|
||||
private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry;
|
||||
|
||||
// Phase K.1b — single input path. Every keyboard/mouse-button reaction
|
||||
// flows through InputDispatcher.Fired (see OnInputAction below) or
|
||||
// IsActionHeld (per-frame polling for movement). The legacy direct
|
||||
// kb.KeyDown switch + mouse.MouseDown/MouseUp handlers are GONE; only
|
||||
// mouse.MouseMove survives as a direct handler because mouse-delta is
|
||||
// axis input, not chord input.
|
||||
// Phase K.1b / Slice 8 F — one semantic input path. Transitional actions
|
||||
// flow through GameplayInputActionRouter; held movement is polled through
|
||||
// InputDispatcher. Raw axis motion belongs to CameraPointerInputController.
|
||||
private AcDream.App.Input.SilkKeyboardSource? _kbSource;
|
||||
private AcDream.App.Input.SilkMouseSource? _mouseSource;
|
||||
private AcDream.UI.Abstractions.Input.InputDispatcher? _inputDispatcher;
|
||||
|
|
@ -462,6 +459,12 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly AcDream.App.Input.DispatcherCameraInputSource _cameraInput = new();
|
||||
private AcDream.App.Input.IMouseLookCursor? _mouseLookCursor;
|
||||
private AcDream.App.Input.GameplayInputFrameController? _gameplayInputFrame;
|
||||
private AcDream.App.Input.GameplayInputActionRouter? _gameplayInputActions;
|
||||
private AcDream.App.Input.RetainedUiGameplayBinding? _retainedUiGameplayBinding;
|
||||
private readonly AcDream.App.Diagnostics.RuntimeDiagnosticCommandSlot
|
||||
_runtimeDiagnosticCommands = new();
|
||||
private readonly AcDream.App.Combat.LiveCombatModeCommandSlot
|
||||
_liveCombatModeCommands = new();
|
||||
// K.1c: load user-customized bindings from %LOCALAPPDATA%\acdream\keybinds.json,
|
||||
// falling back to the retail-faithful defaults if the file is missing
|
||||
// or corrupt. This is THE single source of truth for the keymap at
|
||||
|
|
@ -693,10 +696,6 @@ public sealed class GameWindow : IDisposable
|
|||
_inputDispatcher.Attach();
|
||||
_movementInput.Bind(_inputDispatcher);
|
||||
_cameraInput.Bind(_inputDispatcher);
|
||||
_inputDispatcher.Fired += OnInputAction;
|
||||
Combat.CombatModeChanged += SetInputCombatScope;
|
||||
SetInputCombatScope(Combat.CurrentMode);
|
||||
|
||||
}
|
||||
|
||||
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||
|
|
@ -960,9 +959,12 @@ public sealed class GameWindow : IDisposable
|
|||
getFrameMs: () =>
|
||||
(float)(_renderFrameDiagnostics?.Snapshot.FrameMilliseconds ?? 16.7),
|
||||
combat: Combat);
|
||||
_debugVm.CycleTimeOfDay = CycleTimeOfDay;
|
||||
_debugVm.CycleWeather = CycleWeather;
|
||||
_debugVm.ToggleCollisionWires = ToggleCollisionWires;
|
||||
_debugVm.CycleTimeOfDay =
|
||||
_runtimeDiagnosticCommands.CycleTimeOfDay;
|
||||
_debugVm.CycleWeather =
|
||||
_runtimeDiagnosticCommands.CycleWeather;
|
||||
_debugVm.ToggleCollisionWires =
|
||||
_runtimeDiagnosticCommands.ToggleCollisionWireframes;
|
||||
// Phase K.2: free-fly toggle button — same routine the
|
||||
// legacy F-key alias hits. Cancels the one-shot
|
||||
// auto-entry if the user opts out of player mode before
|
||||
|
|
@ -1406,8 +1408,6 @@ public sealed class GameWindow : IDisposable
|
|||
displayMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||
incrementBusy: () => _itemInteractionController.IncrementBusyCount(),
|
||||
canSend: () => _liveSessionHost?.IsInWorld == true);
|
||||
_uiHost.Root.DragReleasedOutsideUi += OnUiDragReleasedOutside;
|
||||
|
||||
// Feed Silk input to the UiRoot tree so windows drag / close / select.
|
||||
// UiRoot consumes UI events; the game InputDispatcher (subscribed to the
|
||||
// same devices) is gated off via WantCaptureMouse/Keyboard above when the
|
||||
|
|
@ -1577,7 +1577,7 @@ public sealed class GameWindow : IDisposable
|
|||
UseItemByGuid,
|
||||
Combat,
|
||||
ItemMana,
|
||||
ToggleLiveCombatMode,
|
||||
_liveCombatModeCommands.Toggle,
|
||||
_itemInteractionController,
|
||||
entry => LiveSession?.SendAddShortcut(entry),
|
||||
index => LiveSession?.SendRemoveShortcut(index),
|
||||
|
|
@ -2007,6 +2007,17 @@ public sealed class GameWindow : IDisposable
|
|||
text => _debugVm?.AddToast(text),
|
||||
_playerApproachCompletions);
|
||||
}
|
||||
if (_uiHost is { } retainedHost
|
||||
&& _selectionInteractions is { } selectionInteractions)
|
||||
{
|
||||
_retainedUiGameplayBinding =
|
||||
AcDream.App.Input.RetainedUiGameplayBinding.Create(
|
||||
retainedHost.Root,
|
||||
(item, x, y) =>
|
||||
selectionInteractions.PlaceDraggedItem(item, x, y),
|
||||
_hostQuiescence);
|
||||
_retainedUiGameplayBinding.Attach();
|
||||
}
|
||||
// A.5 T22.5: apply A2C gate from quality preset.
|
||||
_wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage;
|
||||
|
||||
|
|
@ -2813,6 +2824,81 @@ public sealed class GameWindow : IDisposable
|
|||
_playerModeAutoEntry),
|
||||
cameraFrame);
|
||||
_liveSessionHost = CreateLiveSessionHost();
|
||||
|
||||
AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm;
|
||||
Action<string>? debugToast = debugVm is null
|
||||
? null
|
||||
: message => debugVm.AddToast(message);
|
||||
AcDream.Core.Chat.ChatLog chat = Chat;
|
||||
var combatCommand =
|
||||
new AcDream.App.Combat.LiveCombatModeCommandController(
|
||||
new AcDream.App.Combat.LiveSessionCombatModeAuthority(
|
||||
_liveSessionHost),
|
||||
new AcDream.App.Combat.LocalPlayerCombatEquipmentSource(
|
||||
Objects,
|
||||
_localPlayerIdentity),
|
||||
Combat,
|
||||
new AcDream.App.Combat.ItemInteractionCombatModeIntentSink(
|
||||
_itemInteractionController!),
|
||||
Console.WriteLine,
|
||||
debugToast,
|
||||
text => chat.OnSystemMessage(text, 0x1Au));
|
||||
_liveCombatModeCommands.Bind(combatCommand);
|
||||
|
||||
var nearbyDiagnostics =
|
||||
new AcDream.App.Diagnostics.NearbyWorldDiagnosticDumper(
|
||||
new AcDream.App.Diagnostics.RuntimeNearbyWorldDiagnosticSource(
|
||||
_localPlayerMode,
|
||||
_playerControllerSlot,
|
||||
_cameraController!,
|
||||
_liveWorldOrigin,
|
||||
_worldState,
|
||||
_physicsEngine),
|
||||
Console.WriteLine);
|
||||
var runtimeDiagnostics =
|
||||
new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(
|
||||
_worldEnvironment,
|
||||
_worldSceneDebugState,
|
||||
_cameraPointerInput,
|
||||
nearbyDiagnostics,
|
||||
debugToast);
|
||||
_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);
|
||||
|
||||
if (_inputDispatcher is { } dispatcher)
|
||||
{
|
||||
var pointer = _cameraPointerInput
|
||||
?? throw new InvalidOperationException(
|
||||
"Gameplay action routing requires the composed pointer owner.");
|
||||
var commands = new AcDream.App.Input.GameplayInputCommandController(
|
||||
new AcDream.App.Input.RetainedGameplayWindowCommands(
|
||||
_retailUiRuntime),
|
||||
new AcDream.App.Input.DevToolsGameplayCommands(
|
||||
_devToolsFramePresenter),
|
||||
runtimeDiagnostics,
|
||||
new AcDream.App.Input.PlayerModeGameplayCommands(
|
||||
_localPlayerMode,
|
||||
_playerModeController),
|
||||
new AcDream.App.Input.ItemTargetModeCommands(
|
||||
_itemInteractionController!),
|
||||
new AcDream.App.Input.GameplayCameraModeCommands(
|
||||
_cameraController!),
|
||||
combatCommand,
|
||||
new AcDream.App.Input.GameplayWindowCommands(_window!.Close));
|
||||
var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets(
|
||||
_gameplayInputFrame!,
|
||||
pointer,
|
||||
_retailUiRuntime,
|
||||
_selectionInteractions,
|
||||
commands);
|
||||
_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(
|
||||
dispatcher,
|
||||
Combat,
|
||||
targets,
|
||||
_hostQuiescence,
|
||||
Console.WriteLine);
|
||||
_gameplayInputActions.Attach();
|
||||
}
|
||||
|
||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||
_liveSessionHost.Start(_options);
|
||||
switch (liveStart.Status)
|
||||
|
|
@ -3241,36 +3327,6 @@ public sealed class GameWindow : IDisposable
|
|||
private bool GetDebugPlayerOnGround() =>
|
||||
_playerMode && _playerController is not null && !_playerController.IsAirborne;
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the time-of-day debug override. Same body as the old F7
|
||||
/// keybind handler; called by both the keybind AND the DebugPanel
|
||||
/// "Cycle time of day" button via DebugVM.CycleTimeOfDay.
|
||||
/// </summary>
|
||||
private void CycleTimeOfDay()
|
||||
{
|
||||
string message = _worldEnvironment.CycleTimeOfDay();
|
||||
_debugVm?.AddToast(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the weather kind. Same body as the old F10 keybind handler.
|
||||
/// </summary>
|
||||
private void CycleWeather()
|
||||
{
|
||||
string message = _worldEnvironment.CycleWeather();
|
||||
_debugVm?.AddToast(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the collision-wires debug renderer. Same body as the old
|
||||
/// F2 keybind handler.
|
||||
/// </summary>
|
||||
private void ToggleCollisionWires()
|
||||
{
|
||||
bool visible = _worldSceneDebugState.ToggleCollisionWireframes();
|
||||
_debugVm?.AddToast($"Collision wireframes {(visible ? "ON" : "OFF")}");
|
||||
}
|
||||
|
||||
// Phase K.3 settings state remains runtime-owned; the presenter owns the
|
||||
// optional SettingsPanel and its visibility/input operations.
|
||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsVM? _settingsVm;
|
||||
|
|
@ -3282,11 +3338,6 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.UI.Abstractions.Panels.Settings.SettingsStore? _settingsStore;
|
||||
private string _activeToonKey = "default";
|
||||
|
||||
private bool ToggleRetailWindow(string name)
|
||||
{
|
||||
return _retailUiRuntime?.ToggleWindow(name) ?? false;
|
||||
}
|
||||
|
||||
private void SyncToolbarWindowButtons()
|
||||
{
|
||||
_retailUiRuntime?.SyncToolbarWindowButtons();
|
||||
|
|
@ -3369,12 +3420,6 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void OnUiDragReleasedOutside(object payload, int x, int y)
|
||||
{
|
||||
if (payload is AcDream.App.UI.ItemDragPayload itemPayload)
|
||||
_selectionInteractions?.PlaceDraggedItem(itemPayload, x, y);
|
||||
}
|
||||
|
||||
// L.0 follow-up: persisted-settings cache populated by
|
||||
// LoadAndApplyPersistedSettings (runs unconditionally in OnLoad,
|
||||
// not gated on DevToolsEnabled). The Settings PANEL construction
|
||||
|
|
@ -3563,202 +3608,6 @@ public sealed class GameWindow : IDisposable
|
|||
&& width > 0
|
||||
&& height > 0;
|
||||
}
|
||||
// ── K.1b: dispatcher action handler ──────────────────────────────────
|
||||
//
|
||||
// SINGLE place where every game-side keyboard/mouse-button reaction
|
||||
// lives. The legacy direct kb.KeyDown switch + mouse.MouseDown/MouseUp
|
||||
// handlers are gone; everything now flows through InputDispatcher.Fired
|
||||
// → here. New behaviors register a new InputAction in the enum + a
|
||||
// case in this switch + a binding in KeyBindings.
|
||||
|
||||
/// <summary>
|
||||
/// K.1b — multicast subscriber on <see cref="InputDispatcher.Fired"/>.
|
||||
/// Handles every game-side reaction to a keyboard/mouse-button chord.
|
||||
/// Per-frame held-state polling (movement WASD/Shift/Space) lives in
|
||||
/// <see cref="OnUpdate"/> via <see cref="InputDispatcher.IsActionHeld"/>;
|
||||
/// this method handles transitional Press/Release events only.
|
||||
/// </summary>
|
||||
private void SetInputCombatScope(AcDream.Core.Combat.CombatMode mode)
|
||||
{
|
||||
_inputDispatcher?.SetCombatScope(mode switch
|
||||
{
|
||||
AcDream.Core.Combat.CombatMode.Melee => AcDream.UI.Abstractions.Input.InputScope.MeleeCombat,
|
||||
AcDream.Core.Combat.CombatMode.Missile => AcDream.UI.Abstractions.Input.InputScope.MissileCombat,
|
||||
AcDream.Core.Combat.CombatMode.Magic => AcDream.UI.Abstractions.Input.InputScope.MagicCombat,
|
||||
_ => null,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnInputAction(
|
||||
AcDream.UI.Abstractions.Input.InputAction action,
|
||||
AcDream.UI.Abstractions.Input.ActivationType activation)
|
||||
{
|
||||
// Diagnostic — kept from K.1a; helpful for K.1c verification.
|
||||
Console.WriteLine($"[input] {action} {activation}");
|
||||
|
||||
// RMB orbit and instant mouse-look remain the first accepted input
|
||||
// transition. Their state is owned by the gameplay/pointer owners.
|
||||
if (_gameplayInputFrame?.HandlePointerAction(action, activation) == true)
|
||||
return;
|
||||
|
||||
// Phase K.2 — MMB-hold instant mouse-look. Press hides the
|
||||
// cursor + activates yaw drive; release restores. WantCapture
|
||||
// edge handling lives in OnUpdate; only Press needs to read it
|
||||
// for the initial gate (defense in depth — the dispatcher
|
||||
// already filters on WantCaptureMouse in OnMouseDown).
|
||||
// ScrollUp / ScrollDown — emit by InputDispatcher.OnScroll on every
|
||||
// wheel tick. Press is the only activation type for wheel.
|
||||
if (action == AcDream.UI.Abstractions.Input.InputAction.ScrollUp
|
||||
|| action == AcDream.UI.Abstractions.Input.InputAction.ScrollDown)
|
||||
{
|
||||
if (activation != AcDream.UI.Abstractions.Input.ActivationType.Press) return;
|
||||
HandleScrollAction(action);
|
||||
return;
|
||||
}
|
||||
|
||||
// ACCmdInterp::HandleNewForwardMovement (0x0058B1F0) aborts automatic
|
||||
// combat before the movement command enters CommandInterpreter. This
|
||||
// notification does not consume the input; the movement owner below
|
||||
// still receives it normally.
|
||||
// Retail attack actions consume their full transition stream: key-down
|
||||
// starts the power build and key-up commits it. Handle them before the
|
||||
// generic Press-only gate below.
|
||||
if (_gameplayInputFrame?.HandleCombatAction(action, activation) == true)
|
||||
return;
|
||||
|
||||
// Every other action fires on Press only (no Release / Hold side-
|
||||
// effects in the K.1b set). Filter out non-Press activations early
|
||||
// so subscribers that have Release-mode bindings don't accidentally
|
||||
// re-fire. B.4b exception: DoubleClick must pass through so
|
||||
// SelectDblLeft / SelectDblRight / SelectDblMid can reach the switch.
|
||||
if (activation != AcDream.UI.Abstractions.Input.ActivationType.Press
|
||||
&& activation != AcDream.UI.Abstractions.Input.ActivationType.DoubleClick) return;
|
||||
|
||||
// Wave 4.1: one retained-UI delegation seam. The runtime routes only
|
||||
// toolbar-owned semantic actions; every other action continues through
|
||||
// the game switch below. Keyboard capture is already enforced at the
|
||||
// InputDispatcher source, matching retail's stack-entry focus gate.
|
||||
if (_retailUiRuntime?.HandleInputAction(action) == true)
|
||||
return;
|
||||
|
||||
if (_selectionInteractions?.HandleInputAction(action) == true)
|
||||
return;
|
||||
|
||||
// K-fix1 (2026-04-26): Q is autorun TOGGLE, not hold-to-run. Press
|
||||
// Q to start, press Q again to stop. Pressing Backup / Stop /
|
||||
// StrafeLeft / StrafeRight while autorun is active also cancels it
|
||||
// — retail-faithful: any deliberate movement input wins. (Pressing
|
||||
// Forward AGAIN does NOT cancel — retail's autorun stays active
|
||||
// even when you press W; the two stack.)
|
||||
if (_gameplayInputFrame?.HandlePressedMovementAction(action) == true)
|
||||
return;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case AcDream.UI.Abstractions.Input.InputAction.ToggleInventoryPanel:
|
||||
// Retail F12 (rebindable). Gated upstream by WantsKeyboard, so it
|
||||
// does not fire while the chat input holds focus. Null _uiHost =
|
||||
// retail UI off (ACDREAM_RETAIL_UI unset) → no-op.
|
||||
ToggleRetailWindow(AcDream.App.UI.WindowNames.Inventory);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleDebugPanel:
|
||||
_devToolsFramePresenter?.ToggleDebugPanel();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleCollisionWires:
|
||||
ToggleCollisionWires();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamDumpNearby:
|
||||
DumpPlayerAndNearbyEntities();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamCycleTimeOfDay:
|
||||
CycleTimeOfDay();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamSensitivityDown:
|
||||
AdjustActiveSensitivity(1f / 1.2f);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamSensitivityUp:
|
||||
AdjustActiveSensitivity(1.2f);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamCycleWeather:
|
||||
CycleWeather();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleFlyMode:
|
||||
// K-fix3 (2026-04-26): proper round-trip when player has
|
||||
// an active chase camera. ToggleFly() only swaps
|
||||
// Fly↔Orbit, so a user who flew out of player mode used
|
||||
// to land in Holtburg-orbit on toggle-back. With a chase
|
||||
// camera available, prefer Fly→Chase / Chase→Fly so the
|
||||
// user round-trips back to the same player view.
|
||||
_playerModeController?.ToggleFlyOrChase();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamTogglePlayerMode:
|
||||
_playerModeController?.Toggle();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.ToggleChatEntry:
|
||||
// K.2: Tab focuses the chat input. ChatPanel.FocusInput()
|
||||
// sets a one-shot flag that emits SetKeyboardFocusHere on
|
||||
// the next render. No-op when the devtools presenter is absent
|
||||
// (offline / non-devtools build) — the dispatcher still
|
||||
// logs the action via the [input] diagnostic above so the
|
||||
// path is observable in either case.
|
||||
_devToolsFramePresenter?.FocusChatInput();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.ToggleOptionsPanel:
|
||||
// K.3: F11 toggles the Settings panel. Null-safe vs.
|
||||
// devtools-off / panel-not-registered — the [input] log
|
||||
// line above still records the press regardless.
|
||||
_devToolsFramePresenter?.ToggleSettingsPanel();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.CombatToggleCombat:
|
||||
ToggleLiveCombatMode();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.EscapeKey:
|
||||
if (_itemInteractionController?.IsAnyTargetModeActive == true)
|
||||
_itemInteractionController.CancelTargetMode();
|
||||
else if (_cameraController?.IsFlyMode == true)
|
||||
_cameraController.ToggleFly(); // exit fly, release cursor
|
||||
else if (_playerMode)
|
||||
_playerModeController?.Exit();
|
||||
else
|
||||
_window!.Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleLiveCombatMode()
|
||||
{
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (_liveSessionHost?.IsInWorld != true || session is null)
|
||||
return;
|
||||
|
||||
IReadOnlyList<AcDream.Core.Items.ClientObject> orderedEquipment =
|
||||
Objects.GetEquippedBy(_playerServerGuid);
|
||||
var defaultMode = AcDream.Core.Combat.CombatInputPlanner
|
||||
.GetDefaultCombatMode(orderedEquipment);
|
||||
var nextMode = AcDream.Core.Combat.CombatInputPlanner.ToggleMode(
|
||||
Combat.CurrentMode,
|
||||
defaultMode);
|
||||
_itemInteractionController?.NotifyExplicitCombatModeRequest();
|
||||
session.SendChangeCombatMode(nextMode);
|
||||
Combat.SetCombatMode(nextMode);
|
||||
string text = $"Combat mode {nextMode}";
|
||||
Console.WriteLine($"combat: {text}");
|
||||
_debugVm?.AddToast(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Item-target-mode world pick at the current cursor. The renderer supplies the
|
||||
/// exact visible CPhysicsPart equivalents and RetailWorldPicker performs
|
||||
|
|
@ -3843,87 +3692,6 @@ public sealed class GameWindow : IDisposable
|
|||
return unhydratable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// K.1b: F8/F9 sensitivity adjust extracted into a helper. Multiplies
|
||||
/// the currently-active mode's sensitivity (chase / fly / orbit) by the
|
||||
/// given factor and clamps to [0.005, 3.0].
|
||||
/// </summary>
|
||||
private void AdjustActiveSensitivity(float factor)
|
||||
{
|
||||
if (_cameraPointerInput is not { } pointer)
|
||||
return;
|
||||
string message = pointer.AdjustSensitivity(factor);
|
||||
_debugVm?.AddToast(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// K.1b: F3 dump handler extracted into a method. Same body as the
|
||||
/// previous in-line F3 branch — prints the player's position +
|
||||
/// nearby visible entities + nearby shadow physics objects.
|
||||
/// </summary>
|
||||
private void DumpPlayerAndNearbyEntities()
|
||||
{
|
||||
System.Numerics.Vector3 pos;
|
||||
if (_playerMode && _playerController is not null)
|
||||
pos = _playerController.Position;
|
||||
else
|
||||
{
|
||||
System.Numerics.Matrix4x4.Invert(_cameraController!.Active.View, out var iv);
|
||||
pos = new System.Numerics.Vector3(iv.M41, iv.M42, iv.M43);
|
||||
}
|
||||
int lbX = _liveCenterX + (int)MathF.Floor(pos.X / 192f);
|
||||
int lbY = _liveCenterY + (int)MathF.Floor(pos.Y / 192f);
|
||||
Console.WriteLine(
|
||||
$"=== F3 DEBUG DUMP ===\n" +
|
||||
$" player pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2})\n" +
|
||||
$" landblock=0x{(uint)((lbX << 24) | (lbY << 16) | 0xFFFF):X8} local=({pos.X - (lbX - _liveCenterX) * 192f:F2},{pos.Y - (lbY - _liveCenterY) * 192f:F2})\n" +
|
||||
$" total shadow objects: {_physicsEngine.ShadowObjects.TotalRegistered}");
|
||||
|
||||
var visibleNearby = new List<AcDream.Core.World.WorldEntity>();
|
||||
foreach (var e in _worldState.Entities)
|
||||
{
|
||||
float dx = e.Position.X - pos.X;
|
||||
float dy = e.Position.Y - pos.Y;
|
||||
if (dx * dx + dy * dy < 15f * 15f) visibleNearby.Add(e);
|
||||
}
|
||||
Console.WriteLine($" VISIBLE entities within 15m: {visibleNearby.Count}");
|
||||
foreach (var e in visibleNearby.OrderBy(e => (e.Position - pos).Length()).Take(12))
|
||||
{
|
||||
float d = (e.Position - pos).Length();
|
||||
Console.WriteLine(
|
||||
$" VIS id=0x{e.Id:X8} src=0x{e.SourceGfxObjOrSetupId:X8} " +
|
||||
$"pos=({e.Position.X:F2},{e.Position.Y:F2},{e.Position.Z:F2}) dist={d:F2} scale={e.Scale:F2}");
|
||||
}
|
||||
|
||||
var sorted = new List<(AcDream.Core.Physics.ShadowEntry obj, float dist)>();
|
||||
foreach (var o in _physicsEngine.ShadowObjects.AllEntriesForDebug())
|
||||
{
|
||||
float dx = o.Position.X - pos.X;
|
||||
float dy = o.Position.Y - pos.Y;
|
||||
float d = MathF.Sqrt(dx * dx + dy * dy);
|
||||
if (d < 15f) sorted.Add((o, d));
|
||||
}
|
||||
sorted.Sort((a, b) => a.dist.CompareTo(b.dist));
|
||||
Console.WriteLine($" SHADOW objects within 15m: {sorted.Count}");
|
||||
foreach (var (o, d) in sorted.Take(12))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$" SHAD id=0x{o.EntityId:X8} {o.CollisionType} r={o.Radius:F2} h={o.CylHeight:F2} " +
|
||||
$"pos=({o.Position.X:F2},{o.Position.Y:F2},{o.Position.Z:F2}) dist={d:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// K.1b: ScrollUp / ScrollDown action handler. Adjusts whichever
|
||||
/// camera distance is current — chase camera distance in player mode,
|
||||
/// orbit camera distance otherwise. Fly mode ignores scroll. Magnitude
|
||||
/// is fixed-step (the previous proportional scroll.Y was lost when we
|
||||
/// moved scroll into the dispatcher, but the discrete step matches
|
||||
/// retail wheel feel).
|
||||
/// </summary>
|
||||
private void HandleScrollAction(AcDream.UI.Abstractions.Input.InputAction action)
|
||||
=> _cameraPointerInput?.HandleScroll(action);
|
||||
|
||||
private void OnClosing()
|
||||
{
|
||||
_hostQuiescence.StopAccepting();
|
||||
|
|
@ -3956,6 +3724,10 @@ public sealed class GameWindow : IDisposable
|
|||
// are already inert.
|
||||
new ResourceShutdownStage("input callback deactivation",
|
||||
[
|
||||
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("dispatcher", () => _inputDispatcher?.Deactivate()),
|
||||
new("mouse source", () => _mouseSource?.Deactivate()),
|
||||
|
|
@ -3991,6 +3763,28 @@ public sealed class GameWindow : IDisposable
|
|||
]),
|
||||
new ResourceShutdownStage("input callback detach",
|
||||
[
|
||||
new("retained gameplay", () =>
|
||||
{
|
||||
AcDream.App.Input.RetainedUiGameplayBinding? binding =
|
||||
_retainedUiGameplayBinding;
|
||||
if (binding is null) return;
|
||||
binding.Dispose();
|
||||
if (!binding.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"Retained gameplay callback removal remains pending.");
|
||||
_retainedUiGameplayBinding = null;
|
||||
}),
|
||||
new("gameplay actions", () =>
|
||||
{
|
||||
AcDream.App.Input.GameplayInputActionRouter? actions =
|
||||
_gameplayInputActions;
|
||||
if (actions is null) return;
|
||||
actions.Dispose();
|
||||
if (!actions.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"Gameplay action callback removal remains pending.");
|
||||
_gameplayInputActions = null;
|
||||
}),
|
||||
new("retained UI input", () => _uiHost?.DeactivateInput()),
|
||||
new("developer tools input", () => _devToolsInputContext?.Dispose()),
|
||||
new("camera pointer", () =>
|
||||
|
|
@ -4003,14 +3797,11 @@ public sealed class GameWindow : IDisposable
|
|||
throw new InvalidOperationException(
|
||||
"Camera pointer callback removal remains pending.");
|
||||
}),
|
||||
new("combat input subscription", () =>
|
||||
Combat.CombatModeChanged -= SetInputCombatScope),
|
||||
new("dispatcher", () =>
|
||||
{
|
||||
AcDream.UI.Abstractions.Input.InputDispatcher? dispatcher =
|
||||
_inputDispatcher;
|
||||
if (dispatcher is null) return;
|
||||
dispatcher.Fired -= OnInputAction;
|
||||
_movementInput.Unbind(dispatcher);
|
||||
_cameraInput.Unbind(dispatcher);
|
||||
dispatcher.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue