refactor(camera): extract the update frame
Move fly/chase publication, combat target tracking, and local player projection behind typed runtime owners. Preserve the inbound-created projection/reconcile barrier while removing GameWindow callbacks and duplicate shadow helpers.
This commit is contained in:
parent
eeb0f6b45c
commit
947c61d2d7
19 changed files with 988 additions and 356 deletions
|
|
@ -170,11 +170,10 @@ public sealed class GameWindow : IDisposable
|
|||
_inboundEntityEvents = new();
|
||||
private LiveEntityAnimationScheduler _liveAnimationScheduler = null!;
|
||||
private LiveEntityAnimationPresenter _animationPresenter = null!;
|
||||
private readonly AcDream.App.Input.LocalPlayerProjectionController _localPlayerProjection;
|
||||
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
||||
_movementTruthDiagnostics;
|
||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||
private readonly AcDream.App.Input.RetailLocalPlayerFrameController _localPlayerFrame;
|
||||
private AcDream.App.Update.ICameraFramePhase _cameraFrame = null!;
|
||||
private AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator = null!;
|
||||
private AcDream.App.Update.LiveSpatialPresentationReconciler
|
||||
_liveSpatialReconciler = null!;
|
||||
|
|
@ -619,6 +618,7 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly AcDream.App.Input.RetainedUiInputCaptureSlot _retainedInputCapture;
|
||||
private readonly AcDream.App.Input.CompositeInputCaptureSource _inputCapture;
|
||||
private readonly AcDream.App.Input.DispatcherMovementInputSource _movementInput;
|
||||
private readonly AcDream.App.Input.DispatcherCameraInputSource _cameraInput = new();
|
||||
private AcDream.App.Input.IMouseLookCursor? _mouseLookCursor;
|
||||
private AcDream.App.Input.GameplayInputFrameController? _gameplayInputFrame;
|
||||
// K.1c: load user-customized bindings from %LOCALAPPDATA%\acdream\keybinds.json,
|
||||
|
|
@ -759,18 +759,6 @@ public sealed class GameWindow : IDisposable
|
|||
movement, cellId, update),
|
||||
(host, targetGuid) =>
|
||||
_liveEntityMotionBindings.StickToObjectFromWire(host, targetGuid));
|
||||
_localPlayerProjection = new AcDream.App.Input.LocalPlayerProjectionController(
|
||||
resolveEntity: () =>
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
||||
? entity
|
||||
: null,
|
||||
liveCenterX: () => _liveCenterX,
|
||||
liveCenterY: () => _liveCenterY,
|
||||
syncShadow: (entity, cellId) => SyncLocalPlayerShadow(entity, cellId),
|
||||
rebucket: (serverGuid, landblockId) =>
|
||||
_liveEntities?.RebucketLiveEntity(serverGuid, landblockId),
|
||||
isCurrentVisibleProjection: IsCurrentVisibleLocalPlayerProjection,
|
||||
suspendShadow: SuspendLocalPlayerShadow);
|
||||
_movementTruthDiagnostics =
|
||||
new AcDream.App.Input.MovementTruthDiagnosticController(
|
||||
options.DumpMoveTruth,
|
||||
|
|
@ -778,32 +766,6 @@ public sealed class GameWindow : IDisposable
|
|||
_localPlayerIdentity);
|
||||
_localPlayerOutbound = new AcDream.App.Input.LocalPlayerOutboundController(
|
||||
_movementTruthDiagnostics);
|
||||
_localPlayerFrame = new AcDream.App.Input.RetailLocalPlayerFrameController(
|
||||
canPresentPlayer: CanAdvanceLocalPlayer,
|
||||
getController: () => _playerController,
|
||||
movementInput: _movementInput,
|
||||
resolveLocalEntityId: () =>
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
||||
? entity.Id
|
||||
: 0u,
|
||||
handleTargeting: () => _playerHost?.HandleTargetting(),
|
||||
isHidden: () => _liveEntities?.IsHidden(_playerServerGuid) == true,
|
||||
project: (controller, movement, hidden) =>
|
||||
_localPlayerProjection.Project(controller, movement, hidden),
|
||||
sendPreNetwork: (controller, movement, hidden) =>
|
||||
_localPlayerOutbound.SendPreNetworkActions(
|
||||
LiveSession,
|
||||
controller,
|
||||
movement,
|
||||
hidden),
|
||||
sendPostNetwork: (controller, hidden) =>
|
||||
_localPlayerOutbound.SendPostNetworkPosition(
|
||||
LiveSession,
|
||||
controller,
|
||||
hidden),
|
||||
objectClockDisposition: () =>
|
||||
_liveEntities?.GetRootObjectClockDisposition(_playerServerGuid)
|
||||
?? AcDream.Core.Physics.RetailObjectClockDisposition.Advance);
|
||||
}
|
||||
|
||||
public void Run()
|
||||
|
|
@ -896,6 +858,7 @@ public sealed class GameWindow : IDisposable
|
|||
_inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher(
|
||||
_kbSource, _mouseSource, _keyBindings);
|
||||
_movementInput.Bind(_inputDispatcher);
|
||||
_cameraInput.Bind(_inputDispatcher);
|
||||
_mouseLookCursor = new AcDream.App.Input.SilkMouseLookCursor(firstMouse);
|
||||
_inputDispatcher.Fired += OnInputAction;
|
||||
Combat.CombatModeChanged += SetInputCombatScope;
|
||||
|
|
@ -2675,19 +2638,6 @@ public sealed class GameWindow : IDisposable
|
|||
new AcDream.App.Update.SettingsParticleRangeSource(
|
||||
_settingsVm,
|
||||
_persistedDisplay.ParticleRange));
|
||||
var liveObjectFrame = new AcDream.App.Update.LiveObjectFrameController(
|
||||
_inboundEntityEvents,
|
||||
_localPlayerFrame,
|
||||
_selectionInteractions,
|
||||
_liveEntities,
|
||||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_liveAnimationScheduler,
|
||||
_staticAnimationScheduler,
|
||||
_animationPresenter,
|
||||
_animatedEntities,
|
||||
_equippedChildRenderer!,
|
||||
liveEffectFrame);
|
||||
_liveSpatialReconciler =
|
||||
new AcDream.App.Update.LiveSpatialPresentationReconciler(
|
||||
_entityEffects!,
|
||||
|
|
@ -2708,6 +2658,44 @@ public sealed class GameWindow : IDisposable
|
|||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_localPlayerShadow);
|
||||
var localPlayerProjection =
|
||||
new AcDream.App.Input.LocalPlayerProjectionController(
|
||||
new AcDream.App.Input.LiveLocalPlayerProjectionRuntime(
|
||||
_liveEntities,
|
||||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_localPlayerShadowSynchronizer));
|
||||
var localPlayerFrameRuntime =
|
||||
new AcDream.App.Input.LiveLocalPlayerFrameRuntime(
|
||||
_cameraController!,
|
||||
_localPlayerMode,
|
||||
_playerControllerSlot,
|
||||
_chaseCameraInput,
|
||||
_movementInput,
|
||||
_inputCapture,
|
||||
_liveEntities,
|
||||
_localPlayerIdentity,
|
||||
_playerHostSlot,
|
||||
localPlayerProjection,
|
||||
_localPlayerOutbound,
|
||||
_liveSessionController);
|
||||
var localPlayerFrame =
|
||||
new AcDream.App.Input.RetailLocalPlayerFrameController(
|
||||
localPlayerFrameRuntime,
|
||||
_movementInput);
|
||||
var liveObjectFrame = new AcDream.App.Update.LiveObjectFrameController(
|
||||
_inboundEntityEvents,
|
||||
localPlayerFrame,
|
||||
_selectionInteractions,
|
||||
_liveEntities,
|
||||
_localPlayerIdentity,
|
||||
_liveWorldOrigin,
|
||||
_liveAnimationScheduler,
|
||||
_staticAnimationScheduler,
|
||||
_animationPresenter,
|
||||
_animatedEntities,
|
||||
_equippedChildRenderer!,
|
||||
liveEffectFrame);
|
||||
_viewportAspect.Update(_window!.Size.X, _window.Size.Y);
|
||||
_playerModeController = new AcDream.App.Input.PlayerModeController(
|
||||
_localPlayerMode,
|
||||
|
|
@ -2783,8 +2771,21 @@ public sealed class GameWindow : IDisposable
|
|||
liveObjectFrame,
|
||||
_worldState,
|
||||
_liveSessionController,
|
||||
_localPlayerFrame,
|
||||
localPlayerFrame,
|
||||
_liveSpatialReconciler);
|
||||
_cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
||||
_cameraController!,
|
||||
_inputCapture,
|
||||
_cameraInput,
|
||||
localPlayerFrameRuntime,
|
||||
_chaseCameraInput,
|
||||
localPlayerFrame,
|
||||
_liveSpatialReconciler,
|
||||
new AcDream.App.Combat.CombatCameraTargetSource(
|
||||
_gameplaySettings,
|
||||
Combat,
|
||||
_selection,
|
||||
_worldSelectionQuery!));
|
||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||
_liveSessionController.Start(
|
||||
_options,
|
||||
|
|
@ -3288,59 +3289,6 @@ public sealed class GameWindow : IDisposable
|
|||
timestamps.ForcePosition);
|
||||
}
|
||||
|
||||
private void SyncLocalPlayerShadow(
|
||||
AcDream.Core.World.WorldEntity playerEntity,
|
||||
uint cellId,
|
||||
bool force = false)
|
||||
{
|
||||
if (_liveEntities?.IsHidden(_playerServerGuid) == true
|
||||
|| cellId == 0
|
||||
|| !IsCurrentVisibleLocalPlayerProjection(playerEntity))
|
||||
{
|
||||
SuspendLocalPlayerShadow(playerEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force
|
||||
&& _localPlayerShadow.Current is { } last
|
||||
&& last.CellId == cellId
|
||||
&& System.Numerics.Vector3.DistanceSquared(
|
||||
last.Position, playerEntity.Position) <= 1e-4f
|
||||
&& MathF.Abs(System.Numerics.Quaternion.Dot(
|
||||
last.Orientation, playerEntity.Rotation)) >= 0.99999f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AcDream.App.Physics.ShadowPositionSynchronizer.Sync(
|
||||
_physicsEngine.ShadowObjects,
|
||||
playerEntity.Id,
|
||||
playerEntity.Position,
|
||||
playerEntity.Rotation,
|
||||
cellId,
|
||||
_liveCenterX,
|
||||
_liveCenterY);
|
||||
_localPlayerShadow.Set(
|
||||
playerEntity.Position,
|
||||
playerEntity.Rotation,
|
||||
cellId);
|
||||
}
|
||||
|
||||
private bool IsCurrentVisibleLocalPlayerProjection(
|
||||
AcDream.Core.World.WorldEntity playerEntity) =>
|
||||
_liveEntities is { } runtime
|
||||
&& runtime.TryGetRecord(playerEntity.ServerGuid, out var record)
|
||||
&& ReferenceEquals(record.WorldEntity, playerEntity)
|
||||
&& runtime.IsCurrentSpatialRootObject(record);
|
||||
|
||||
private void SuspendLocalPlayerShadow(
|
||||
AcDream.Core.World.WorldEntity playerEntity)
|
||||
{
|
||||
_physicsEngine.ShadowObjects.Suspend(playerEntity.Id);
|
||||
_localPlayerShadow.Clear();
|
||||
}
|
||||
|
||||
|
||||
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
||||
// body's cell-relative CellPosition. This is the ONE place the streaming center
|
||||
// (_liveCenter) is allowed to touch the physics frame — at the placement seam,
|
||||
|
|
@ -3584,7 +3532,6 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
AcDream.App.Update.UpdateFrameTiming frameTiming = _updateFrameClock.Advance(
|
||||
new AcDream.App.Update.UpdateFrameInput(dt));
|
||||
double frameSeconds = frameTiming.SimulationDeltaSeconds;
|
||||
float frameDelta = frameTiming.SimulationDeltaSecondsSingle;
|
||||
|
||||
// Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
|
||||
|
|
@ -3634,131 +3581,9 @@ public sealed class GameWindow : IDisposable
|
|||
// OnInputAction (Ctrl+Tab) or DebugPanel.
|
||||
_playerModeAutoEntry?.TryEnter();
|
||||
|
||||
if (_cameraController is null || _input is null) return;
|
||||
|
||||
// Phase D.2a / K.1b — suppress game-side input polling when ImGui
|
||||
// has keyboard focus (e.g. a text field is active). The InputDispatcher
|
||||
// already gates KeyDown/MouseDown via WantCaptureKeyboard internally;
|
||||
// this guard adds defense-in-depth for the per-frame IsActionHeld
|
||||
// movement poll below (typing "walk" into a chat field shouldn't
|
||||
// walk).
|
||||
// ImGui dev-tools text fields fully pause game input (incl. autorun) — fine, it's a
|
||||
// debug overlay. The RETAIL chat "write mode" does NOT early-return here: the block
|
||||
// below still runs so AUTORUN keeps driving the character while you type. Held WASD
|
||||
// is silenced at the source instead — InputDispatcher.IsActionHeld returns false
|
||||
// while WantCaptureKeyboard (which includes a focused chat input) is set.
|
||||
bool suppressGameInput =
|
||||
DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard;
|
||||
if (suppressGameInput) return;
|
||||
|
||||
if (_cameraController.IsFlyMode)
|
||||
{
|
||||
// K.1b: fly-camera input flows through the dispatcher. Reuses
|
||||
// movement actions (Forward/Backup/TurnLeft/TurnRight/Jump/
|
||||
// RunLock) — in fly mode "TurnLeft" semantically maps to
|
||||
// strafe-left because A/D in fly is strafe, not turn (mouse
|
||||
// delta drives fly heading instead).
|
||||
if (_inputDispatcher is null) return;
|
||||
_cameraController.Fly.Update(
|
||||
frameSeconds,
|
||||
w: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementForward),
|
||||
a: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnLeft),
|
||||
s: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementBackup),
|
||||
d: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnRight),
|
||||
up: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementJump),
|
||||
down: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.AcdreamFlyDown),
|
||||
boost: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementRunLock));
|
||||
}
|
||||
else if (_playerMode && _playerController is not null && _chaseCamera is not null)
|
||||
{
|
||||
// Phase B.2 / K.1b: player movement mode — every input flows
|
||||
// through the dispatcher. WASD walks/runs, A/D turns, Z/X
|
||||
// strafes, Shift runs, Space jumps. Mouse delta NEVER drives
|
||||
// character yaw (regression-prevention per K.1b plan §D);
|
||||
// MouseDeltaX is hardcoded 0f. RMB held still pans the chase
|
||||
// camera (handled in the mouse-move handler via _rmbHeld).
|
||||
if (_inputDispatcher is null) return;
|
||||
|
||||
// Retail-style held-key offset integration. Only active when
|
||||
// retail chase is selected; legacy camera ignores these.
|
||||
if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null)
|
||||
{
|
||||
float adj = AcDream.Core.Rendering.CameraDiagnostics.CameraAdjustmentSpeed * frameDelta;
|
||||
if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraZoomIn))
|
||||
_retailChaseCamera.AdjustDistance(-adj);
|
||||
if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraZoomOut))
|
||||
_retailChaseCamera.AdjustDistance(+adj);
|
||||
if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraRaise))
|
||||
_retailChaseCamera.AdjustPitch(+adj * 0.02f);
|
||||
if (_inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.CameraLower))
|
||||
_retailChaseCamera.AdjustPitch(-adj * 0.02f);
|
||||
}
|
||||
|
||||
// K-fix1 (2026-04-26): retail-faithful movement semantics.
|
||||
// * Default speed = RUN. Forward / backward / strafe all run
|
||||
// by default; holding Shift (MovementWalkMode) drops to
|
||||
// walk speed.
|
||||
// * Q = AUTORUN TOGGLE: pressing Q latches forward-running
|
||||
// until Q is pressed again. Handled in OnInputAction; here
|
||||
// DispatcherMovementInputSource owns the autorun latch.
|
||||
// * Mouse never drives character yaw (K.1b regression-prevention).
|
||||
if (!_localPlayerFrame.TryGetPresentationAfterNetwork(out var playerFrame))
|
||||
return;
|
||||
|
||||
AcDream.App.Input.MovementResult result = playerFrame.Movement;
|
||||
bool localPlayerHidden = playerFrame.Hidden;
|
||||
if (!playerFrame.AdvancedBeforeNetwork)
|
||||
{
|
||||
// The player was materialized by this frame's inbound pass.
|
||||
// Its non-advancing initial projection must publish child and
|
||||
// effect anchors before the first draw too.
|
||||
_liveSpatialReconciler.Reconcile();
|
||||
}
|
||||
|
||||
// Update chase camera(s). The CameraController exposes whichever
|
||||
// is currently selected via CameraDiagnostics.UseRetailChaseCamera;
|
||||
// both update every frame so toggling the flag swaps instantly
|
||||
// with the new camera already warm.
|
||||
//
|
||||
// Legacy ChaseCamera: pre-K-fix12 args (isOnGround pins Z during
|
||||
// jumps as a workaround for the visual feel retail gets from
|
||||
// low-stiffness damping).
|
||||
_chaseCamera.Update(result.RenderPosition, _playerController.Yaw,
|
||||
isOnGround: result.IsOnGround,
|
||||
dt: frameDelta);
|
||||
// RetailChaseCamera: heading is the player's facing direction
|
||||
// projected onto the contact plane when grounded, or the
|
||||
// world XY plane when airborne. The contact plane normal
|
||||
// tilts the camera basis with terrain; the airborne
|
||||
// fallback keeps the basis horizontal during jumps so the
|
||||
// player visibly rises in frame without the camera
|
||||
// swinging vertically (was the symptom of using raw
|
||||
// velocity-vector heading).
|
||||
System.Numerics.Vector3? trackedCombatTarget = GetCombatCameraTargetPoint();
|
||||
_retailChaseCamera!.Update(result.RenderPosition, _playerController.Yaw,
|
||||
playerVelocity: _playerController.BodyVelocity,
|
||||
isOnGround: result.IsOnGround,
|
||||
contactPlaneNormal: _playerController.ContactPlane.Normal,
|
||||
dt: frameDelta,
|
||||
cellId: _playerController.CellId,
|
||||
selfEntityId: _playerController.LocalEntityId,
|
||||
trackedTargetPoint: trackedCombatTarget);
|
||||
|
||||
|
||||
// Update the player entity's animation cycle to match current motion.
|
||||
}
|
||||
_cameraFrame.Tick(frameTiming);
|
||||
}
|
||||
|
||||
private bool CanAdvanceLocalPlayer() =>
|
||||
_cameraController is not null
|
||||
&& _input is not null
|
||||
&& !_cameraController.IsFlyMode
|
||||
&& _playerMode
|
||||
&& _playerController is not null
|
||||
&& _chaseCamera is not null
|
||||
&& _inputDispatcher is not null
|
||||
&& !(DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard);
|
||||
|
||||
private void OnCameraModeChanged(bool _modeBool)
|
||||
{
|
||||
if (_gameplayInputFrame?.MouseLookActive == true
|
||||
|
|
@ -6800,22 +6625,6 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.App.Input.MovementResult result) =>
|
||||
AcDream.App.Input.LocalPlayerOutboundController.BuildRawMotionState(result);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves retail's combat-camera target. ClientCombatSystem::
|
||||
/// UpdateTargetTracking (0x0056A950) enables CameraSet::TrackTarget only
|
||||
/// for melee/missile combat with the option enabled and a valid attack
|
||||
/// target. TrackTarget applies target-local offset (0,0,0.5).
|
||||
/// </summary>
|
||||
private System.Numerics.Vector3? GetCombatCameraTargetPoint()
|
||||
{
|
||||
if (!_persistedGameplay.ViewCombatTarget
|
||||
|| !AcDream.Core.Combat.CombatInputPlanner.SupportsTargetedAttack(Combat.CurrentMode)
|
||||
|| _selection.SelectedObjectId is not uint selected)
|
||||
return null;
|
||||
|
||||
return _worldSelectionQuery?.GetCombatCameraTargetPoint(selected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Item-target-mode world pick at the current cursor. The renderer supplies the
|
||||
/// exact visible CPhysicsPart equivalents and RetailWorldPicker performs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue