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
|
|
@ -22,9 +22,9 @@ never hidden behind a retry, delay, suppression flag, or reordered callback.
|
|||
collapse, streaming tick, and rescued-entity rebucketing.
|
||||
- [x] D — extract dispatcher/raw-mouse/combat input sampling without changing
|
||||
movement-command or UI-capture semantics.
|
||||
- [ ] E — extract the complete local-player teleport owner and wire it between
|
||||
- [x] E — extract the complete local-player teleport owner and wire it between
|
||||
the existing post-network liveness and player-mode auto-entry phases.
|
||||
- [ ] F — extract the shared player-mode lifecycle plus fly/chase/player
|
||||
- [x] F — extract the shared player-mode lifecycle plus fly/chase/player
|
||||
camera presentation and cut production over to one
|
||||
`UpdateFrameOrchestrator`.
|
||||
- [ ] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
|
||||
|
|
@ -455,6 +455,20 @@ lines at this checkpoint, down from the slice baseline of 15,723.
|
|||
- measure line/field/method count. The target is below 8,000 lines, but the
|
||||
ownership/ordering exit criteria outrank the count.
|
||||
|
||||
**Checkpoint F completed 2026-07-22.** `CameraFrameController` now owns fly
|
||||
held input, retail chase adjustment, the inbound-created-player projection
|
||||
and second reconcile, both chase-camera publications, and combat-target
|
||||
tracking. `RetailLocalPlayerFrameController` and
|
||||
`LocalPlayerProjectionController` resolve current identity, session, world,
|
||||
shadow, and player slots through focused typed runtimes instead of callbacks
|
||||
into `GameWindow`; the camera receives only the two-read player-presentation
|
||||
seam. Projection and frame owners remain composition locals, so the window
|
||||
retains no duplicate lifecycle state. Focused Release tests are green at
|
||||
37/37, the App suite at 2,820 passed / 3 skipped, and the full suite at 7,179
|
||||
passed / 5 skipped. All three corrected-diff reviews are clean.
|
||||
`GameWindow.cs` is 7,160 lines, down 8,563 lines (54.5%) from the 15,723-line
|
||||
slice baseline.
|
||||
|
||||
### H — release and connected gates
|
||||
|
||||
After all three independent corrected-diff reviews are clean:
|
||||
|
|
|
|||
53
src/AcDream.App/Combat/CombatCameraTargetSource.cs
Normal file
53
src/AcDream.App/Combat/CombatCameraTargetSource.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.App.Combat;
|
||||
|
||||
internal interface ICombatCameraTargetSource
|
||||
{
|
||||
Vector3? GetTrackedTargetPoint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves retail's combat-camera target from canonical combat, selection,
|
||||
/// gameplay-option, and world-selection owners.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>ClientCombatSystem::UpdateTargetTracking @ 0x0056A950</c> enables
|
||||
/// <c>CameraSet::TrackTarget</c> only for melee/missile combat with the option
|
||||
/// enabled and a valid attack target. TrackTarget applies target-local offset
|
||||
/// (0, 0, 0.5).
|
||||
/// </remarks>
|
||||
internal sealed class CombatCameraTargetSource : ICombatCameraTargetSource
|
||||
{
|
||||
private readonly ICombatGameplaySettingsSource _settings;
|
||||
private readonly CombatState _combat;
|
||||
private readonly SelectionState _selection;
|
||||
private readonly IWorldSelectionQuery _world;
|
||||
|
||||
public CombatCameraTargetSource(
|
||||
ICombatGameplaySettingsSource settings,
|
||||
CombatState combat,
|
||||
SelectionState selection,
|
||||
IWorldSelectionQuery world)
|
||||
{
|
||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
}
|
||||
|
||||
public Vector3? GetTrackedTargetPoint()
|
||||
{
|
||||
if (!_settings.ViewCombatTarget
|
||||
|| !CombatInputPlanner.SupportsTargetedAttack(_combat.CurrentMode)
|
||||
|| _selection.SelectedObjectId is not uint selected)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _world.GetCombatCameraTargetPoint(selected);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ internal interface ICombatGameplaySettingsSource
|
|||
{
|
||||
bool AutoTarget { get; }
|
||||
bool AutoRepeatAttack { get; }
|
||||
bool ViewCombatTarget { get; }
|
||||
}
|
||||
|
||||
internal sealed class GameplaySettingsState : ICombatGameplaySettingsSource
|
||||
|
|
@ -22,6 +23,7 @@ internal sealed class GameplaySettingsState : ICombatGameplaySettingsSource
|
|||
public GameplaySettings Value { get; set; } = GameplaySettings.Default;
|
||||
public bool AutoTarget => Value.AutoTarget;
|
||||
public bool AutoRepeatAttack => Value.AutoRepeatAttack;
|
||||
public bool ViewCombatTarget => Value.ViewCombatTarget;
|
||||
}
|
||||
|
||||
internal interface ICombatFeedbackSink
|
||||
|
|
|
|||
72
src/AcDream.App/Input/DispatcherCameraInputSource.cs
Normal file
72
src/AcDream.App/Input/DispatcherCameraInputSource.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
internal readonly record struct FlyCameraInput(
|
||||
bool Forward,
|
||||
bool Left,
|
||||
bool Backward,
|
||||
bool Right,
|
||||
bool Up,
|
||||
bool Down,
|
||||
bool Boost);
|
||||
|
||||
internal readonly record struct ChaseCameraAdjustmentInput(
|
||||
bool ZoomIn,
|
||||
bool ZoomOut,
|
||||
bool Raise,
|
||||
bool Lower);
|
||||
|
||||
internal interface ICameraFrameInputSource
|
||||
{
|
||||
bool IsAvailable { get; }
|
||||
FlyCameraInput CaptureFly();
|
||||
ChaseCameraAdjustmentInput CaptureChaseAdjustment();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Samples held camera actions from the same semantic dispatcher used by
|
||||
/// gameplay movement. Binding is a one-time startup edge.
|
||||
/// </summary>
|
||||
internal sealed class DispatcherCameraInputSource : ICameraFrameInputSource
|
||||
{
|
||||
private InputDispatcher? _dispatcher;
|
||||
|
||||
public bool IsAvailable => _dispatcher is not null;
|
||||
|
||||
public void Bind(InputDispatcher dispatcher)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
if (_dispatcher is not null && !ReferenceEquals(_dispatcher, dispatcher))
|
||||
throw new InvalidOperationException(
|
||||
"The camera input source is already bound to another dispatcher.");
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public FlyCameraInput CaptureFly()
|
||||
{
|
||||
if (_dispatcher is not { } dispatcher)
|
||||
return default;
|
||||
|
||||
return new FlyCameraInput(
|
||||
dispatcher.IsActionHeld(InputAction.MovementForward),
|
||||
dispatcher.IsActionHeld(InputAction.MovementTurnLeft),
|
||||
dispatcher.IsActionHeld(InputAction.MovementBackup),
|
||||
dispatcher.IsActionHeld(InputAction.MovementTurnRight),
|
||||
dispatcher.IsActionHeld(InputAction.MovementJump),
|
||||
dispatcher.IsActionHeld(InputAction.AcdreamFlyDown),
|
||||
dispatcher.IsActionHeld(InputAction.MovementRunLock));
|
||||
}
|
||||
|
||||
public ChaseCameraAdjustmentInput CaptureChaseAdjustment()
|
||||
{
|
||||
if (_dispatcher is not { } dispatcher)
|
||||
return default;
|
||||
|
||||
return new ChaseCameraAdjustmentInput(
|
||||
dispatcher.IsActionHeld(InputAction.CameraZoomIn),
|
||||
dispatcher.IsActionHeld(InputAction.CameraZoomOut),
|
||||
dispatcher.IsActionHeld(InputAction.CameraRaise),
|
||||
dispatcher.IsActionHeld(InputAction.CameraLower));
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
|||
_capture = capture;
|
||||
|
||||
public bool AutoRunActive { get; private set; }
|
||||
public bool IsAvailable => _dispatcher is not null;
|
||||
|
||||
public void Bind(InputDispatcher dispatcher)
|
||||
{
|
||||
|
|
|
|||
122
src/AcDream.App/Input/LocalPlayerFrameRuntime.cs
Normal file
122
src/AcDream.App/Input/LocalPlayerFrameRuntime.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
using AcDream.App.Net;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Focused runtime seam for the local player's retail object and command
|
||||
/// phases. The frame controller owns ordering; this seam resolves the current
|
||||
/// session-scoped owners without retaining callbacks into the window host.
|
||||
/// </summary>
|
||||
internal interface ILocalPlayerPresentationRuntime
|
||||
{
|
||||
bool CanPresentPlayer { get; }
|
||||
PlayerMovementController? Controller { get; }
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerFrameRuntime : ILocalPlayerPresentationRuntime
|
||||
{
|
||||
uint ResolveLocalEntityId();
|
||||
void HandleTargeting();
|
||||
bool IsHidden { get; }
|
||||
RetailObjectClockDisposition ObjectClockDisposition { get; }
|
||||
void Project(PlayerMovementController controller, MovementResult movement, bool hidden);
|
||||
void SendPreNetwork(PlayerMovementController controller, MovementResult movement, bool hidden);
|
||||
void SendPostNetwork(PlayerMovementController controller, bool hidden);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production local-player frame runtime composed from canonical typed owners.
|
||||
/// </summary>
|
||||
internal sealed class LiveLocalPlayerFrameRuntime : ILocalPlayerFrameRuntime
|
||||
{
|
||||
private readonly CameraController _camera;
|
||||
private readonly ILocalPlayerModeSource _mode;
|
||||
private readonly ILocalPlayerControllerSource _controller;
|
||||
private readonly IChaseCameraSource _chase;
|
||||
private readonly DispatcherMovementInputSource _input;
|
||||
private readonly IInputCaptureSource _capture;
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly ILocalPlayerPhysicsHostSource _physicsHost;
|
||||
private readonly LocalPlayerProjectionController _projection;
|
||||
private readonly LocalPlayerOutboundController _outbound;
|
||||
private readonly ILiveWorldSessionSource _session;
|
||||
|
||||
public LiveLocalPlayerFrameRuntime(
|
||||
CameraController camera,
|
||||
ILocalPlayerModeSource mode,
|
||||
ILocalPlayerControllerSource controller,
|
||||
IChaseCameraSource chase,
|
||||
DispatcherMovementInputSource input,
|
||||
IInputCaptureSource capture,
|
||||
LiveEntityRuntime liveEntities,
|
||||
ILocalPlayerIdentitySource identity,
|
||||
ILocalPlayerPhysicsHostSource physicsHost,
|
||||
LocalPlayerProjectionController projection,
|
||||
LocalPlayerOutboundController outbound,
|
||||
ILiveWorldSessionSource session)
|
||||
{
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
||||
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||
_chase = chase ?? throw new ArgumentNullException(nameof(chase));
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_capture = capture ?? throw new ArgumentNullException(nameof(capture));
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_physicsHost = physicsHost ?? throw new ArgumentNullException(nameof(physicsHost));
|
||||
_projection = projection ?? throw new ArgumentNullException(nameof(projection));
|
||||
_outbound = outbound ?? throw new ArgumentNullException(nameof(outbound));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
public bool CanPresentPlayer =>
|
||||
!_camera.IsFlyMode
|
||||
&& _mode.IsPlayerMode
|
||||
&& _controller.Controller is not null
|
||||
&& _chase.Legacy is not null
|
||||
&& _input.IsAvailable
|
||||
&& !_capture.DevToolsWantCaptureKeyboard;
|
||||
|
||||
public PlayerMovementController? Controller => _controller.Controller;
|
||||
|
||||
public uint ResolveLocalEntityId() =>
|
||||
_liveEntities.TryGetWorldEntity(_identity.ServerGuid, out var entity)
|
||||
? entity.Id
|
||||
: 0u;
|
||||
|
||||
public void HandleTargeting() => _physicsHost.Host?.HandleTargetting();
|
||||
|
||||
public bool IsHidden => _liveEntities.IsHidden(_identity.ServerGuid);
|
||||
|
||||
public RetailObjectClockDisposition ObjectClockDisposition =>
|
||||
_liveEntities.GetRootObjectClockDisposition(_identity.ServerGuid);
|
||||
|
||||
public void Project(
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden) =>
|
||||
_projection.Project(controller, movement, hidden);
|
||||
|
||||
public void SendPreNetwork(
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden) =>
|
||||
_outbound.SendPreNetworkActions(
|
||||
_session.CurrentSession,
|
||||
controller,
|
||||
movement,
|
||||
hidden);
|
||||
|
||||
public void SendPostNetwork(
|
||||
PlayerMovementController controller,
|
||||
bool hidden) =>
|
||||
_outbound.SendPostNetworkPosition(
|
||||
_session.CurrentSession,
|
||||
controller,
|
||||
hidden);
|
||||
}
|
||||
|
|
@ -1,47 +1,77 @@
|
|||
using AcDream.Core.World;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
internal interface ILocalPlayerProjectionRuntime
|
||||
{
|
||||
WorldEntity? ResolveEntity();
|
||||
int LiveCenterX { get; }
|
||||
int LiveCenterY { get; }
|
||||
void SyncShadow(WorldEntity entity, uint cellId);
|
||||
void Rebucket(uint serverGuid, uint landblockId);
|
||||
bool IsCurrentVisibleProjection(WorldEntity entity);
|
||||
void SuspendShadow(WorldEntity entity);
|
||||
}
|
||||
|
||||
internal sealed class LiveLocalPlayerProjectionRuntime
|
||||
: ILocalPlayerProjectionRuntime
|
||||
{
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly LiveWorldOriginState _origin;
|
||||
private readonly LocalPlayerShadowSynchronizer _shadow;
|
||||
|
||||
public LiveLocalPlayerProjectionRuntime(
|
||||
LiveEntityRuntime liveEntities,
|
||||
ILocalPlayerIdentitySource identity,
|
||||
LiveWorldOriginState origin,
|
||||
LocalPlayerShadowSynchronizer shadow)
|
||||
{
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||
}
|
||||
|
||||
public WorldEntity? ResolveEntity() =>
|
||||
_liveEntities.TryGetWorldEntity(_identity.ServerGuid, out var entity)
|
||||
? entity
|
||||
: null;
|
||||
|
||||
public int LiveCenterX => _origin.CenterX;
|
||||
public int LiveCenterY => _origin.CenterY;
|
||||
|
||||
public void SyncShadow(WorldEntity entity, uint cellId) =>
|
||||
_shadow.Sync(entity, cellId);
|
||||
|
||||
public void Rebucket(uint serverGuid, uint landblockId) =>
|
||||
_liveEntities.RebucketLiveEntity(serverGuid, landblockId);
|
||||
|
||||
public bool IsCurrentVisibleProjection(WorldEntity entity) =>
|
||||
_shadow.IsCurrentVisibleProjection(entity);
|
||||
|
||||
public void SuspendShadow(WorldEntity entity) => _shadow.Suspend(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects the canonical local physics body into world rendering and spatial
|
||||
/// buckets without advancing it.
|
||||
/// </summary>
|
||||
public sealed class LocalPlayerProjectionController
|
||||
{
|
||||
private readonly Func<WorldEntity?> _resolveEntity;
|
||||
private readonly Func<int> _liveCenterX;
|
||||
private readonly Func<int> _liveCenterY;
|
||||
private readonly Action<WorldEntity, uint> _syncShadow;
|
||||
private readonly Action<uint, uint> _rebucket;
|
||||
private readonly Func<WorldEntity, bool> _isCurrentVisibleProjection;
|
||||
private readonly Action<WorldEntity> _suspendShadow;
|
||||
private readonly ILocalPlayerProjectionRuntime _runtime;
|
||||
|
||||
public LocalPlayerProjectionController(
|
||||
Func<WorldEntity?> resolveEntity,
|
||||
Func<int> liveCenterX,
|
||||
Func<int> liveCenterY,
|
||||
Action<WorldEntity, uint> syncShadow,
|
||||
Action<uint, uint> rebucket,
|
||||
Func<WorldEntity, bool> isCurrentVisibleProjection,
|
||||
Action<WorldEntity> suspendShadow)
|
||||
{
|
||||
_resolveEntity = resolveEntity ?? throw new ArgumentNullException(nameof(resolveEntity));
|
||||
_liveCenterX = liveCenterX ?? throw new ArgumentNullException(nameof(liveCenterX));
|
||||
_liveCenterY = liveCenterY ?? throw new ArgumentNullException(nameof(liveCenterY));
|
||||
_syncShadow = syncShadow ?? throw new ArgumentNullException(nameof(syncShadow));
|
||||
_rebucket = rebucket ?? throw new ArgumentNullException(nameof(rebucket));
|
||||
_isCurrentVisibleProjection = isCurrentVisibleProjection
|
||||
?? throw new ArgumentNullException(nameof(isCurrentVisibleProjection));
|
||||
_suspendShadow = suspendShadow
|
||||
?? throw new ArgumentNullException(nameof(suspendShadow));
|
||||
}
|
||||
internal LocalPlayerProjectionController(ILocalPlayerProjectionRuntime runtime) =>
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
|
||||
public void Project(
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden)
|
||||
{
|
||||
WorldEntity? entity = _resolveEntity();
|
||||
WorldEntity? entity = _runtime.ResolveEntity();
|
||||
if (entity is null)
|
||||
return;
|
||||
|
||||
|
|
@ -62,8 +92,8 @@ public sealed class LocalPlayerProjectionController
|
|||
else
|
||||
{
|
||||
System.Numerics.Vector3 position = controller.Position;
|
||||
int landblockX = _liveCenterX() + (int)Math.Floor(position.X / 192f);
|
||||
int landblockY = _liveCenterY() + (int)Math.Floor(position.Y / 192f);
|
||||
int landblockX = _runtime.LiveCenterX + (int)Math.Floor(position.X / 192f);
|
||||
int landblockY = _runtime.LiveCenterY + (int)Math.Floor(position.Y / 192f);
|
||||
currentLandblock = (uint)((landblockX << 24) | (landblockY << 16) | 0xFFFF);
|
||||
}
|
||||
|
||||
|
|
@ -76,15 +106,15 @@ public sealed class LocalPlayerProjectionController
|
|||
// membership, then replaces collision rows only while the object still
|
||||
// owns a loaded cell. Rebucket is a synchronous callback boundary: it
|
||||
// can move this incarnation to pending or replace the GUID entirely.
|
||||
_rebucket(entity.ServerGuid, currentLandblock);
|
||||
_runtime.Rebucket(entity.ServerGuid, currentLandblock);
|
||||
if (hidden
|
||||
|| movement.CellId == 0
|
||||
|| !_isCurrentVisibleProjection(entity))
|
||||
|| !_runtime.IsCurrentVisibleProjection(entity))
|
||||
{
|
||||
_suspendShadow(entity);
|
||||
_runtime.SuspendShadow(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
_syncShadow(entity, movement.CellId);
|
||||
_runtime.SyncShadow(entity, movement.CellId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,13 @@ internal sealed class SilkMouseLookCursor : IMouseLookCursor
|
|||
/// The current chase-camera input targets. GameWindow keeps compatibility
|
||||
/// properties over this one slot until checkpoint F moves camera ownership.
|
||||
/// </summary>
|
||||
internal sealed class ChaseCameraInputState
|
||||
internal interface IChaseCameraSource
|
||||
{
|
||||
ChaseCamera? Legacy { get; }
|
||||
RetailChaseCamera? Retail { get; }
|
||||
}
|
||||
|
||||
internal sealed class ChaseCameraInputState : IChaseCameraSource
|
||||
{
|
||||
public ChaseCamera? Legacy { get; set; }
|
||||
public RetailChaseCamera? Retail { get; set; }
|
||||
|
|
|
|||
|
|
@ -14,17 +14,8 @@ namespace AcDream.App.Input;
|
|||
/// </remarks>
|
||||
internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFramePhase
|
||||
{
|
||||
private readonly Func<bool> _canPresentPlayer;
|
||||
private readonly Func<PlayerMovementController?> _getController;
|
||||
private readonly ILocalPlayerFrameRuntime _runtime;
|
||||
private readonly IMovementInputSource _movementInput;
|
||||
private readonly Func<uint> _resolveLocalEntityId;
|
||||
private readonly Action _handleTargeting;
|
||||
private readonly Func<bool> _isHidden;
|
||||
private readonly Action<PlayerMovementController, MovementResult, bool> _project;
|
||||
private readonly Action<PlayerMovementController, MovementResult, bool> _sendPreNetwork;
|
||||
private readonly Action<PlayerMovementController, bool> _sendPostNetwork;
|
||||
private readonly Func<AcDream.Core.Physics.RetailObjectClockDisposition>
|
||||
_objectClockDisposition;
|
||||
|
||||
private AdvancedFrame? _advancedFrame;
|
||||
|
||||
|
|
@ -51,38 +42,12 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
};
|
||||
|
||||
public RetailLocalPlayerFrameController(
|
||||
Func<bool> canPresentPlayer,
|
||||
Func<PlayerMovementController?> getController,
|
||||
IMovementInputSource movementInput,
|
||||
Func<uint> resolveLocalEntityId,
|
||||
Action handleTargeting,
|
||||
Func<bool> isHidden,
|
||||
Action<PlayerMovementController, MovementResult, bool> project,
|
||||
Action<PlayerMovementController, MovementResult, bool> sendPreNetwork,
|
||||
Action<PlayerMovementController, bool> sendPostNetwork,
|
||||
Func<AcDream.Core.Physics.RetailObjectClockDisposition>?
|
||||
objectClockDisposition = null)
|
||||
ILocalPlayerFrameRuntime runtime,
|
||||
IMovementInputSource movementInput)
|
||||
{
|
||||
_canPresentPlayer = canPresentPlayer
|
||||
?? throw new ArgumentNullException(nameof(canPresentPlayer));
|
||||
_getController = getController
|
||||
?? throw new ArgumentNullException(nameof(getController));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_movementInput = movementInput
|
||||
?? throw new ArgumentNullException(nameof(movementInput));
|
||||
_resolveLocalEntityId = resolveLocalEntityId
|
||||
?? throw new ArgumentNullException(nameof(resolveLocalEntityId));
|
||||
_handleTargeting = handleTargeting
|
||||
?? throw new ArgumentNullException(nameof(handleTargeting));
|
||||
_isHidden = isHidden
|
||||
?? throw new ArgumentNullException(nameof(isHidden));
|
||||
_project = project
|
||||
?? throw new ArgumentNullException(nameof(project));
|
||||
_sendPreNetwork = sendPreNetwork
|
||||
?? throw new ArgumentNullException(nameof(sendPreNetwork));
|
||||
_sendPostNetwork = sendPostNetwork
|
||||
?? throw new ArgumentNullException(nameof(sendPostNetwork));
|
||||
_objectClockDisposition = objectClockDisposition
|
||||
?? (() => AcDream.Core.Physics.RetailObjectClockDisposition.Advance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -92,15 +57,15 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
public void AdvanceBeforeNetwork(float deltaSeconds)
|
||||
{
|
||||
_advancedFrame = null;
|
||||
PlayerMovementController? controller = _getController();
|
||||
if (!_canPresentPlayer() || controller is null)
|
||||
PlayerMovementController? controller = _runtime.Controller;
|
||||
if (!_runtime.CanPresentPlayer || controller is null)
|
||||
return;
|
||||
|
||||
if (!float.IsFinite(deltaSeconds) || deltaSeconds <= 0f)
|
||||
{
|
||||
bool rejectedHidden = _isHidden();
|
||||
bool rejectedHidden = _runtime.IsHidden;
|
||||
MovementResult rejected = controller.CapturePresentationResult();
|
||||
_project(controller, rejected, rejectedHidden);
|
||||
_runtime.Project(controller, rejected, rejectedHidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
rejected,
|
||||
|
|
@ -110,10 +75,10 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
return;
|
||||
}
|
||||
|
||||
controller.LocalEntityId = _resolveLocalEntityId();
|
||||
controller.LocalEntityId = _runtime.ResolveLocalEntityId();
|
||||
|
||||
bool hidden = _isHidden();
|
||||
if (_objectClockDisposition()
|
||||
bool hidden = _runtime.IsHidden;
|
||||
if (_runtime.ObjectClockDisposition
|
||||
is AcDream.Core.Physics.RetailObjectClockDisposition.Suspend)
|
||||
{
|
||||
// CPhysicsObj::update_object returns before advancing time for a
|
||||
|
|
@ -122,7 +87,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
// physics tick that retail did not run.
|
||||
controller.SuspendObjectUpdate(deltaSeconds);
|
||||
MovementResult paused = controller.CapturePresentationResult();
|
||||
_project(controller, paused, hidden);
|
||||
_runtime.Project(controller, paused, hidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
paused,
|
||||
|
|
@ -133,11 +98,14 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
}
|
||||
|
||||
MovementResult movement = hidden
|
||||
? controller.TickHidden(deltaSeconds, _handleTargeting)
|
||||
: controller.Update(deltaSeconds, _movementInput.Capture(), _handleTargeting);
|
||||
? controller.TickHidden(deltaSeconds, _runtime.HandleTargeting)
|
||||
: controller.Update(
|
||||
deltaSeconds,
|
||||
_movementInput.Capture(),
|
||||
_runtime.HandleTargeting);
|
||||
|
||||
_project(controller, movement, hidden);
|
||||
_sendPreNetwork(controller, movement, hidden);
|
||||
_runtime.Project(controller, movement, hidden);
|
||||
_runtime.SendPreNetwork(controller, movement, hidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
movement,
|
||||
|
|
@ -153,18 +121,18 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
/// </summary>
|
||||
public void RunPostNetworkCommandPhase()
|
||||
{
|
||||
PlayerMovementController? controller = _getController();
|
||||
if (!_canPresentPlayer() || controller is null)
|
||||
PlayerMovementController? controller = _runtime.Controller;
|
||||
if (!_runtime.CanPresentPlayer || controller is null)
|
||||
return;
|
||||
|
||||
bool hidden = _isHidden();
|
||||
bool hidden = _runtime.IsHidden;
|
||||
if (_advancedFrame is { } advanced
|
||||
&& ReferenceEquals(advanced.Controller, controller))
|
||||
{
|
||||
MovementResult movement = RefreshSpatialFields(
|
||||
advanced.Movement,
|
||||
controller);
|
||||
_project(controller, movement, hidden);
|
||||
_runtime.Project(controller, movement, hidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
movement,
|
||||
|
|
@ -176,7 +144,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
return;
|
||||
}
|
||||
|
||||
_sendPostNetwork(controller, hidden);
|
||||
_runtime.SendPostNetwork(controller, hidden);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -187,11 +155,11 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
public bool TryGetPresentationAfterNetwork(out PresentationFrame frame)
|
||||
{
|
||||
frame = default;
|
||||
PlayerMovementController? controller = _getController();
|
||||
if (!_canPresentPlayer() || controller is null)
|
||||
PlayerMovementController? controller = _runtime.Controller;
|
||||
if (!_runtime.CanPresentPlayer || controller is null)
|
||||
return false;
|
||||
|
||||
bool hidden = _isHidden();
|
||||
bool hidden = _runtime.IsHidden;
|
||||
if (_advancedFrame is { } advanced
|
||||
&& ReferenceEquals(advanced.Controller, controller))
|
||||
{
|
||||
|
|
@ -206,7 +174,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
}
|
||||
|
||||
MovementResult initial = controller.CapturePresentationResult();
|
||||
_project(controller, initial, hidden);
|
||||
_runtime.Project(controller, initial, hidden);
|
||||
frame = new PresentationFrame(initial, hidden, AdvancedBeforeNetwork: false);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ internal interface IWorldSelectionQuery
|
|||
bool IsUseable(uint serverGuid);
|
||||
bool IsPickupable(uint serverGuid);
|
||||
bool TryGetApproach(uint serverGuid, out InteractionApproach approach);
|
||||
Vector3? GetCombatCameraTargetPoint(uint serverGuid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
114
src/AcDream.App/Rendering/CameraFrameController.cs
Normal file
114
src/AcDream.App/Rendering/CameraFrameController.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.Core.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the per-update fly/chase camera publication tail.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The local object phase normally publishes the player before inbound
|
||||
/// traffic. When the player is first created by that inbound pass, the local
|
||||
/// frame publishes its initial root here and the spatial reconciler refreshes
|
||||
/// child/effect anchors before either chase camera samples it.
|
||||
/// </remarks>
|
||||
internal sealed class CameraFrameController : ICameraFramePhase
|
||||
{
|
||||
private readonly CameraController _camera;
|
||||
private readonly IInputCaptureSource _capture;
|
||||
private readonly ICameraFrameInputSource _input;
|
||||
private readonly ILocalPlayerPresentationRuntime _player;
|
||||
private readonly IChaseCameraSource _chase;
|
||||
private readonly RetailLocalPlayerFrameController _localFrame;
|
||||
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
||||
private readonly ICombatCameraTargetSource _combatTarget;
|
||||
|
||||
public CameraFrameController(
|
||||
CameraController camera,
|
||||
IInputCaptureSource capture,
|
||||
ICameraFrameInputSource input,
|
||||
ILocalPlayerPresentationRuntime player,
|
||||
IChaseCameraSource chase,
|
||||
RetailLocalPlayerFrameController localFrame,
|
||||
ILiveSpatialReconcilePhase spatialReconciler,
|
||||
ICombatCameraTargetSource combatTarget)
|
||||
{
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_capture = capture ?? throw new ArgumentNullException(nameof(capture));
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_player = player ?? throw new ArgumentNullException(nameof(player));
|
||||
_chase = chase ?? throw new ArgumentNullException(nameof(chase));
|
||||
_localFrame = localFrame ?? throw new ArgumentNullException(nameof(localFrame));
|
||||
_spatialReconciler = spatialReconciler
|
||||
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
||||
_combatTarget = combatTarget ?? throw new ArgumentNullException(nameof(combatTarget));
|
||||
}
|
||||
|
||||
public void Tick(UpdateFrameTiming timing)
|
||||
{
|
||||
if (_capture.DevToolsWantCaptureKeyboard || !_input.IsAvailable)
|
||||
return;
|
||||
|
||||
if (_camera.IsFlyMode)
|
||||
{
|
||||
FlyCameraInput input = _input.CaptureFly();
|
||||
_camera.Fly.Update(
|
||||
timing.SimulationDeltaSeconds,
|
||||
input.Forward,
|
||||
input.Left,
|
||||
input.Backward,
|
||||
input.Right,
|
||||
input.Up,
|
||||
input.Down,
|
||||
input.Boost);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerMovementController? controller = _player.Controller;
|
||||
ChaseCamera? legacy = _chase.Legacy;
|
||||
RetailChaseCamera? retail = _chase.Retail;
|
||||
if (!_player.CanPresentPlayer || controller is null || legacy is null)
|
||||
return;
|
||||
|
||||
if (CameraDiagnostics.UseRetailChaseCamera && retail is not null)
|
||||
{
|
||||
ChaseCameraAdjustmentInput input = _input.CaptureChaseAdjustment();
|
||||
float adjustment = CameraDiagnostics.CameraAdjustmentSpeed
|
||||
* timing.SimulationDeltaSecondsSingle;
|
||||
if (input.ZoomIn)
|
||||
retail.AdjustDistance(-adjustment);
|
||||
if (input.ZoomOut)
|
||||
retail.AdjustDistance(+adjustment);
|
||||
if (input.Raise)
|
||||
retail.AdjustPitch(+adjustment * 0.02f);
|
||||
if (input.Lower)
|
||||
retail.AdjustPitch(-adjustment * 0.02f);
|
||||
}
|
||||
|
||||
if (!_localFrame.TryGetPresentationAfterNetwork(out var playerFrame))
|
||||
return;
|
||||
|
||||
if (!playerFrame.AdvancedBeforeNetwork)
|
||||
_spatialReconciler.Reconcile();
|
||||
|
||||
MovementResult result = playerFrame.Movement;
|
||||
legacy.Update(
|
||||
result.RenderPosition,
|
||||
controller.Yaw,
|
||||
isOnGround: result.IsOnGround,
|
||||
dt: timing.SimulationDeltaSecondsSingle);
|
||||
|
||||
retail?.Update(
|
||||
result.RenderPosition,
|
||||
controller.Yaw,
|
||||
playerVelocity: controller.BodyVelocity,
|
||||
isOnGround: result.IsOnGround,
|
||||
contactPlaneNormal: controller.ContactPlane.Normal,
|
||||
dt: timing.SimulationDeltaSecondsSingle,
|
||||
cellId: controller.CellId,
|
||||
selfEntityId: controller.LocalEntityId,
|
||||
trackedTargetPoint: _combatTarget.GetTrackedTargetPoint());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Combat;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.App.Tests.Combat;
|
||||
|
||||
public sealed class CombatCameraTargetSourceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetTrackedTargetPoint_RequiresOptionTargetedModeAndSelection()
|
||||
{
|
||||
const uint target = 0x70000001u;
|
||||
Vector3 point = new(10f, 20f, 30f);
|
||||
var settings = new GameplaySettingsState();
|
||||
var combat = new CombatState();
|
||||
var selection = new SelectionState();
|
||||
var world = new TargetQuery(point);
|
||||
var source = new CombatCameraTargetSource(
|
||||
settings,
|
||||
combat,
|
||||
selection,
|
||||
world);
|
||||
|
||||
Assert.Null(source.GetTrackedTargetPoint());
|
||||
|
||||
settings.Value = settings.Value with { ViewCombatTarget = true };
|
||||
combat.SetCombatMode(CombatMode.Melee);
|
||||
selection.Select(target, SelectionChangeSource.World);
|
||||
|
||||
Assert.Equal(point, source.GetTrackedTargetPoint());
|
||||
Assert.Equal(target, world.LastGuid);
|
||||
|
||||
combat.SetCombatMode(CombatMode.Magic);
|
||||
Assert.Null(source.GetTrackedTargetPoint());
|
||||
}
|
||||
|
||||
private sealed class TargetQuery(Vector3 point) : IWorldSelectionQuery
|
||||
{
|
||||
public uint LastGuid { get; private set; }
|
||||
public Vector3? GetCombatCameraTargetPoint(uint serverGuid)
|
||||
{
|
||||
LastGuid = serverGuid;
|
||||
return point;
|
||||
}
|
||||
|
||||
public uint? PickAtCursor(bool includeSelf) => null;
|
||||
public uint? PickAt(float mouseX, float mouseY, bool includeSelf) => null;
|
||||
public void BeginLightingPulse(uint serverGuid) { }
|
||||
public bool TryCaptureIdentity(uint serverGuid, out uint localEntityId)
|
||||
{
|
||||
localEntityId = 0;
|
||||
return false;
|
||||
}
|
||||
public bool IsCurrent(uint serverGuid, uint localEntityId) => false;
|
||||
public string Describe(uint serverGuid) => string.Empty;
|
||||
public bool IsCreature(uint serverGuid) => false;
|
||||
public bool IsHostileMonster(uint serverGuid) => false;
|
||||
public ClosestCombatTarget? FindClosestHostileMonster() => null;
|
||||
public bool IsUseable(uint serverGuid) => false;
|
||||
public bool IsPickupable(uint serverGuid) => false;
|
||||
public bool TryGetApproach(uint serverGuid, out InteractionApproach approach)
|
||||
{
|
||||
approach = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -120,6 +120,7 @@ public sealed class LiveCombatAttackOperationsTests
|
|||
{
|
||||
public bool AutoTarget { get; set; }
|
||||
public bool AutoRepeatAttack { get; set; }
|
||||
public bool ViewCombatTarget { get; set; }
|
||||
}
|
||||
|
||||
private sealed class FakeLiveSource : ILiveInWorldSource, ILiveWorldSessionSource
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var projection = new LocalPlayerProjectionController(
|
||||
var projection = new LocalPlayerProjectionController(new TestProjectionRuntime(
|
||||
() => entity,
|
||||
() => 1,
|
||||
() => 1,
|
||||
(_, _) => { },
|
||||
(_, _) => { },
|
||||
_ => true,
|
||||
_ => { });
|
||||
_ => { }));
|
||||
|
||||
projection.Project(
|
||||
movement,
|
||||
|
|
@ -64,14 +64,14 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var order = new List<string>();
|
||||
var projection = new LocalPlayerProjectionController(
|
||||
var projection = new LocalPlayerProjectionController(new TestProjectionRuntime(
|
||||
() => entity,
|
||||
() => 2,
|
||||
() => 2,
|
||||
(_, _) => order.Add("shadow"),
|
||||
(_, _) => order.Add("rebucket"),
|
||||
_ => false,
|
||||
_ => order.Add("suspend"));
|
||||
_ => order.Add("suspend")));
|
||||
|
||||
projection.Project(
|
||||
movement,
|
||||
|
|
@ -100,7 +100,7 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
};
|
||||
bool visible = false;
|
||||
var order = new List<string>();
|
||||
var projection = new LocalPlayerProjectionController(
|
||||
var projection = new LocalPlayerProjectionController(new TestProjectionRuntime(
|
||||
() => entity,
|
||||
() => 2,
|
||||
() => 2,
|
||||
|
|
@ -111,7 +111,7 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
visible = true;
|
||||
},
|
||||
_ => visible,
|
||||
_ => order.Add("suspend"));
|
||||
_ => order.Add("suspend")));
|
||||
|
||||
projection.Project(
|
||||
movement,
|
||||
|
|
@ -120,4 +120,25 @@ public sealed class LocalPlayerProjectionControllerTests
|
|||
|
||||
Assert.Equal(["rebucket", "shadow"], order);
|
||||
}
|
||||
|
||||
private sealed class TestProjectionRuntime(
|
||||
Func<WorldEntity?> resolveEntity,
|
||||
Func<int> liveCenterX,
|
||||
Func<int> liveCenterY,
|
||||
Action<WorldEntity, uint> syncShadow,
|
||||
Action<uint, uint> rebucket,
|
||||
Func<WorldEntity, bool> isCurrentVisibleProjection,
|
||||
Action<WorldEntity> suspendShadow) : ILocalPlayerProjectionRuntime
|
||||
{
|
||||
public WorldEntity? ResolveEntity() => resolveEntity();
|
||||
public int LiveCenterX => liveCenterX();
|
||||
public int LiveCenterY => liveCenterY();
|
||||
public void SyncShadow(WorldEntity entity, uint cellId) =>
|
||||
syncShadow(entity, cellId);
|
||||
public void Rebucket(uint serverGuid, uint landblockId) =>
|
||||
rebucket(serverGuid, landblockId);
|
||||
public bool IsCurrentVisibleProjection(WorldEntity entity) =>
|
||||
isCurrentVisibleProjection(entity);
|
||||
public void SuspendShadow(WorldEntity entity) => suspendShadow(entity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
var calls = new List<string>();
|
||||
var preNetwork = new List<(PlayerState State, MovementResult Movement)>();
|
||||
var postNetwork = new List<PlayerState>();
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
var local = CreateFrame(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
movementInput: Input(() => new MovementInput(Forward: true)),
|
||||
|
|
@ -79,7 +79,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
int projections = 0;
|
||||
int preNetwork = 0;
|
||||
int postNetwork = 0;
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
var local = CreateFrame(
|
||||
canPresentPlayer: () => controller is not null,
|
||||
getController: () => controller,
|
||||
movementInput: Input(() => new MovementInput(Forward: true)),
|
||||
|
|
@ -109,7 +109,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
{
|
||||
PlayerMovementController controller = CreateController();
|
||||
Vector3 projectedRoot = default;
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
var local = CreateFrame(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
movementInput: Input(() => new MovementInput()),
|
||||
|
|
@ -140,7 +140,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
PlayerMovementController controller = CreateController();
|
||||
Vector3 initial = controller.Position;
|
||||
Vector3 positionSeenByTargetManager = new(float.NaN);
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
var local = CreateFrame(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
movementInput: Input(() => new MovementInput(Forward: true)),
|
||||
|
|
@ -166,7 +166,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
int projections = 0;
|
||||
int preNetwork = 0;
|
||||
int postNetwork = 0;
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
var local = CreateFrame(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
movementInput: Input(() =>
|
||||
|
|
@ -201,7 +201,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
public void HiddenPoseIsDirtyOnlyWhenACompleteObjectQuantumRuns()
|
||||
{
|
||||
PlayerMovementController controller = CreateController();
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
var local = CreateFrame(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
movementInput: Input(() => new MovementInput()),
|
||||
|
|
@ -228,7 +228,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
int projections = 0;
|
||||
int preNetwork = 0;
|
||||
int postNetwork = 0;
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
var local = CreateFrame(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
movementInput: Input(() =>
|
||||
|
|
@ -297,6 +297,65 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
private static IMovementInputSource Input(Func<MovementInput> capture) =>
|
||||
new TestMovementInputSource(capture);
|
||||
|
||||
private static RetailLocalPlayerFrameController CreateFrame(
|
||||
Func<bool> canPresentPlayer,
|
||||
Func<PlayerMovementController?> getController,
|
||||
IMovementInputSource movementInput,
|
||||
Func<uint> resolveLocalEntityId,
|
||||
Action handleTargeting,
|
||||
Func<bool> isHidden,
|
||||
Action<PlayerMovementController, MovementResult, bool> project,
|
||||
Action<PlayerMovementController, MovementResult, bool> sendPreNetwork,
|
||||
Action<PlayerMovementController, bool> sendPostNetwork,
|
||||
Func<RetailObjectClockDisposition>? objectClockDisposition = null) =>
|
||||
new(
|
||||
new TestLocalPlayerFrameRuntime(
|
||||
canPresentPlayer,
|
||||
getController,
|
||||
resolveLocalEntityId,
|
||||
handleTargeting,
|
||||
isHidden,
|
||||
project,
|
||||
sendPreNetwork,
|
||||
sendPostNetwork,
|
||||
objectClockDisposition),
|
||||
movementInput);
|
||||
|
||||
private sealed class TestLocalPlayerFrameRuntime(
|
||||
Func<bool> canPresentPlayer,
|
||||
Func<PlayerMovementController?> getController,
|
||||
Func<uint> resolveLocalEntityId,
|
||||
Action handleTargeting,
|
||||
Func<bool> isHidden,
|
||||
Action<PlayerMovementController, MovementResult, bool> project,
|
||||
Action<PlayerMovementController, MovementResult, bool> sendPreNetwork,
|
||||
Action<PlayerMovementController, bool> sendPostNetwork,
|
||||
Func<RetailObjectClockDisposition>? objectClockDisposition)
|
||||
: ILocalPlayerFrameRuntime
|
||||
{
|
||||
public bool CanPresentPlayer => canPresentPlayer();
|
||||
public PlayerMovementController? Controller => getController();
|
||||
public uint ResolveLocalEntityId() => resolveLocalEntityId();
|
||||
public void HandleTargeting() => handleTargeting();
|
||||
public bool IsHidden => isHidden();
|
||||
public RetailObjectClockDisposition ObjectClockDisposition =>
|
||||
objectClockDisposition?.Invoke() ?? RetailObjectClockDisposition.Advance;
|
||||
|
||||
public void Project(
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden) => project(controller, movement, hidden);
|
||||
|
||||
public void SendPreNetwork(
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden) => sendPreNetwork(controller, movement, hidden);
|
||||
|
||||
public void SendPostNetwork(
|
||||
PlayerMovementController controller,
|
||||
bool hidden) => sendPostNetwork(controller, hidden);
|
||||
}
|
||||
|
||||
private sealed class TestMovementInputSource(Func<MovementInput> capture)
|
||||
: IMovementInputSource
|
||||
{
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
public ClosestCombatTarget? FindClosestHostileMonster() => Closest;
|
||||
public bool IsUseable(uint serverGuid) => Useable;
|
||||
public bool IsPickupable(uint serverGuid) => Pickupable;
|
||||
public Vector3? GetCombatCameraTargetPoint(uint serverGuid) => null;
|
||||
|
||||
public bool TryGetApproach(uint serverGuid, out InteractionApproach approach)
|
||||
{
|
||||
|
|
|
|||
231
tests/AcDream.App.Tests/Rendering/CameraFrameControllerTests.cs
Normal file
231
tests/AcDream.App.Tests/Rendering/CameraFrameControllerTests.cs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class CameraFrameControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void FlyMode_UsesOneSemanticHeldInputSnapshot()
|
||||
{
|
||||
CameraController camera = CreateCamera();
|
||||
camera.ToggleFly();
|
||||
Vector3 start = camera.Fly.Position;
|
||||
var input = new InputSource
|
||||
{
|
||||
Fly = new FlyCameraInput(
|
||||
Forward: true,
|
||||
Left: false,
|
||||
Backward: false,
|
||||
Right: false,
|
||||
Up: true,
|
||||
Down: false,
|
||||
Boost: false),
|
||||
};
|
||||
CameraFrameController frame = CreateFrame(camera, input: input);
|
||||
|
||||
frame.Tick(new UpdateFrameTiming(0.5, 0.5f, 0.5));
|
||||
|
||||
Assert.Equal(1, input.FlyCaptures);
|
||||
Assert.InRange(camera.Fly.Position.Y - start.Y, 5.999f, 6.001f);
|
||||
Assert.InRange(camera.Fly.Position.Z - start.Z, 5.999f, 6.001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DevToolsKeyboardCapture_PausesFlyCamera()
|
||||
{
|
||||
CameraController camera = CreateCamera();
|
||||
camera.ToggleFly();
|
||||
Vector3 start = camera.Fly.Position;
|
||||
var input = new InputSource
|
||||
{
|
||||
Fly = new FlyCameraInput(true, false, false, false, false, false, false),
|
||||
};
|
||||
CameraFrameController frame = CreateFrame(
|
||||
camera,
|
||||
capture: new CaptureSource { DevToolsKeyboard = true },
|
||||
input: input);
|
||||
|
||||
frame.Tick(new UpdateFrameTiming(1.0, 1f, 1.0));
|
||||
|
||||
Assert.Equal(start, camera.Fly.Position);
|
||||
Assert.Equal(0, input.FlyCaptures);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InboundCreatedPlayer_ProjectsThenReconcilesBeforeCameraPublication()
|
||||
{
|
||||
PlayerMovementController controller = CreatePlayer();
|
||||
var calls = new List<string>();
|
||||
var runtime = new PlayerRuntime(controller, calls);
|
||||
var localFrame = new RetailLocalPlayerFrameController(
|
||||
runtime,
|
||||
new StillMovementInput());
|
||||
CameraController camera = CreateCamera();
|
||||
var legacy = new ChaseCamera();
|
||||
var retail = new RetailChaseCamera();
|
||||
camera.EnterChaseMode(legacy, retail);
|
||||
var chase = new ChaseSource(legacy, retail);
|
||||
var reconciler = new Reconciler(calls);
|
||||
var frame = new CameraFrameController(
|
||||
camera,
|
||||
new CaptureSource(),
|
||||
new InputSource(),
|
||||
runtime,
|
||||
chase,
|
||||
localFrame,
|
||||
reconciler,
|
||||
new CombatTargetSource());
|
||||
|
||||
frame.Tick(new UpdateFrameTiming(1.0 / 60.0, 1f / 60f, 1.0));
|
||||
|
||||
Assert.Equal(["project", "reconcile"], calls);
|
||||
Assert.NotEqual(Vector3.Zero, legacy.Position);
|
||||
Assert.NotEqual(Vector3.Zero, retail.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreNetworkAdvancedPlayer_DoesNotRunTheInboundCreationReconcile()
|
||||
{
|
||||
PlayerMovementController controller = CreatePlayer();
|
||||
var calls = new List<string>();
|
||||
var runtime = new PlayerRuntime(controller, calls);
|
||||
var localFrame = new RetailLocalPlayerFrameController(
|
||||
runtime,
|
||||
new StillMovementInput());
|
||||
localFrame.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum);
|
||||
calls.Clear();
|
||||
CameraController camera = CreateCamera();
|
||||
var legacy = new ChaseCamera();
|
||||
var retail = new RetailChaseCamera();
|
||||
camera.EnterChaseMode(legacy, retail);
|
||||
var frame = new CameraFrameController(
|
||||
camera,
|
||||
new CaptureSource(),
|
||||
new InputSource(),
|
||||
runtime,
|
||||
new ChaseSource(legacy, retail),
|
||||
localFrame,
|
||||
new Reconciler(calls),
|
||||
new CombatTargetSource());
|
||||
|
||||
frame.Tick(new UpdateFrameTiming(1.0 / 60.0, 1f / 60f, 1.0));
|
||||
|
||||
Assert.Empty(calls);
|
||||
}
|
||||
|
||||
private static CameraFrameController CreateFrame(
|
||||
CameraController camera,
|
||||
CaptureSource? capture = null,
|
||||
InputSource? input = null)
|
||||
{
|
||||
var runtime = new PlayerRuntime(null, []);
|
||||
var localFrame = new RetailLocalPlayerFrameController(
|
||||
runtime,
|
||||
new StillMovementInput());
|
||||
return new CameraFrameController(
|
||||
camera,
|
||||
capture ?? new CaptureSource(),
|
||||
input ?? new InputSource(),
|
||||
runtime,
|
||||
new ChaseSource(null, null),
|
||||
localFrame,
|
||||
new Reconciler([]),
|
||||
new CombatTargetSource());
|
||||
}
|
||||
|
||||
private static CameraController CreateCamera() =>
|
||||
new(new OrbitCamera(), new FlyCamera());
|
||||
|
||||
private static PlayerMovementController CreatePlayer()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var heights = new byte[81];
|
||||
Array.Fill(heights, (byte)50);
|
||||
var heightTable = new float[256];
|
||||
for (int i = 0; i < heightTable.Length; i++)
|
||||
heightTable[i] = i;
|
||||
engine.AddLandblock(
|
||||
0xA9B4FFFFu,
|
||||
new TerrainSurface(heights, heightTable),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f);
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private sealed class CaptureSource : IInputCaptureSource
|
||||
{
|
||||
public bool DevToolsKeyboard { get; set; }
|
||||
public bool WantCaptureMouse => false;
|
||||
public bool WantCaptureKeyboard => DevToolsKeyboard;
|
||||
public bool DevToolsWantCaptureKeyboard => DevToolsKeyboard;
|
||||
}
|
||||
|
||||
private sealed class InputSource : ICameraFrameInputSource
|
||||
{
|
||||
public bool IsAvailable { get; set; } = true;
|
||||
public FlyCameraInput Fly { get; set; }
|
||||
public ChaseCameraAdjustmentInput Chase { get; set; }
|
||||
public int FlyCaptures { get; private set; }
|
||||
public FlyCameraInput CaptureFly()
|
||||
{
|
||||
FlyCaptures++;
|
||||
return Fly;
|
||||
}
|
||||
public ChaseCameraAdjustmentInput CaptureChaseAdjustment() => Chase;
|
||||
}
|
||||
|
||||
private sealed class ChaseSource(
|
||||
ChaseCamera? legacy,
|
||||
RetailChaseCamera? retail) : IChaseCameraSource
|
||||
{
|
||||
public ChaseCamera? Legacy => legacy;
|
||||
public RetailChaseCamera? Retail => retail;
|
||||
}
|
||||
|
||||
private sealed class StillMovementInput : IMovementInputSource
|
||||
{
|
||||
public MovementInput Capture() => default;
|
||||
}
|
||||
|
||||
private sealed class PlayerRuntime(
|
||||
PlayerMovementController? controller,
|
||||
List<string> calls) : ILocalPlayerFrameRuntime
|
||||
{
|
||||
public bool CanPresentPlayer { get; set; } = controller is not null;
|
||||
public PlayerMovementController? Controller => controller;
|
||||
public uint ResolveLocalEntityId() => 7u;
|
||||
public void HandleTargeting() { }
|
||||
public bool IsHidden => false;
|
||||
public RetailObjectClockDisposition ObjectClockDisposition =>
|
||||
RetailObjectClockDisposition.Advance;
|
||||
public void Project(
|
||||
PlayerMovementController owner,
|
||||
MovementResult movement,
|
||||
bool hidden) => calls.Add("project");
|
||||
public void SendPreNetwork(
|
||||
PlayerMovementController owner,
|
||||
MovementResult movement,
|
||||
bool hidden) { }
|
||||
public void SendPostNetwork(PlayerMovementController owner, bool hidden) { }
|
||||
}
|
||||
|
||||
private sealed class Reconciler(List<string> calls)
|
||||
: ILiveSpatialReconcilePhase
|
||||
{
|
||||
public void Reconcile() => calls.Add("reconcile");
|
||||
}
|
||||
|
||||
private sealed class CombatTargetSource : ICombatCameraTargetSource
|
||||
{
|
||||
public Vector3? GetTrackedTargetPoint() => null;
|
||||
}
|
||||
}
|
||||
|
|
@ -619,6 +619,64 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraAndLocalPlayerFrameOwnersUseTypedSeamsWithoutWindowCallbacks()
|
||||
{
|
||||
Type[] owners =
|
||||
[
|
||||
typeof(AcDream.App.Rendering.CameraFrameController),
|
||||
typeof(AcDream.App.Input.RetailLocalPlayerFrameController),
|
||||
typeof(AcDream.App.Input.LocalPlayerProjectionController),
|
||||
typeof(AcDream.App.Input.LiveLocalPlayerFrameRuntime),
|
||||
typeof(AcDream.App.Input.LiveLocalPlayerProjectionRuntime),
|
||||
typeof(AcDream.App.Combat.CombatCameraTargetSource),
|
||||
];
|
||||
|
||||
foreach (Type owner in owners)
|
||||
{
|
||||
FieldInfo[] fields = owner.GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
||||
Assert.DoesNotContain(
|
||||
fields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
}
|
||||
|
||||
string root = FindRepoRoot();
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
Assert.Equal(1, CountOccurrences(source, "_cameraFrame.Tick(frameTiming);"));
|
||||
Assert.DoesNotContain("CanAdvanceLocalPlayer", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("GetCombatCameraTargetPoint()", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_cameraController.Fly.Update(", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_localPlayerFrame.TryGetPresentationAfterNetwork", source,
|
||||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_localPlayerTeleport!.Tick(frameDelta);",
|
||||
"_playerModeAutoEntry?.TryEnter();",
|
||||
"_cameraFrame.Tick(frameTiming);");
|
||||
|
||||
string cameraSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"CameraFrameController.cs"));
|
||||
AssertAppearsInOrder(
|
||||
cameraSource,
|
||||
"_localFrame.TryGetPresentationAfterNetwork",
|
||||
"_spatialReconciler.Reconcile();",
|
||||
"legacy.Update(",
|
||||
"retail?.Update(");
|
||||
}
|
||||
|
||||
private static UpdateFrameOrchestrator Create(
|
||||
List<string> calls,
|
||||
RecordingTeardown? teardown = null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue