refactor(runtime): extract the live object frame
This commit is contained in:
parent
99a3e819c4
commit
4e4aac2c5a
27 changed files with 1217 additions and 371 deletions
|
|
@ -15,7 +15,7 @@ never hidden behind a retry, delay, suppression flag, or reordered callback.
|
||||||
|
|
||||||
- [x] A — freeze the complete production phase graph and introduce the typed
|
- [x] A — freeze the complete production phase graph and introduce the typed
|
||||||
orchestration contract plus structural/order guards.
|
orchestration contract plus structural/order guards.
|
||||||
- [ ] B — extract the pre-network live-object presentation phase and the
|
- [x] B — extract the pre-network live-object presentation phase and the
|
||||||
non-advancing post-network spatial reconciler; remove callbacks into
|
non-advancing post-network spatial reconciler; remove callbacks into
|
||||||
`GameWindow` from `RetailLiveFrameCoordinator`.
|
`GameWindow` from `RetailLiveFrameCoordinator`.
|
||||||
- [ ] C — extract streaming-origin convergence, observer selection, dungeon
|
- [ ] C — extract streaming-origin convergence, observer selection, dungeon
|
||||||
|
|
@ -42,6 +42,14 @@ oracle, exact teardown and invalid-delta tests, and guards against a transitive
|
||||||
18 focused Release tests and the full 2,709-test App project pass, and all
|
18 focused Release tests and the full 2,709-test App project pass, and all
|
||||||
three corrected-diff reviews are clean.
|
three corrected-diff reviews are clean.
|
||||||
|
|
||||||
|
Checkpoint B moved the complete local/ordinary/static/effect body into
|
||||||
|
`LiveObjectFrameController`, made `RetailLiveFrameCoordinator`'s
|
||||||
|
object → inbound batch → command → spatial-reconcile barrier fully typed,
|
||||||
|
and removed animation/runtime/projectile back-references to `GameWindow`.
|
||||||
|
`GameWindow.cs` is now 8,716 raw lines. The corrected diff passes 239 focused
|
||||||
|
Release tests, the 2,732-test App suite, and the full 7,090-test Release suite
|
||||||
|
(5 fixture/environment skips); all three independent reviews are clean.
|
||||||
|
|
||||||
## 1. Outcome and non-goals
|
## 1. Outcome and non-goals
|
||||||
|
|
||||||
At slice exit, `GameWindow.OnUpdate` starts the profiler scope and delegates one
|
At slice exit, `GameWindow.OnUpdate` starts the profiler scope and delegates one
|
||||||
|
|
@ -352,6 +360,13 @@ multiple ordinary/static owners, invalid delta, and reentrant inbound work;
|
||||||
the one published PES clock remains visible from hook capture through the
|
the one published PES clock remains visible from hook capture through the
|
||||||
script tick.
|
script tick.
|
||||||
|
|
||||||
|
Checkpoint B removes the coordinator, animation presenter/scheduler, runtime
|
||||||
|
view, static-root, and projectile callbacks into `GameWindow`. The callback
|
||||||
|
composition still retained by `RetailLocalPlayerFrameController` is explicitly
|
||||||
|
deferred: checkpoint D owns its input/capture sources and checkpoint F owns its
|
||||||
|
mutable player-mode/controller/session sources. A structural test permits that
|
||||||
|
one named legacy owner only; D/F must remove the exception before final cutover.
|
||||||
|
|
||||||
### C — streaming frame
|
### C — streaming frame
|
||||||
|
|
||||||
- extract readiness/recenter advancement, observer selection, dungeon gate,
|
- extract readiness/recenter advancement, observer selection, dungeon gate,
|
||||||
|
|
|
||||||
30
src/AcDream.App/Input/LocalPlayerRuntimeState.cs
Normal file
30
src/AcDream.App/Input/LocalPlayerRuntimeState.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
|
internal interface ILocalPlayerIdentitySource
|
||||||
|
{
|
||||||
|
uint ServerGuid { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Session-scoped identity slot shared by focused update owners.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LocalPlayerIdentityState : ILocalPlayerIdentitySource
|
||||||
|
{
|
||||||
|
public uint ServerGuid { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ILocalPlayerControllerSource
|
||||||
|
{
|
||||||
|
PlayerMovementController? Controller { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The one mutable local movement-controller slot. Player-mode lifecycle owns
|
||||||
|
/// assignment; update and presentation owners receive the read-only seam.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LocalPlayerControllerSlot : ILocalPlayerControllerSource
|
||||||
|
{
|
||||||
|
public PlayerMovementController? Controller { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using AcDream.App.Update;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -10,7 +12,7 @@ namespace AcDream.App.Input;
|
||||||
/// state. The cached input result is presentation-only after the barrier: its
|
/// state. The cached input result is presentation-only after the barrier: its
|
||||||
/// one-shot commands are never replayed against a later authoritative cell.
|
/// one-shot commands are never replayed against a later authoritative cell.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class RetailLocalPlayerFrameController
|
internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFramePhase
|
||||||
{
|
{
|
||||||
private readonly Func<bool> _canPresentPlayer;
|
private readonly Func<bool> _canPresentPlayer;
|
||||||
private readonly Func<PlayerMovementController?> _getController;
|
private readonly Func<PlayerMovementController?> _getController;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.App.Update;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Net;
|
namespace AcDream.App.Net;
|
||||||
|
|
@ -157,7 +158,7 @@ internal sealed class ProductionLiveSessionOperations : ILiveSessionOperations
|
||||||
/// publication, exact-generation Tick, graceful replacement, and convergent
|
/// publication, exact-generation Tick, graceful replacement, and convergent
|
||||||
/// teardown. Retail wire behavior remains inside WorldSession.
|
/// teardown. Retail wire behavior remains inside WorldSession.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LiveSessionController : IDisposable
|
internal sealed class LiveSessionController : IDisposable, ILiveSessionFramePhase
|
||||||
{
|
{
|
||||||
private sealed class SessionScope(
|
private sealed class SessionScope(
|
||||||
WorldSession session,
|
WorldSession session,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
@ -9,6 +11,29 @@ using RemoteMotion = AcDream.App.Physics.RemoteMotion;
|
||||||
|
|
||||||
namespace AcDream.App.Physics;
|
namespace AcDream.App.Physics;
|
||||||
|
|
||||||
|
internal interface IProjectileSetupResolver
|
||||||
|
{
|
||||||
|
Setup? Resolve(uint setupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DatProjectileSetupResolver : IProjectileSetupResolver
|
||||||
|
{
|
||||||
|
private readonly IDatReaderWriter _dats;
|
||||||
|
private readonly object _datLock;
|
||||||
|
|
||||||
|
public DatProjectileSetupResolver(IDatReaderWriter dats, object datLock)
|
||||||
|
{
|
||||||
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
|
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Setup? Resolve(uint setupId)
|
||||||
|
{
|
||||||
|
lock (_datLock)
|
||||||
|
return _dats.Get<Setup>(setupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update-thread presentation owner for live retail missiles. Logical identity,
|
/// Update-thread presentation owner for live retail missiles. Logical identity,
|
||||||
/// generation, accepted network state, and teardown remain owned by
|
/// generation, accepted network state, and teardown remain owned by
|
||||||
|
|
@ -56,26 +81,26 @@ internal sealed class ProjectileController
|
||||||
private readonly LiveEntityRuntime _liveEntities;
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
private readonly ProjectilePhysicsStepper _stepper;
|
private readonly ProjectilePhysicsStepper _stepper;
|
||||||
private readonly ShadowObjectRegistry _shadows;
|
private readonly ShadowObjectRegistry _shadows;
|
||||||
private readonly Func<uint, Setup?> _setupResolver;
|
private readonly IProjectileSetupResolver? _setupResolver;
|
||||||
private readonly Action<WorldEntity> _publishRootPose;
|
private readonly IEntityRootPosePublisher? _rootPoses;
|
||||||
private readonly Func<(int X, int Y)> _liveCenterProvider;
|
private readonly LiveWorldOriginState? _origin;
|
||||||
private readonly List<LiveEntityRecord> _spatialProjectileSnapshot = new();
|
private readonly List<LiveEntityRecord> _spatialProjectileSnapshot = new();
|
||||||
private double _lastFiniteGameTime;
|
private double _lastFiniteGameTime;
|
||||||
|
|
||||||
internal ProjectileController(
|
internal ProjectileController(
|
||||||
LiveEntityRuntime liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
PhysicsEngine physicsEngine,
|
PhysicsEngine physicsEngine,
|
||||||
Func<uint, Setup?>? setupResolver = null,
|
IProjectileSetupResolver? setupResolver = null,
|
||||||
Action<WorldEntity>? publishRootPose = null,
|
IEntityRootPosePublisher? rootPoses = null,
|
||||||
Func<(int X, int Y)>? liveCenterProvider = null)
|
LiveWorldOriginState? origin = null)
|
||||||
{
|
{
|
||||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||||
ArgumentNullException.ThrowIfNull(physicsEngine);
|
ArgumentNullException.ThrowIfNull(physicsEngine);
|
||||||
_stepper = new ProjectilePhysicsStepper(physicsEngine);
|
_stepper = new ProjectilePhysicsStepper(physicsEngine);
|
||||||
_shadows = physicsEngine.ShadowObjects;
|
_shadows = physicsEngine.ShadowObjects;
|
||||||
_setupResolver = setupResolver ?? (_ => null);
|
_setupResolver = setupResolver;
|
||||||
_publishRootPose = publishRootPose ?? (_ => { });
|
_rootPoses = rootPoses;
|
||||||
_liveCenterProvider = liveCenterProvider ?? (() => (0, 0));
|
_origin = origin;
|
||||||
_liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged;
|
_liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,7 +347,7 @@ internal sealed class ProjectileController
|
||||||
{
|
{
|
||||||
SuspendOutsideWorld(runtime, entity.Id);
|
SuspendOutsideWorld(runtime, entity.Id);
|
||||||
}
|
}
|
||||||
_publishRootPose(entity);
|
_rootPoses?.UpdateRoot(entity);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,7 +497,7 @@ internal sealed class ProjectileController
|
||||||
// A newer State packet can make an already materialized object a
|
// A newer State packet can make an already materialized object a
|
||||||
// missile. TryBind adopts an existing MovementManager body so the
|
// missile. TryBind adopts an existing MovementManager body so the
|
||||||
// live record still represents retail's one CPhysicsObj.
|
// live record still represents retail's one CPhysicsObj.
|
||||||
Setup? setup = _setupResolver(entity.SourceGfxObjOrSetupId);
|
Setup? setup = _setupResolver?.Resolve(entity.SourceGfxObjOrSetupId);
|
||||||
if (setup is null)
|
if (setup is null)
|
||||||
{
|
{
|
||||||
DiagnosticSink?.Invoke(
|
DiagnosticSink?.Invoke(
|
||||||
|
|
@ -617,7 +642,7 @@ internal sealed class ProjectileController
|
||||||
{
|
{
|
||||||
SuspendOutsideWorld(runtime, entity.Id);
|
SuspendOutsideWorld(runtime, entity.Id);
|
||||||
}
|
}
|
||||||
_publishRootPose(entity);
|
_rootPoses?.UpdateRoot(entity);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -712,7 +737,7 @@ internal sealed class ProjectileController
|
||||||
record.FullCellId,
|
record.FullCellId,
|
||||||
liveCenterX,
|
liveCenterX,
|
||||||
liveCenterY);
|
liveCenterY);
|
||||||
_publishRootPose(entity);
|
_rootPoses?.UpdateRoot(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsHidden(record))
|
if (IsHidden(record))
|
||||||
|
|
@ -909,7 +934,7 @@ internal sealed class ProjectileController
|
||||||
current.FullCellId,
|
current.FullCellId,
|
||||||
liveCenterX,
|
liveCenterX,
|
||||||
liveCenterY);
|
liveCenterY);
|
||||||
_publishRootPose(entity);
|
_rootPoses?.UpdateRoot(entity);
|
||||||
if (!IsCurrentQuantumIdentity(step))
|
if (!IsCurrentQuantumIdentity(step))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -966,7 +991,8 @@ internal sealed class ProjectileController
|
||||||
// not lazily on the next simulated quantum: a zero-velocity or
|
// not lazily on the next simulated quantum: a zero-velocity or
|
||||||
// newly ordinary retained projectile may have no later projectile
|
// newly ordinary retained projectile may have no later projectile
|
||||||
// step from which to repair its shadow membership.
|
// step from which to repair its shadow membership.
|
||||||
(int liveCenterX, int liveCenterY) = _liveCenterProvider();
|
int liveCenterX = _origin?.CenterX ?? 0;
|
||||||
|
int liveCenterY = _origin?.CenterY ?? 0;
|
||||||
ShadowPositionSynchronizer.Sync(
|
ShadowPositionSynchronizer.Sync(
|
||||||
_shadows,
|
_shadows,
|
||||||
entity.Id,
|
entity.Id,
|
||||||
|
|
@ -975,7 +1001,7 @@ internal sealed class ProjectileController
|
||||||
record.FullCellId,
|
record.FullCellId,
|
||||||
liveCenterX,
|
liveCenterX,
|
||||||
liveCenterY);
|
liveCenterY);
|
||||||
_publishRootPose(entity);
|
_rootPoses?.UpdateRoot(entity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ using Silk.NET.Windowing;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
private static double ClientTimerNow() =>
|
private static double ClientTimerNow() =>
|
||||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||||
|
|
@ -152,12 +152,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_remoteInboundMotion;
|
_remoteInboundMotion;
|
||||||
private readonly AcDream.App.World.RetailInboundEventDispatcher
|
private readonly AcDream.App.World.RetailInboundEventDispatcher
|
||||||
_inboundEntityEvents = new();
|
_inboundEntityEvents = new();
|
||||||
private readonly LiveEntityAnimationScheduler _liveAnimationScheduler;
|
private LiveEntityAnimationScheduler _liveAnimationScheduler = null!;
|
||||||
private readonly LiveEntityAnimationPresenter _animationPresenter;
|
private LiveEntityAnimationPresenter _animationPresenter = null!;
|
||||||
private readonly AcDream.App.Input.LocalPlayerProjectionController _localPlayerProjection;
|
private readonly AcDream.App.Input.LocalPlayerProjectionController _localPlayerProjection;
|
||||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||||
private readonly AcDream.App.Input.RetailLocalPlayerFrameController _localPlayerFrame;
|
private readonly AcDream.App.Input.RetailLocalPlayerFrameController _localPlayerFrame;
|
||||||
private readonly AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator;
|
private AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator = null!;
|
||||||
|
private AcDream.App.Update.LiveSpatialPresentationReconciler
|
||||||
|
_liveSpatialReconciler = null!;
|
||||||
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
||||||
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||||
// Step 7 projectile presentation. The controller owns no identity map;
|
// Step 7 projectile presentation. The controller owns no identity map;
|
||||||
|
|
@ -247,6 +249,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
/// appear in this map.
|
/// appear in this map.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animatedEntities;
|
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animatedEntities;
|
||||||
|
private readonly AcDream.App.World.LiveEntityRuntimeSlot _liveEntityRuntimeSlot = new();
|
||||||
|
|
||||||
// MP-Alloc (2026-07-05): reusable per-frame snapshot of _animatedEntities.Keys.
|
// MP-Alloc (2026-07-05): reusable per-frame snapshot of _animatedEntities.Keys.
|
||||||
// WbDrawDispatcher.WalkEntitiesInto treats null and an empty set identically
|
// WbDrawDispatcher.WalkEntitiesInto treats null and an empty set identically
|
||||||
|
|
@ -520,11 +523,21 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
private int _weatherDebugStep = 0;
|
private int _weatherDebugStep = 0;
|
||||||
|
|
||||||
// Phase B.2: player movement mode.
|
// Phase B.2: player movement mode.
|
||||||
private AcDream.App.Input.PlayerMovementController? _playerController;
|
private readonly AcDream.App.Input.LocalPlayerControllerSlot _playerControllerSlot = new();
|
||||||
|
private AcDream.App.Input.PlayerMovementController? _playerController
|
||||||
|
{
|
||||||
|
get => _playerControllerSlot.Controller;
|
||||||
|
set => _playerControllerSlot.Controller = value;
|
||||||
|
}
|
||||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
||||||
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera;
|
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera;
|
||||||
private bool _playerMode;
|
private bool _playerMode;
|
||||||
private uint _playerServerGuid;
|
private readonly AcDream.App.Input.LocalPlayerIdentityState _localPlayerIdentity = new();
|
||||||
|
private uint _playerServerGuid
|
||||||
|
{
|
||||||
|
get => _localPlayerIdentity.ServerGuid;
|
||||||
|
set => _localPlayerIdentity.ServerGuid = value;
|
||||||
|
}
|
||||||
// Retail Default_CharacterOption (acclient.h:3434). PlayerDescription
|
// Retail Default_CharacterOption (acclient.h:3434). PlayerDescription
|
||||||
// replaces this before any in-world drag can normally occur.
|
// replaces this before any in-world drag can normally occur.
|
||||||
private AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
private AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
||||||
|
|
@ -745,7 +758,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_selection = selection ?? throw new System.ArgumentNullException(nameof(selection));
|
_selection = selection ?? throw new System.ArgumentNullException(nameof(selection));
|
||||||
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
||||||
_uiRegistry = uiRegistry;
|
_uiRegistry = uiRegistry;
|
||||||
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() => _liveEntities);
|
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
|
||||||
|
_liveEntityRuntimeSlot);
|
||||||
SpellBook = new AcDream.Core.Spells.Spellbook();
|
SpellBook = new AcDream.Core.Spells.Spellbook();
|
||||||
LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook);
|
LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook);
|
||||||
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
||||||
|
|
@ -761,25 +775,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
movement, cellId, update),
|
movement, cellId, update),
|
||||||
(host, targetGuid) =>
|
(host, targetGuid) =>
|
||||||
_liveEntityMotionBindings.StickToObjectFromWire(host, targetGuid));
|
_liveEntityMotionBindings.StickToObjectFromWire(host, targetGuid));
|
||||||
var ordinaryPhysicsUpdater =
|
|
||||||
new AcDream.App.Physics.LiveEntityOrdinaryPhysicsUpdater(
|
|
||||||
_physicsEngine,
|
|
||||||
_liveEntityMotionBindings.GetSetupCylinder);
|
|
||||||
_liveAnimationScheduler = new LiveEntityAnimationScheduler(
|
|
||||||
() => _liveEntities,
|
|
||||||
() => _playerServerGuid,
|
|
||||||
_remotePhysicsUpdater,
|
|
||||||
ordinaryPhysicsUpdater,
|
|
||||||
() => _projectileController,
|
|
||||||
entity => _effectPoses.UpdateRoot(entity),
|
|
||||||
CaptureAnimationHooks);
|
|
||||||
_animationPresenter = new LiveEntityAnimationPresenter(
|
|
||||||
() => _liveEntities,
|
|
||||||
() => _staticAnimationScheduler,
|
|
||||||
_effectPoses,
|
|
||||||
this,
|
|
||||||
_animationDiagnostics,
|
|
||||||
options.HidePartIndex);
|
|
||||||
_localPlayerProjection = new AcDream.App.Input.LocalPlayerProjectionController(
|
_localPlayerProjection = new AcDream.App.Input.LocalPlayerProjectionController(
|
||||||
resolveEntity: () =>
|
resolveEntity: () =>
|
||||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
||||||
|
|
@ -824,16 +819,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
objectClockDisposition: () =>
|
objectClockDisposition: () =>
|
||||||
_liveEntities?.GetRootObjectClockDisposition(_playerServerGuid)
|
_liveEntities?.GetRootObjectClockDisposition(_playerServerGuid)
|
||||||
?? AcDream.Core.Physics.RetailObjectClockDisposition.Advance);
|
?? AcDream.Core.Physics.RetailObjectClockDisposition.Advance);
|
||||||
_liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
|
||||||
AdvanceLiveObjectRuntime,
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
using AcDream.App.Streaming.GpuWorldState.MutationBatch spatialBatch =
|
|
||||||
_worldState.BeginMutationBatch();
|
|
||||||
_liveSessionController?.Tick();
|
|
||||||
},
|
|
||||||
_localPlayerFrame.RunPostNetworkCommandPhase,
|
|
||||||
ReconcileLiveObjectSpatialPresentation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run()
|
public void Run()
|
||||||
|
|
@ -2080,34 +2065,20 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// dat-lookup throws — see C.1.5a spec §6 (error handling) for rationale.
|
// dat-lookup throws — see C.1.5a spec §6 (error handling) for rationale.
|
||||||
AcDream.App.Rendering.Vfx.EntityEffectController? entityEffects = null;
|
AcDream.App.Rendering.Vfx.EntityEffectController? entityEffects = null;
|
||||||
var staticLiveRootCommitter = new StaticLiveRootCommitter(
|
var staticLiveRootCommitter = new StaticLiveRootCommitter(
|
||||||
() => _liveEntities,
|
_liveEntityRuntimeSlot,
|
||||||
_physicsEngine.ShadowObjects,
|
_physicsEngine.ShadowObjects,
|
||||||
() => (_liveCenterX, _liveCenterY),
|
_liveWorldOrigin,
|
||||||
entity => _effectPoses.UpdateRoot(entity));
|
_effectPoses);
|
||||||
|
var staticAnimationResidency = new LiveStaticAnimationResidency(
|
||||||
|
_liveEntityRuntimeSlot);
|
||||||
_staticAnimationScheduler = new RetailStaticAnimatingObjectScheduler(
|
_staticAnimationScheduler = new RetailStaticAnimatingObjectScheduler(
|
||||||
capturedAnimLoader!,
|
capturedAnimLoader!,
|
||||||
CaptureAnimationHooks,
|
_animationHookFrames!.Capture,
|
||||||
(entity, poses, availability) =>
|
_effectPoses.Publish,
|
||||||
_effectPoses.Publish(entity, poses, availability),
|
staticAnimationResidency.IsResident,
|
||||||
entity => entity.ServerGuid == 0
|
|
||||||
|| (_liveEntities?.TryGetRecord(
|
|
||||||
entity.ServerGuid,
|
|
||||||
out LiveEntityRecord resident) == true
|
|
||||||
&& resident.ProjectionKind is LiveEntityProjectionKind.World
|
|
||||||
&& ReferenceEquals(resident.WorldEntity, entity)
|
|
||||||
&& resident.IsSpatiallyProjected
|
|
||||||
&& resident.IsSpatiallyVisible
|
|
||||||
&& resident.FullCellId != 0),
|
|
||||||
(entity, body) =>
|
(entity, body) =>
|
||||||
_ = staticLiveRootCommitter.Commit(entity, body),
|
_ = staticLiveRootCommitter.Commit(entity, body),
|
||||||
entity => entity.ServerGuid == 0
|
staticAnimationResidency.ProjectionVersion);
|
||||||
? 0UL
|
|
||||||
: _liveEntities?.TryGetRecord(
|
|
||||||
entity.ServerGuid,
|
|
||||||
out LiveEntityRecord resident) == true
|
|
||||||
&& ReferenceEquals(resident.WorldEntity, entity)
|
|
||||||
? resident.ProjectionMutationVersion
|
|
||||||
: ulong.MaxValue);
|
|
||||||
AcDream.App.Rendering.Vfx.ScriptActivationInfo? ResolveActivation(AcDream.Core.World.WorldEntity e)
|
AcDream.App.Rendering.Vfx.ScriptActivationInfo? ResolveActivation(AcDream.Core.World.WorldEntity e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -2183,6 +2154,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
entityScriptActivator.OnCreate,
|
entityScriptActivator.OnCreate,
|
||||||
entityScriptActivator.OnRemove)),
|
entityScriptActivator.OnRemove)),
|
||||||
liveEntityComponentLifecycle);
|
liveEntityComponentLifecycle);
|
||||||
|
_liveEntityRuntimeSlot.Bind(_liveEntities);
|
||||||
liveEntityMotionRuntime =
|
liveEntityMotionRuntime =
|
||||||
new AcDream.App.Physics.LiveEntityMotionRuntimeController(
|
new AcDream.App.Physics.LiveEntityMotionRuntimeController(
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
|
|
@ -2204,17 +2176,9 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_projectileController = new AcDream.App.Physics.ProjectileController(
|
_projectileController = new AcDream.App.Physics.ProjectileController(
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
setupId =>
|
new AcDream.App.Physics.DatProjectileSetupResolver(_dats!, _datLock),
|
||||||
{
|
new EntityRootPosePublisher(_effectPoses),
|
||||||
// State can classify an already-live object as Missile
|
_liveWorldOrigin)
|
||||||
// outside the spawn handler's lock. DatCollection shares
|
|
||||||
// one mutable reader cursor with the streaming worker, so
|
|
||||||
// this resolver must serialize both initial and late bind.
|
|
||||||
lock (_datLock)
|
|
||||||
return _dats?.Get<DatReaderWriter.DBObjs.Setup>(setupId);
|
|
||||||
},
|
|
||||||
entity => _effectPoses.UpdateRoot(entity),
|
|
||||||
() => (_liveCenterX, _liveCenterY))
|
|
||||||
{
|
{
|
||||||
DiagnosticSink = message => Console.Error.WriteLine($"projectile: {message}"),
|
DiagnosticSink = message => Console.Error.WriteLine($"projectile: {message}"),
|
||||||
};
|
};
|
||||||
|
|
@ -2233,6 +2197,29 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_lightingSink!,
|
_lightingSink!,
|
||||||
setupId => _dats!.Get<DatReaderWriter.DBObjs.Setup>(setupId));
|
setupId => _dats!.Get<DatReaderWriter.DBObjs.Setup>(setupId));
|
||||||
|
|
||||||
|
var ordinaryPhysicsUpdater =
|
||||||
|
new AcDream.App.Physics.LiveEntityOrdinaryPhysicsUpdater(
|
||||||
|
_physicsEngine,
|
||||||
|
_liveEntityMotionBindings.GetSetupCylinder);
|
||||||
|
_liveAnimationScheduler = new LiveEntityAnimationScheduler(
|
||||||
|
_liveEntities,
|
||||||
|
_localPlayerIdentity,
|
||||||
|
_remotePhysicsUpdater,
|
||||||
|
ordinaryPhysicsUpdater,
|
||||||
|
_projectileController,
|
||||||
|
new EntityRootPosePublisher(_effectPoses),
|
||||||
|
new AnimationHookCaptureSink(_animationHookFrames!));
|
||||||
|
_animationPresenter = new LiveEntityAnimationPresenter(
|
||||||
|
_liveEntities,
|
||||||
|
_staticAnimationScheduler,
|
||||||
|
_effectPoses,
|
||||||
|
new LiveAnimationPresentationContext(
|
||||||
|
_liveEntities,
|
||||||
|
_localPlayerIdentity,
|
||||||
|
_playerControllerSlot),
|
||||||
|
_animationDiagnostics,
|
||||||
|
_options.HidePartIndex);
|
||||||
|
|
||||||
parentAcceptance = new AcDream.App.World.DeferredLiveEntityParentAcceptance();
|
parentAcceptance = new AcDream.App.World.DeferredLiveEntityParentAcceptance();
|
||||||
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
|
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
|
||||||
_dats!,
|
_dats!,
|
||||||
|
|
@ -2627,7 +2614,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_remoteTeleportController!,
|
_remoteTeleportController!,
|
||||||
_animatedEntities,
|
_animatedEntities,
|
||||||
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(
|
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(
|
||||||
() => _liveEntities),
|
_liveEntityRuntimeSlot),
|
||||||
_remoteMovementObservations,
|
_remoteMovementObservations,
|
||||||
_remotePhysicsUpdater,
|
_remotePhysicsUpdater,
|
||||||
_remoteInboundMotion,
|
_remoteInboundMotion,
|
||||||
|
|
@ -2667,6 +2654,44 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
||||||
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
||||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
||||||
|
var liveEffectFrame = new AcDream.App.Update.LiveEffectFrameController(
|
||||||
|
_translucencyFades,
|
||||||
|
_animationHookFrames!,
|
||||||
|
_entityEffects!,
|
||||||
|
_particleSink!,
|
||||||
|
_liveEntityLights!,
|
||||||
|
_particleVisibility,
|
||||||
|
_particleSystem!,
|
||||||
|
_scriptRunner!,
|
||||||
|
_updateFrameClock,
|
||||||
|
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!,
|
||||||
|
_equippedChildRenderer!,
|
||||||
|
_particleSink!,
|
||||||
|
_liveEntityLights!);
|
||||||
|
_liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||||
|
liveObjectFrame,
|
||||||
|
_worldState,
|
||||||
|
_liveSessionController,
|
||||||
|
_localPlayerFrame,
|
||||||
|
_liveSpatialReconciler);
|
||||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||||
_liveSessionController.Start(
|
_liveSessionController.Start(
|
||||||
_options,
|
_options,
|
||||||
|
|
@ -3520,7 +3545,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// camera path re-extends the boom. Updating directly from the stale
|
// camera path re-extends the boom. Updating directly from the stale
|
||||||
// source-world viewer made the destination bend across the reveal.
|
// source-world viewer made the destination bend across the reveal.
|
||||||
_retailChaseCamera?.ResetViewerToPlayer(snappedPos, _playerController.Yaw);
|
_retailChaseCamera?.ResetViewerToPlayer(snappedPos, _playerController.Yaw);
|
||||||
ReconcileLiveObjectSpatialPresentation();
|
_liveSpatialReconciler.Reconcile();
|
||||||
|
|
||||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||||
"PLACED", resolved.CellId, $"forced={forced}");
|
"PLACED", resolved.CellId, $"forced={forced}");
|
||||||
|
|
@ -4203,7 +4228,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// The player was materialized by this frame's inbound pass.
|
// The player was materialized by this frame's inbound pass.
|
||||||
// Its non-advancing initial projection must publish child and
|
// Its non-advancing initial projection must publish child and
|
||||||
// effect anchors before the first draw too.
|
// effect anchors before the first draw too.
|
||||||
ReconcileLiveObjectSpatialPresentation();
|
_liveSpatialReconciler.Reconcile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update chase camera(s). The CameraController exposes whichever
|
// Update chase camera(s). The CameraController exposes whichever
|
||||||
|
|
@ -5715,102 +5740,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
LastFrameProfile: _frameProfiler.LastReport);
|
LastFrameProfile: _frameProfiler.LastReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Advances the live object's retail UseTime phase before inbound packet
|
|
||||||
/// dispatch. Final root/part poses and their animation hooks are published
|
|
||||||
/// together so rendering consumes one coherent update snapshot.
|
|
||||||
/// </summary>
|
|
||||||
private void AdvanceLiveObjectRuntime(float dt) =>
|
|
||||||
_inboundEntityEvents.Run(
|
|
||||||
this,
|
|
||||||
dt,
|
|
||||||
static (window, deltaTime) =>
|
|
||||||
window.AdvanceLiveObjectRuntimeCore(deltaTime));
|
|
||||||
|
|
||||||
private void AdvanceLiveObjectRuntimeCore(float dt)
|
|
||||||
{
|
|
||||||
// The local body participates in the same retail CPhysics phase as
|
|
||||||
// remote objects. Its outbound movement snapshot is completed here,
|
|
||||||
// before SmartBox dispatch can apply F751/ForcePosition.
|
|
||||||
_localPlayerFrame.AdvanceBeforeNetwork(dt);
|
|
||||||
// Retail input ordering: movement edges reach MoveToState before a
|
|
||||||
// later Use/PickUp action from the same input frame. ACE cancels an
|
|
||||||
// active server MoveTo chain when it receives MoveToState, so sending
|
|
||||||
// Use directly from Silk's key callback inverted that order and left
|
|
||||||
// the client walking to a corpse whose server callback was cancelled.
|
|
||||||
_selectionInteractions?.DrainOutbound();
|
|
||||||
System.Numerics.Vector3? playerPosition =
|
|
||||||
_entitiesByServerGuid.TryGetValue(
|
|
||||||
_playerServerGuid,
|
|
||||||
out var ordinaryObjectViewer)
|
|
||||||
? ordinaryObjectViewer.Position
|
|
||||||
: null;
|
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> animationSchedules =
|
|
||||||
_liveAnimationScheduler.Tick(
|
|
||||||
dt,
|
|
||||||
playerPosition,
|
|
||||||
_localPlayerFrame.HiddenPartPoseDirty,
|
|
||||||
_liveCenterX,
|
|
||||||
_liveCenterY,
|
|
||||||
_animationPresenter.PrepareAnimation);
|
|
||||||
// CPhysics::UseTime (0x00509950) processes the ordinary object table
|
|
||||||
// first, then its distinct static_animating_objects workset.
|
|
||||||
_staticAnimationScheduler?.Tick(dt);
|
|
||||||
if (_animatedEntities.Count > 0)
|
|
||||||
_animationPresenter.Present(animationSchedules);
|
|
||||||
_equippedChildRenderer?.Tick();
|
|
||||||
|
|
||||||
// CPhysicsObj::animate_static_object (0x00513DF0) reaches
|
|
||||||
// process_hooks only after UpdatePartsInternal and
|
|
||||||
// UpdateChildrenInternal. Ordinary objects complete AnimationDone in
|
|
||||||
// their earlier UpdatePositionInternal slot; the static workset has
|
|
||||||
// this distinct tail.
|
|
||||||
_staticAnimationScheduler?.ProcessHooks();
|
|
||||||
|
|
||||||
// Advance already-active FP hooks before routing hooks emitted by
|
|
||||||
// this PartArray update. A newly created translucency hook begins at
|
|
||||||
// its authored Start value this frame; it does not consume dt twice.
|
|
||||||
_translucencyFades.AdvanceAll(dt);
|
|
||||||
_animationHookFrames?.Drain();
|
|
||||||
_entityEffects?.RefreshLiveOwnerPoses();
|
|
||||||
_particleSink?.RefreshAttachedEmitters();
|
|
||||||
_liveEntityLights?.Refresh();
|
|
||||||
|
|
||||||
if (_particleSystem is not null)
|
|
||||||
{
|
|
||||||
var particleRange = _settingsVm?.DisplayDraft.ParticleRange
|
|
||||||
?? _persistedDisplay.ParticleRange;
|
|
||||||
float rangeMultiplier = particleRange
|
|
||||||
== AcDream.UI.Abstractions.Panels.Settings.ParticleRange.Extended
|
|
||||||
? AcDream.App.Rendering.Vfx.ParticleVisibilityController.ExtendedRangeMultiplier
|
|
||||||
: 1f;
|
|
||||||
_particleVisibility.Apply(_particleSystem, rangeMultiplier);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retail CPhysicsObj::UpdateObjectInternal (0x005156B0) advances
|
|
||||||
// ParticleManager before ScriptManager. A particle created by a PES
|
|
||||||
// hook therefore begins simulation on the following object frame.
|
|
||||||
_particleSystem?.Tick(dt);
|
|
||||||
_scriptRunner?.Tick(_updateFrameClock.CurrentScriptTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Re-composes spatial derivatives after authoritative packet handlers
|
|
||||||
/// move roots. This pass advances no time, animation, scripts, particles,
|
|
||||||
/// or fades; it only makes children and live anchors agree with the root
|
|
||||||
/// that the same frame will draw.
|
|
||||||
/// </summary>
|
|
||||||
private void ReconcileLiveObjectSpatialPresentation()
|
|
||||||
{
|
|
||||||
// Authoritative handlers mutate WorldEntity roots directly. Publish
|
|
||||||
// those roots before children read their parent's pose; child Tick
|
|
||||||
// then publishes each newly composed child pose for attached effects.
|
|
||||||
_entityEffects?.RefreshLiveOwnerPoses();
|
|
||||||
_equippedChildRenderer?.Tick();
|
|
||||||
_particleSink?.RefreshAttachedEmitters();
|
|
||||||
_liveEntityLights?.Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase 6.4: advance every animated entity's frame counter by
|
/// Phase 6.4: advance every animated entity's frame counter by
|
||||||
/// <paramref name="dt"/> * Framerate, wrapping around the cycle's
|
/// <paramref name="dt"/> * Framerate, wrapping around the cycle's
|
||||||
|
|
@ -5855,33 +5784,10 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
&& _animatedEntities.TryGetValue(entity.Id, out var ae)
|
&& _animatedEntities.TryGetValue(entity.Id, out var ae)
|
||||||
&& ae.Sequencer is { } sequencer)
|
&& ae.Sequencer is { } sequencer)
|
||||||
{
|
{
|
||||||
CaptureAnimationHooks(ae.Entity.Id, sequencer);
|
_animationHookFrames?.Capture(ae.Entity.Id, sequencer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CaptureAnimationHooks(
|
|
||||||
uint ownerLocalId,
|
|
||||||
AcDream.Core.Physics.AnimationSequencer sequencer) =>
|
|
||||||
_animationHookFrames?.Capture(ownerLocalId, sequencer);
|
|
||||||
|
|
||||||
uint ILiveAnimationPresentationContext.LocalPlayerGuid => _playerServerGuid;
|
|
||||||
|
|
||||||
AcDream.Core.Physics.MotionInterpreter?
|
|
||||||
ILiveAnimationPresentationContext.ResolveMotionInterpreter(
|
|
||||||
LiveEntityRecord record)
|
|
||||||
{
|
|
||||||
if (_liveEntities?.TryGetRecord(record.ServerGuid, out LiveEntityRecord current) != true
|
|
||||||
|| !ReferenceEquals(current, record))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (record.ServerGuid == _playerServerGuid)
|
|
||||||
return _playerController?.Motion;
|
|
||||||
return record.RemoteMotionRuntime is RemoteMotion remote
|
|
||||||
? remote.Motion
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
||||||
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
||||||
// stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the
|
// stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the
|
||||||
|
|
|
||||||
36
src/AcDream.App/Rendering/IAnimationHookCaptureSink.cs
Normal file
36
src/AcDream.App/Rendering/IAnimationHookCaptureSink.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
internal interface IAnimationHookCaptureSink
|
||||||
|
{
|
||||||
|
void Capture(uint ownerLocalId, AnimationSequencer sequencer);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class AnimationHookCaptureSink : IAnimationHookCaptureSink
|
||||||
|
{
|
||||||
|
private readonly AnimationHookFrameQueue _queue;
|
||||||
|
|
||||||
|
public AnimationHookCaptureSink(AnimationHookFrameQueue queue) =>
|
||||||
|
_queue = queue ?? throw new ArgumentNullException(nameof(queue));
|
||||||
|
|
||||||
|
public void Capture(uint ownerLocalId, AnimationSequencer sequencer) =>
|
||||||
|
_queue.Capture(ownerLocalId, sequencer);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IEntityRootPosePublisher
|
||||||
|
{
|
||||||
|
void UpdateRoot(WorldEntity entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class EntityRootPosePublisher : IEntityRootPosePublisher
|
||||||
|
{
|
||||||
|
private readonly EntityEffectPoseRegistry _poses;
|
||||||
|
|
||||||
|
public EntityRootPosePublisher(EntityEffectPoseRegistry poses) =>
|
||||||
|
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
|
||||||
|
|
||||||
|
public void UpdateRoot(WorldEntity entity) => _poses.UpdateRoot(entity);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the current motion owner without retaining the application window.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveAnimationPresentationContext
|
||||||
|
: ILiveAnimationPresentationContext
|
||||||
|
{
|
||||||
|
private readonly LiveEntityRuntime _runtime;
|
||||||
|
private readonly ILocalPlayerIdentitySource _localPlayer;
|
||||||
|
private readonly ILocalPlayerControllerSource _playerController;
|
||||||
|
|
||||||
|
public LiveAnimationPresentationContext(
|
||||||
|
LiveEntityRuntime runtime,
|
||||||
|
ILocalPlayerIdentitySource localPlayer,
|
||||||
|
ILocalPlayerControllerSource playerController)
|
||||||
|
{
|
||||||
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
_localPlayer = localPlayer ?? throw new ArgumentNullException(nameof(localPlayer));
|
||||||
|
_playerController = playerController
|
||||||
|
?? throw new ArgumentNullException(nameof(playerController));
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint LocalPlayerGuid => _localPlayer.ServerGuid;
|
||||||
|
|
||||||
|
public MotionInterpreter? ResolveMotionInterpreter(LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
if (!_runtime.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|
||||||
|
|| !ReferenceEquals(current, record))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.ServerGuid == _localPlayer.ServerGuid)
|
||||||
|
return _playerController.Controller?.Motion;
|
||||||
|
return record.RemoteMotionRuntime is RemoteMotion remote
|
||||||
|
? remote.Motion
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,8 @@ namespace AcDream.App.Rendering;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LiveEntityAnimationPresenter
|
internal sealed class LiveEntityAnimationPresenter
|
||||||
{
|
{
|
||||||
private readonly Func<LiveEntityRuntime?> _liveEntities;
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
private readonly Func<ILiveStaticPartFrameSource?> _staticFrames;
|
private readonly ILiveStaticPartFrameSource _staticFrames;
|
||||||
private readonly EntityEffectPoseRegistry _effectPoses;
|
private readonly EntityEffectPoseRegistry _effectPoses;
|
||||||
private readonly ILiveAnimationPresentationContext _context;
|
private readonly ILiveAnimationPresentationContext _context;
|
||||||
private readonly AnimationPresentationDiagnostics _diagnostics;
|
private readonly AnimationPresentationDiagnostics _diagnostics;
|
||||||
|
|
@ -22,8 +22,8 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
private bool _isPresenting;
|
private bool _isPresenting;
|
||||||
|
|
||||||
public LiveEntityAnimationPresenter(
|
public LiveEntityAnimationPresenter(
|
||||||
Func<LiveEntityRuntime?> liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
Func<ILiveStaticPartFrameSource?> staticFrames,
|
ILiveStaticPartFrameSource staticFrames,
|
||||||
EntityEffectPoseRegistry effectPoses,
|
EntityEffectPoseRegistry effectPoses,
|
||||||
ILiveAnimationPresentationContext context,
|
ILiveAnimationPresentationContext context,
|
||||||
AnimationPresentationDiagnostics diagnostics,
|
AnimationPresentationDiagnostics diagnostics,
|
||||||
|
|
@ -43,9 +43,8 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
ArgumentNullException.ThrowIfNull(animation);
|
ArgumentNullException.ThrowIfNull(animation);
|
||||||
LiveEntityRuntime? runtime = _liveEntities();
|
LiveEntityRuntime runtime = _liveEntities;
|
||||||
if (runtime is null
|
if (!runtime.IsCurrentSpatialAnimation(record, animation)
|
||||||
|| !runtime.IsCurrentSpatialAnimation(record, animation)
|
|
||||||
|| !ReferenceEquals(record.WorldEntity, animation.Entity)
|
|| !ReferenceEquals(record.WorldEntity, animation.Entity)
|
||||||
|| animation.Sequencer is not { MotionDoneTarget: null } sequencer)
|
|| animation.Sequencer is not { MotionDoneTarget: null } sequencer)
|
||||||
{
|
{
|
||||||
|
|
@ -55,9 +54,8 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
WorldEntity capturedEntity = animation.Entity;
|
WorldEntity capturedEntity = animation.Entity;
|
||||||
sequencer.MotionDoneTarget = (motion, success) =>
|
sequencer.MotionDoneTarget = (motion, success) =>
|
||||||
{
|
{
|
||||||
LiveEntityRuntime? current = _liveEntities();
|
LiveEntityRuntime current = _liveEntities;
|
||||||
if (current is null
|
if (!current.IsCurrentSpatialAnimation(record, animation)
|
||||||
|| !current.IsCurrentSpatialAnimation(record, animation)
|
|
||||||
|| !ReferenceEquals(record.WorldEntity, capturedEntity)
|
|| !ReferenceEquals(record.WorldEntity, capturedEntity)
|
||||||
|| !ReferenceEquals(animation.Entity, capturedEntity))
|
|| !ReferenceEquals(animation.Entity, capturedEntity))
|
||||||
{
|
{
|
||||||
|
|
@ -79,8 +77,8 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules)
|
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(schedules);
|
ArgumentNullException.ThrowIfNull(schedules);
|
||||||
LiveEntityRuntime? runtime = _liveEntities();
|
LiveEntityRuntime runtime = _liveEntities;
|
||||||
if (runtime is null || _isPresenting)
|
if (_isPresenting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// EffectPoseChanged is synchronous. Presentation is one consuming
|
// EffectPoseChanged is synchronous. Presentation is one consuming
|
||||||
|
|
@ -112,7 +110,7 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
ulong projectionVersion = record.ProjectionMutationVersion;
|
ulong projectionVersion = record.ProjectionMutationVersion;
|
||||||
ulong presentationRevision = animation.PresentationRevision;
|
ulong presentationRevision = animation.PresentationRevision;
|
||||||
|
|
||||||
if (_staticFrames()?.TryTakeLivePartFrames(
|
if (_staticFrames.TryTakeLivePartFrames(
|
||||||
record,
|
record,
|
||||||
entity,
|
entity,
|
||||||
animation,
|
animation,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
@ -17,41 +19,38 @@ namespace AcDream.App.Rendering;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LiveEntityAnimationScheduler
|
internal sealed class LiveEntityAnimationScheduler
|
||||||
{
|
{
|
||||||
private readonly Func<LiveEntityRuntime?> _liveEntities;
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
private readonly Func<uint> _localPlayerGuid;
|
private readonly ILocalPlayerIdentitySource _localPlayer;
|
||||||
private readonly RemotePhysicsUpdater _remotePhysics;
|
private readonly RemotePhysicsUpdater _remotePhysics;
|
||||||
private readonly LiveEntityOrdinaryPhysicsUpdater _ordinaryPhysics;
|
private readonly LiveEntityOrdinaryPhysicsUpdater _ordinaryPhysics;
|
||||||
private readonly Func<ProjectileController?> _projectiles;
|
private readonly ProjectileController _projectiles;
|
||||||
private readonly Action<WorldEntity> _publishRootPose;
|
private readonly IEntityRootPosePublisher _rootPoses;
|
||||||
private readonly Action<uint, AnimationSequencer> _captureAnimationHooks;
|
private readonly IAnimationHookCaptureSink _animationHooks;
|
||||||
private readonly List<LiveEntityRecord> _rootSnapshot = new();
|
private readonly List<LiveEntityRecord> _rootSnapshot = new();
|
||||||
private readonly Dictionary<uint, LiveEntityAnimationSchedule> _schedules = new();
|
private readonly Dictionary<uint, LiveEntityAnimationSchedule> _schedules = new();
|
||||||
private readonly Frame _rootFrameScratch = new();
|
private readonly Frame _rootFrameScratch = new();
|
||||||
private readonly MotionDeltaFrame _rootDeltaScratch = new();
|
private readonly MotionDeltaFrame _rootDeltaScratch = new();
|
||||||
|
|
||||||
public LiveEntityAnimationScheduler(
|
public LiveEntityAnimationScheduler(
|
||||||
Func<LiveEntityRuntime?> liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
Func<uint> localPlayerGuid,
|
ILocalPlayerIdentitySource localPlayer,
|
||||||
RemotePhysicsUpdater remotePhysics,
|
RemotePhysicsUpdater remotePhysics,
|
||||||
LiveEntityOrdinaryPhysicsUpdater ordinaryPhysics,
|
LiveEntityOrdinaryPhysicsUpdater ordinaryPhysics,
|
||||||
Func<ProjectileController?> projectiles,
|
ProjectileController projectiles,
|
||||||
Action<WorldEntity> publishRootPose,
|
IEntityRootPosePublisher rootPoses,
|
||||||
Action<uint, AnimationSequencer> captureAnimationHooks)
|
IAnimationHookCaptureSink animationHooks)
|
||||||
{
|
{
|
||||||
_liveEntities = liveEntities
|
_liveEntities = liveEntities
|
||||||
?? throw new ArgumentNullException(nameof(liveEntities));
|
?? throw new ArgumentNullException(nameof(liveEntities));
|
||||||
_localPlayerGuid = localPlayerGuid
|
_localPlayer = localPlayer
|
||||||
?? throw new ArgumentNullException(nameof(localPlayerGuid));
|
?? throw new ArgumentNullException(nameof(localPlayer));
|
||||||
_remotePhysics = remotePhysics
|
_remotePhysics = remotePhysics
|
||||||
?? throw new ArgumentNullException(nameof(remotePhysics));
|
?? throw new ArgumentNullException(nameof(remotePhysics));
|
||||||
_ordinaryPhysics = ordinaryPhysics
|
_ordinaryPhysics = ordinaryPhysics
|
||||||
?? throw new ArgumentNullException(nameof(ordinaryPhysics));
|
?? throw new ArgumentNullException(nameof(ordinaryPhysics));
|
||||||
_projectiles = projectiles
|
_projectiles = projectiles ?? throw new ArgumentNullException(nameof(projectiles));
|
||||||
?? throw new ArgumentNullException(nameof(projectiles));
|
_rootPoses = rootPoses ?? throw new ArgumentNullException(nameof(rootPoses));
|
||||||
_publishRootPose = publishRootPose
|
_animationHooks = animationHooks ?? throw new ArgumentNullException(nameof(animationHooks));
|
||||||
?? throw new ArgumentNullException(nameof(publishRootPose));
|
|
||||||
_captureAnimationHooks = captureAnimationHooks
|
|
||||||
?? throw new ArgumentNullException(nameof(captureAnimationHooks));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -69,9 +68,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
Action<LiveEntityRecord, LiveEntityAnimationState>? prepareAnimation = null)
|
Action<LiveEntityRecord, LiveEntityAnimationState>? prepareAnimation = null)
|
||||||
{
|
{
|
||||||
_schedules.Clear();
|
_schedules.Clear();
|
||||||
LiveEntityRuntime? runtime = _liveEntities();
|
LiveEntityRuntime runtime = _liveEntities;
|
||||||
if (runtime is null)
|
|
||||||
return _schedules;
|
|
||||||
|
|
||||||
runtime.CopySpatialRootObjectRecordsTo(_rootSnapshot);
|
runtime.CopySpatialRootObjectRecordsTo(_rootSnapshot);
|
||||||
foreach (LiveEntityRecord record in _rootSnapshot)
|
foreach (LiveEntityRecord record in _rootSnapshot)
|
||||||
|
|
@ -180,7 +177,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
// PlayerMovementController owns this exact record clock and advances
|
// PlayerMovementController owns this exact record clock and advances
|
||||||
// its PartArray before local collision. Consume that prepared pose;
|
// its PartArray before local collision. Consume that prepared pose;
|
||||||
// never tick the same clock or sequence a second time here.
|
// never tick the same clock or sequence a second time here.
|
||||||
if (serverGuid == _localPlayerGuid())
|
if (serverGuid == _localPlayer.ServerGuid)
|
||||||
{
|
{
|
||||||
IReadOnlyList<PartTransform>? prepared =
|
IReadOnlyList<PartTransform>? prepared =
|
||||||
animation?.PreparedSequenceFrames;
|
animation?.PreparedSequenceFrames;
|
||||||
|
|
@ -208,7 +205,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
// that are smaller than an admitted object quantum. Publish that
|
// that are smaller than an admitted object quantum. Publish that
|
||||||
// current root independently of PartArray recomposition so local
|
// current root independently of PartArray recomposition so local
|
||||||
// attached effects and lights never trail the rendered character.
|
// attached effects and lights never trail the rendered character.
|
||||||
_publishRootPose(entity);
|
_rootPoses.UpdateRoot(entity);
|
||||||
if (!IsCurrent(
|
if (!IsCurrent(
|
||||||
runtime,
|
runtime,
|
||||||
record,
|
record,
|
||||||
|
|
@ -249,7 +246,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
IReadOnlyList<PartTransform>? frames = null;
|
IReadOnlyList<PartTransform>? frames = null;
|
||||||
float legacyElapsed = 0f;
|
float legacyElapsed = 0f;
|
||||||
bool completed = true;
|
bool completed = true;
|
||||||
ProjectileController? projectileController = _projectiles();
|
ProjectileController projectileController = _projectiles;
|
||||||
bool projectileHandlesMovement = projectile is not null
|
bool projectileHandlesMovement = projectile is not null
|
||||||
&& projectileController?.HandlesMovement(serverGuid) == true;
|
&& projectileController?.HandlesMovement(serverGuid) == true;
|
||||||
float objectScale = animation?.Scale
|
float objectScale = animation?.Scale
|
||||||
|
|
@ -275,7 +272,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
entity,
|
entity,
|
||||||
quantum,
|
quantum,
|
||||||
sequencer?.Manager,
|
sequencer?.Manager,
|
||||||
_captureAnimationHooks,
|
_animationHooks.Capture,
|
||||||
sequencer,
|
sequencer,
|
||||||
runtime,
|
runtime,
|
||||||
record,
|
record,
|
||||||
|
|
@ -289,7 +286,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
{
|
{
|
||||||
if (sequencer is not null)
|
if (sequencer is not null)
|
||||||
{
|
{
|
||||||
_captureAnimationHooks(entity.Id, sequencer);
|
_animationHooks.Capture(entity.Id, sequencer);
|
||||||
if (!IsCurrent(
|
if (!IsCurrent(
|
||||||
runtime,
|
runtime,
|
||||||
record,
|
record,
|
||||||
|
|
@ -355,7 +352,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
rootDelta,
|
rootDelta,
|
||||||
liveCenterX,
|
liveCenterX,
|
||||||
liveCenterY,
|
liveCenterY,
|
||||||
_captureAnimationHooks,
|
_animationHooks.Capture,
|
||||||
runtime,
|
runtime,
|
||||||
record,
|
record,
|
||||||
objectClockEpoch))
|
objectClockEpoch))
|
||||||
|
|
@ -381,7 +378,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
liveCenterY,
|
liveCenterY,
|
||||||
objectClockEpoch,
|
objectClockEpoch,
|
||||||
sequencer,
|
sequencer,
|
||||||
_captureAnimationHooks))
|
_animationHooks.Capture))
|
||||||
{
|
{
|
||||||
completed = false;
|
completed = false;
|
||||||
break;
|
break;
|
||||||
|
|
@ -405,7 +402,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
out projectileStep) == true;
|
out projectileStep) == true;
|
||||||
|
|
||||||
if (!ordinaryBodyHandlesMovement && sequencer is not null)
|
if (!ordinaryBodyHandlesMovement && sequencer is not null)
|
||||||
_captureAnimationHooks(entity.Id, sequencer);
|
_animationHooks.Capture(entity.Id, sequencer);
|
||||||
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
|
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
|
||||||
{
|
{
|
||||||
completed = false;
|
completed = false;
|
||||||
|
|
@ -443,7 +440,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_publishRootPose(entity);
|
_rootPoses.UpdateRoot(entity);
|
||||||
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
|
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
|
|
||||||
34
src/AcDream.App/Rendering/LiveStaticAnimationResidency.cs
Normal file
34
src/AcDream.App/Rendering/LiveStaticAnimationResidency.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves static-workset residency against the canonical live incarnation.
|
||||||
|
/// DAT-only statics have no server identity and remain resident for their
|
||||||
|
/// enclosing landblock resource lifetime.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveStaticAnimationResidency
|
||||||
|
{
|
||||||
|
private readonly ILiveEntityRuntimeSource _runtime;
|
||||||
|
|
||||||
|
public LiveStaticAnimationResidency(ILiveEntityRuntimeSource runtime) =>
|
||||||
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
|
||||||
|
public bool IsResident(WorldEntity entity) =>
|
||||||
|
entity.ServerGuid == 0
|
||||||
|
|| (_runtime.Current?.TryGetRecord(entity.ServerGuid, out LiveEntityRecord resident) == true
|
||||||
|
&& resident.ProjectionKind is LiveEntityProjectionKind.World
|
||||||
|
&& ReferenceEquals(resident.WorldEntity, entity)
|
||||||
|
&& resident.IsSpatiallyProjected
|
||||||
|
&& resident.IsSpatiallyVisible
|
||||||
|
&& resident.FullCellId != 0);
|
||||||
|
|
||||||
|
public ulong ProjectionVersion(WorldEntity entity) =>
|
||||||
|
entity.ServerGuid == 0
|
||||||
|
? 0UL
|
||||||
|
: _runtime.Current?.TryGetRecord(entity.ServerGuid, out LiveEntityRecord resident) == true
|
||||||
|
&& ReferenceEquals(resident.WorldEntity, entity)
|
||||||
|
? resident.ProjectionMutationVersion
|
||||||
|
: ulong.MaxValue;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
@ -12,30 +13,28 @@ namespace AcDream.App.Rendering;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class StaticLiveRootCommitter
|
internal sealed class StaticLiveRootCommitter
|
||||||
{
|
{
|
||||||
private readonly Func<LiveEntityRuntime?> _runtime;
|
private readonly ILiveEntityRuntimeSource _runtime;
|
||||||
private readonly ShadowObjectRegistry _shadows;
|
private readonly ShadowObjectRegistry _shadows;
|
||||||
private readonly Func<(int X, int Y)> _liveCenter;
|
private readonly LiveWorldOriginState _origin;
|
||||||
private readonly Action<WorldEntity> _updateEffectRoot;
|
private readonly EntityEffectPoseRegistry _effectPoses;
|
||||||
|
|
||||||
public StaticLiveRootCommitter(
|
public StaticLiveRootCommitter(
|
||||||
Func<LiveEntityRuntime?> runtime,
|
ILiveEntityRuntimeSource runtime,
|
||||||
ShadowObjectRegistry shadows,
|
ShadowObjectRegistry shadows,
|
||||||
Func<(int X, int Y)> liveCenter,
|
LiveWorldOriginState origin,
|
||||||
Action<WorldEntity> updateEffectRoot)
|
EntityEffectPoseRegistry effectPoses)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
||||||
_liveCenter = liveCenter
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||||
?? throw new ArgumentNullException(nameof(liveCenter));
|
_effectPoses = effectPoses ?? throw new ArgumentNullException(nameof(effectPoses));
|
||||||
_updateEffectRoot = updateEffectRoot
|
|
||||||
?? throw new ArgumentNullException(nameof(updateEffectRoot));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Commit(WorldEntity entity, PhysicsBody body)
|
public bool Commit(WorldEntity entity, PhysicsBody body)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(entity);
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
ArgumentNullException.ThrowIfNull(body);
|
ArgumentNullException.ThrowIfNull(body);
|
||||||
LiveEntityRuntime? runtime = _runtime();
|
LiveEntityRuntime? runtime = _runtime.Current;
|
||||||
if (runtime is null
|
if (runtime is null
|
||||||
|| !runtime.TryGetRecord(entity.ServerGuid, out LiveEntityRecord record)
|
|| !runtime.TryGetRecord(entity.ServerGuid, out LiveEntityRecord record)
|
||||||
|| !ReferenceEquals(record.WorldEntity, entity)
|
|| !ReferenceEquals(record.WorldEntity, entity)
|
||||||
|
|
@ -46,15 +45,14 @@ internal sealed class StaticLiveRootCommitter
|
||||||
|
|
||||||
// The retained pose owner follows the CPhysicsObj root even while its
|
// The retained pose owner follows the CPhysicsObj root even while its
|
||||||
// mesh is hidden. Hidden is presentation state, not logical teardown.
|
// mesh is hidden. Hidden is presentation state, not logical teardown.
|
||||||
_updateEffectRoot(entity);
|
_effectPoses.UpdateRoot(entity);
|
||||||
|
|
||||||
// EffectPoseChanged observers run synchronously and may delete this
|
// EffectPoseChanged observers run synchronously and may delete this
|
||||||
// incarnation (or replace the GUID) before returning. Do not let the
|
// incarnation (or replace the GUID) before returning. Do not let the
|
||||||
// stale root restore collision shadows for an owner which no longer
|
// stale root restore collision shadows for an owner which no longer
|
||||||
// exists. This is the same callback-boundary lifetime rule used by the
|
// exists. This is the same callback-boundary lifetime rule used by the
|
||||||
// live schedulers and movement controllers.
|
// live schedulers and movement controllers.
|
||||||
if (!ReferenceEquals(_runtime(), runtime)
|
if (!runtime.TryGetRecord(entity.ServerGuid, out LiveEntityRecord current)
|
||||||
|| !runtime.TryGetRecord(entity.ServerGuid, out LiveEntityRecord current)
|
|
||||||
|| !ReferenceEquals(current, record)
|
|| !ReferenceEquals(current, record)
|
||||||
|| !ReferenceEquals(current.WorldEntity, entity)
|
|| !ReferenceEquals(current.WorldEntity, entity)
|
||||||
|| !ReferenceEquals(current.PhysicsBody, body))
|
|| !ReferenceEquals(current.PhysicsBody, body))
|
||||||
|
|
@ -76,15 +74,14 @@ internal sealed class StaticLiveRootCommitter
|
||||||
uint cellId = body.CellPosition.ObjCellId != 0
|
uint cellId = body.CellPosition.ObjCellId != 0
|
||||||
? body.CellPosition.ObjCellId
|
? body.CellPosition.ObjCellId
|
||||||
: record.FullCellId;
|
: record.FullCellId;
|
||||||
(int centerX, int centerY) = _liveCenter();
|
|
||||||
ShadowPositionSynchronizer.Sync(
|
ShadowPositionSynchronizer.Sync(
|
||||||
_shadows,
|
_shadows,
|
||||||
entity.Id,
|
entity.Id,
|
||||||
body.Position,
|
body.Position,
|
||||||
body.Orientation,
|
body.Orientation,
|
||||||
cellId,
|
cellId,
|
||||||
centerX,
|
_origin.CenterX,
|
||||||
centerY);
|
_origin.CenterY);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
246
src/AcDream.App/Update/LiveObjectFrameController.cs
Normal file
246
src/AcDream.App/Update/LiveObjectFrameController.cs
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Interaction;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
|
using AcDream.Core.Vfx;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
|
|
||||||
|
namespace AcDream.App.Update;
|
||||||
|
|
||||||
|
internal interface ILiveObjectFramePhase
|
||||||
|
{
|
||||||
|
void Tick(float deltaSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ILiveSessionFramePhase
|
||||||
|
{
|
||||||
|
void Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IPostNetworkCommandFramePhase
|
||||||
|
{
|
||||||
|
void RunPostNetworkCommandPhase();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ILiveSpatialReconcilePhase
|
||||||
|
{
|
||||||
|
void Reconcile();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IParticleRangeSource
|
||||||
|
{
|
||||||
|
float RangeMultiplier { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class SettingsParticleRangeSource : IParticleRangeSource
|
||||||
|
{
|
||||||
|
private readonly SettingsVM? _settings;
|
||||||
|
private readonly ParticleRange _fallback;
|
||||||
|
|
||||||
|
public SettingsParticleRangeSource(SettingsVM? settings, ParticleRange fallback)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
_fallback = fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float RangeMultiplier =>
|
||||||
|
(_settings?.DisplayDraft.ParticleRange ?? _fallback) == ParticleRange.Extended
|
||||||
|
? ParticleVisibilityController.ExtendedRangeMultiplier
|
||||||
|
: 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the shared effect-manager tail that follows ordinary and static
|
||||||
|
/// object worksets in acdream's registered TS-50/TS-51 adaptation.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveEffectFrameController
|
||||||
|
{
|
||||||
|
private readonly TranslucencyFadeManager _translucencyFades;
|
||||||
|
private readonly AnimationHookFrameQueue _animationHooks;
|
||||||
|
private readonly EntityEffectController _entityEffects;
|
||||||
|
private readonly ParticleHookSink _particleSink;
|
||||||
|
private readonly LiveEntityLightController _lights;
|
||||||
|
private readonly ParticleVisibilityController _particleVisibility;
|
||||||
|
private readonly ParticleSystem _particles;
|
||||||
|
private readonly PhysicsScriptRunner _scripts;
|
||||||
|
private readonly IPhysicsScriptTimeSource _scriptTime;
|
||||||
|
private readonly IParticleRangeSource _particleRange;
|
||||||
|
|
||||||
|
public LiveEffectFrameController(
|
||||||
|
TranslucencyFadeManager translucencyFades,
|
||||||
|
AnimationHookFrameQueue animationHooks,
|
||||||
|
EntityEffectController entityEffects,
|
||||||
|
ParticleHookSink particleSink,
|
||||||
|
LiveEntityLightController lights,
|
||||||
|
ParticleVisibilityController particleVisibility,
|
||||||
|
ParticleSystem particles,
|
||||||
|
PhysicsScriptRunner scripts,
|
||||||
|
IPhysicsScriptTimeSource scriptTime,
|
||||||
|
IParticleRangeSource particleRange)
|
||||||
|
{
|
||||||
|
_translucencyFades = translucencyFades
|
||||||
|
?? throw new ArgumentNullException(nameof(translucencyFades));
|
||||||
|
_animationHooks = animationHooks ?? throw new ArgumentNullException(nameof(animationHooks));
|
||||||
|
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
|
||||||
|
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
||||||
|
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||||
|
_particleVisibility = particleVisibility
|
||||||
|
?? throw new ArgumentNullException(nameof(particleVisibility));
|
||||||
|
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
||||||
|
_scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
|
||||||
|
_scriptTime = scriptTime ?? throw new ArgumentNullException(nameof(scriptTime));
|
||||||
|
_particleRange = particleRange ?? throw new ArgumentNullException(nameof(particleRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick(float deltaSeconds)
|
||||||
|
{
|
||||||
|
// Retail's ordinary-object UpdatePositionInternal @ 0x00512C30 calls
|
||||||
|
// CPhysicsObj::process_hooks @ 0x00511550 before its manager tail.
|
||||||
|
// acdream currently keeps non-AnimationDone hooks at this deferred
|
||||||
|
// shared boundary under TS-50; this extraction must not silently
|
||||||
|
// claim that adaptation is gone.
|
||||||
|
_translucencyFades.AdvanceAll(deltaSeconds);
|
||||||
|
_animationHooks.Drain();
|
||||||
|
_entityEffects.RefreshLiveOwnerPoses();
|
||||||
|
_particleSink.RefreshAttachedEmitters();
|
||||||
|
_lights.Refresh();
|
||||||
|
_particleVisibility.Apply(_particles, _particleRange.RangeMultiplier);
|
||||||
|
|
||||||
|
// Retail CPhysicsObj::UpdateObjectInternal @ 0x005156B0 advances the
|
||||||
|
// ordinary-object Particle manager before Script. acdream's shared
|
||||||
|
// once-per-host tail and its static-order difference remain TS-51.
|
||||||
|
_particles.Tick(deltaSeconds);
|
||||||
|
_scripts.Tick(_scriptTime.CurrentScriptTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the complete pre-network local/ordinary/static live-object phase.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
|
{
|
||||||
|
private readonly RetailInboundEventDispatcher _inboundEvents;
|
||||||
|
private readonly RetailLocalPlayerFrameController _localPlayerFrame;
|
||||||
|
private readonly SelectionInteractionController? _selectionInteractions;
|
||||||
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
|
private readonly ILocalPlayerIdentitySource _localPlayer;
|
||||||
|
private readonly LiveWorldOriginState _origin;
|
||||||
|
private readonly LiveEntityAnimationScheduler _animations;
|
||||||
|
private readonly RetailStaticAnimatingObjectScheduler _staticAnimations;
|
||||||
|
private readonly LiveEntityAnimationPresenter _animationPresenter;
|
||||||
|
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState>
|
||||||
|
_animatedEntities;
|
||||||
|
private readonly EquippedChildRenderController _equippedChildren;
|
||||||
|
private readonly LiveEffectFrameController _effects;
|
||||||
|
|
||||||
|
public LiveObjectFrameController(
|
||||||
|
RetailInboundEventDispatcher inboundEvents,
|
||||||
|
RetailLocalPlayerFrameController localPlayerFrame,
|
||||||
|
SelectionInteractionController? selectionInteractions,
|
||||||
|
LiveEntityRuntime liveEntities,
|
||||||
|
ILocalPlayerIdentitySource localPlayer,
|
||||||
|
LiveWorldOriginState origin,
|
||||||
|
LiveEntityAnimationScheduler animations,
|
||||||
|
RetailStaticAnimatingObjectScheduler staticAnimations,
|
||||||
|
LiveEntityAnimationPresenter animationPresenter,
|
||||||
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
|
||||||
|
EquippedChildRenderController equippedChildren,
|
||||||
|
LiveEffectFrameController effects)
|
||||||
|
{
|
||||||
|
_inboundEvents = inboundEvents ?? throw new ArgumentNullException(nameof(inboundEvents));
|
||||||
|
_localPlayerFrame = localPlayerFrame
|
||||||
|
?? throw new ArgumentNullException(nameof(localPlayerFrame));
|
||||||
|
_selectionInteractions = selectionInteractions;
|
||||||
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||||
|
_localPlayer = localPlayer ?? throw new ArgumentNullException(nameof(localPlayer));
|
||||||
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||||
|
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
||||||
|
_staticAnimations = staticAnimations
|
||||||
|
?? throw new ArgumentNullException(nameof(staticAnimations));
|
||||||
|
_animationPresenter = animationPresenter
|
||||||
|
?? throw new ArgumentNullException(nameof(animationPresenter));
|
||||||
|
_animatedEntities = animatedEntities
|
||||||
|
?? throw new ArgumentNullException(nameof(animatedEntities));
|
||||||
|
_equippedChildren = equippedChildren
|
||||||
|
?? throw new ArgumentNullException(nameof(equippedChildren));
|
||||||
|
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick(float deltaSeconds) =>
|
||||||
|
_inboundEvents.Run(
|
||||||
|
this,
|
||||||
|
deltaSeconds,
|
||||||
|
static (controller, elapsed) => controller.TickCore(elapsed));
|
||||||
|
|
||||||
|
private void TickCore(float deltaSeconds)
|
||||||
|
{
|
||||||
|
_localPlayerFrame.AdvanceBeforeNetwork(deltaSeconds);
|
||||||
|
_selectionInteractions?.DrainOutbound();
|
||||||
|
|
||||||
|
Vector3? playerPosition =
|
||||||
|
_liveEntities.MaterializedWorldEntities.TryGetValue(
|
||||||
|
_localPlayer.ServerGuid,
|
||||||
|
out WorldEntity? player)
|
||||||
|
? player.Position
|
||||||
|
: null;
|
||||||
|
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules =
|
||||||
|
_animations.Tick(
|
||||||
|
deltaSeconds,
|
||||||
|
playerPosition,
|
||||||
|
_localPlayerFrame.HiddenPartPoseDirty,
|
||||||
|
_origin.CenterX,
|
||||||
|
_origin.CenterY,
|
||||||
|
_animationPresenter.PrepareAnimation);
|
||||||
|
|
||||||
|
// CPhysics::UseTime @ 0x00509950 walks ordinary objects before the
|
||||||
|
// distinct static_animating_objects workset.
|
||||||
|
_staticAnimations.Tick(deltaSeconds);
|
||||||
|
if (_animatedEntities.Count > 0)
|
||||||
|
_animationPresenter.Present(schedules);
|
||||||
|
_equippedChildren.Tick();
|
||||||
|
// Retail CPhysicsObj::animate_static_object @ 0x00513DF0 runs
|
||||||
|
// Script -> Particle -> process_hooks. acdream intentionally retains
|
||||||
|
// the TS-51 static-order difference here: ProcessHooks precedes the
|
||||||
|
// shared once-per-host Particle -> Script tail below.
|
||||||
|
_staticAnimations.ProcessHooks();
|
||||||
|
_effects.Tick(deltaSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-composes spatial derivatives after authoritative root mutations without
|
||||||
|
/// advancing animation, physics, effects, particles, scripts, or fades.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcilePhase
|
||||||
|
{
|
||||||
|
private readonly EntityEffectController _entityEffects;
|
||||||
|
private readonly EquippedChildRenderController _equippedChildren;
|
||||||
|
private readonly ParticleHookSink _particleSink;
|
||||||
|
private readonly LiveEntityLightController _lights;
|
||||||
|
|
||||||
|
public LiveSpatialPresentationReconciler(
|
||||||
|
EntityEffectController entityEffects,
|
||||||
|
EquippedChildRenderController equippedChildren,
|
||||||
|
ParticleHookSink particleSink,
|
||||||
|
LiveEntityLightController lights)
|
||||||
|
{
|
||||||
|
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
|
||||||
|
_equippedChildren = equippedChildren
|
||||||
|
?? throw new ArgumentNullException(nameof(equippedChildren));
|
||||||
|
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
||||||
|
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reconcile()
|
||||||
|
{
|
||||||
|
_entityEffects.RefreshLiveOwnerPoses();
|
||||||
|
_equippedChildren.Tick();
|
||||||
|
_particleSink.RefreshAttachedEmitters();
|
||||||
|
_lights.Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/AcDream.App/World/LiveEntityRuntimeSlot.cs
Normal file
24
src/AcDream.App/World/LiveEntityRuntimeSlot.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
internal interface ILiveEntityRuntimeSource
|
||||||
|
{
|
||||||
|
LiveEntityRuntime? Current { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One-time composition bridge for the resource-activation/runtime cycle.
|
||||||
|
/// It owns no identity or entity state; all authority remains in the bound
|
||||||
|
/// <see cref="LiveEntityRuntime"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveEntityRuntimeSlot : ILiveEntityRuntimeSource
|
||||||
|
{
|
||||||
|
public LiveEntityRuntime? Current { get; private set; }
|
||||||
|
|
||||||
|
public void Bind(LiveEntityRuntime runtime)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
if (Current is not null)
|
||||||
|
throw new InvalidOperationException("The live entity runtime is already bound.");
|
||||||
|
Current = runtime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,21 +9,21 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
: IEnumerable<KeyValuePair<uint, TAnimation>>
|
: IEnumerable<KeyValuePair<uint, TAnimation>>
|
||||||
where TAnimation : class, ILiveEntityAnimationRuntime
|
where TAnimation : class, ILiveEntityAnimationRuntime
|
||||||
{
|
{
|
||||||
private readonly Func<LiveEntityRuntime?> _runtime;
|
private readonly ILiveEntityRuntimeSource _runtime;
|
||||||
private readonly List<KeyValuePair<uint, TAnimation>> _iterationSnapshot = new();
|
private readonly List<KeyValuePair<uint, TAnimation>> _iterationSnapshot = new();
|
||||||
|
|
||||||
public LiveEntityAnimationRuntimeView(Func<LiveEntityRuntime?> runtime) =>
|
internal LiveEntityAnimationRuntimeView(ILiveEntityRuntimeSource runtime) =>
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
|
||||||
public int Count => _runtime()?.SpatialAnimationRuntimes.Count ?? 0;
|
public int Count => _runtime.Current?.SpatialAnimationRuntimes.Count ?? 0;
|
||||||
public IEnumerable<uint> Keys =>
|
public IEnumerable<uint> Keys =>
|
||||||
_runtime()?.SpatialAnimationRuntimes.Keys ?? Array.Empty<uint>();
|
_runtime.Current?.SpatialAnimationRuntimes.Keys ?? Array.Empty<uint>();
|
||||||
|
|
||||||
public TAnimation this[uint localEntityId]
|
public TAnimation this[uint localEntityId]
|
||||||
{
|
{
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = _runtime()
|
LiveEntityRuntime runtime = _runtime.Current
|
||||||
?? throw new InvalidOperationException("Live entity runtime is not initialized.");
|
?? throw new InvalidOperationException("Live entity runtime is not initialized.");
|
||||||
if (!runtime.TryGetServerGuid(localEntityId, out uint guid))
|
if (!runtime.TryGetServerGuid(localEntityId, out uint guid))
|
||||||
throw new InvalidOperationException($"No live entity owns local id 0x{localEntityId:X8}.");
|
throw new InvalidOperationException($"No live entity owns local id 0x{localEntityId:X8}.");
|
||||||
|
|
@ -33,7 +33,7 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
|
|
||||||
public bool TryGetValue(uint localEntityId, out TAnimation animation)
|
public bool TryGetValue(uint localEntityId, out TAnimation animation)
|
||||||
{
|
{
|
||||||
if (_runtime()?.TryGetAnimationRuntime(localEntityId, out var found) == true
|
if (_runtime.Current?.TryGetAnimationRuntime(localEntityId, out var found) == true
|
||||||
&& found is TAnimation typed)
|
&& found is TAnimation typed)
|
||||||
{
|
{
|
||||||
animation = typed;
|
animation = typed;
|
||||||
|
|
@ -46,7 +46,7 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
|
|
||||||
public bool Remove(uint localEntityId)
|
public bool Remove(uint localEntityId)
|
||||||
{
|
{
|
||||||
LiveEntityRuntime? runtime = _runtime();
|
LiveEntityRuntime? runtime = _runtime.Current;
|
||||||
return runtime is not null
|
return runtime is not null
|
||||||
&& runtime.TryGetServerGuid(localEntityId, out uint guid)
|
&& runtime.TryGetServerGuid(localEntityId, out uint guid)
|
||||||
&& runtime.ClearAnimationRuntime(guid);
|
&& runtime.ClearAnimationRuntime(guid);
|
||||||
|
|
@ -57,7 +57,7 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
public void CopySpatialIdsTo(HashSet<uint> destination)
|
public void CopySpatialIdsTo(HashSet<uint> destination)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(destination);
|
ArgumentNullException.ThrowIfNull(destination);
|
||||||
LiveEntityRuntime? runtime = _runtime();
|
LiveEntityRuntime? runtime = _runtime.Current;
|
||||||
if (runtime is null)
|
if (runtime is null)
|
||||||
destination.Clear();
|
destination.Clear();
|
||||||
else
|
else
|
||||||
|
|
@ -66,7 +66,7 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
|
|
||||||
public Enumerator GetEnumerator()
|
public Enumerator GetEnumerator()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime? runtime = _runtime();
|
LiveEntityRuntime? runtime = _runtime.Current;
|
||||||
if (runtime is null)
|
if (runtime is null)
|
||||||
_iterationSnapshot.Clear();
|
_iterationSnapshot.Clear();
|
||||||
else
|
else
|
||||||
|
|
@ -135,21 +135,21 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
public sealed class LiveEntityRemoteMotionRuntimeView<TRemoteMotion>
|
public sealed class LiveEntityRemoteMotionRuntimeView<TRemoteMotion>
|
||||||
where TRemoteMotion : class, ILiveEntityRemoteMotionRuntime
|
where TRemoteMotion : class, ILiveEntityRemoteMotionRuntime
|
||||||
{
|
{
|
||||||
private readonly Func<LiveEntityRuntime?> _runtime;
|
private readonly ILiveEntityRuntimeSource _runtime;
|
||||||
|
|
||||||
public LiveEntityRemoteMotionRuntimeView(Func<LiveEntityRuntime?> runtime) =>
|
internal LiveEntityRemoteMotionRuntimeView(ILiveEntityRuntimeSource runtime) =>
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
|
||||||
public TRemoteMotion this[uint serverGuid]
|
public TRemoteMotion this[uint serverGuid]
|
||||||
{
|
{
|
||||||
set => (_runtime()
|
set => (_runtime.Current
|
||||||
?? throw new InvalidOperationException("Live entity runtime is not initialized."))
|
?? throw new InvalidOperationException("Live entity runtime is not initialized."))
|
||||||
.SetRemoteMotionRuntime(serverGuid, value);
|
.SetRemoteMotionRuntime(serverGuid, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetValue(uint serverGuid, out TRemoteMotion motion)
|
public bool TryGetValue(uint serverGuid, out TRemoteMotion motion)
|
||||||
{
|
{
|
||||||
if (_runtime()?.TryGetRemoteMotionRuntime(serverGuid, out var found) == true
|
if (_runtime.Current?.TryGetRemoteMotionRuntime(serverGuid, out var found) == true
|
||||||
&& found is TRemoteMotion typed)
|
&& found is TRemoteMotion typed)
|
||||||
{
|
{
|
||||||
motion = typed;
|
motion = typed;
|
||||||
|
|
@ -161,5 +161,5 @@ public sealed class LiveEntityRemoteMotionRuntimeView<TRemoteMotion>
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Remove(uint serverGuid) =>
|
public bool Remove(uint serverGuid) =>
|
||||||
_runtime()?.ClearRemoteMotionRuntime(serverGuid) == true;
|
_runtime.Current?.ClearRemoteMotionRuntime(serverGuid) == true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Net;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
|
||||||
namespace AcDream.App.World;
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
|
@ -14,36 +17,37 @@ namespace AcDream.App.World;
|
||||||
/// from freezing an action due to complete this frame while ensuring the
|
/// from freezing an action due to complete this frame while ensuring the
|
||||||
/// periodic AutonomousPosition check observes accepted inbound state.
|
/// periodic AutonomousPosition check observes accepted inbound state.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class RetailLiveFrameCoordinator
|
internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
{
|
{
|
||||||
private readonly Action<float> _advanceObjectRuntime;
|
private readonly ILiveObjectFramePhase _objects;
|
||||||
private readonly Action _dispatchInboundNetwork;
|
private readonly GpuWorldState _worldState;
|
||||||
private readonly Action _runPostNetworkCommands;
|
private readonly ILiveSessionFramePhase _session;
|
||||||
private readonly Action _reconcileSpatialPresentation;
|
private readonly IPostNetworkCommandFramePhase _localPlayer;
|
||||||
|
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
||||||
|
|
||||||
public RetailLiveFrameCoordinator(
|
public RetailLiveFrameCoordinator(
|
||||||
Action<float> advanceObjectRuntime,
|
ILiveObjectFramePhase objects,
|
||||||
Action dispatchInboundNetwork,
|
GpuWorldState worldState,
|
||||||
Action runPostNetworkCommands,
|
ILiveSessionFramePhase session,
|
||||||
Action reconcileSpatialPresentation)
|
IPostNetworkCommandFramePhase localPlayer,
|
||||||
|
ILiveSpatialReconcilePhase spatialReconciler)
|
||||||
{
|
{
|
||||||
_advanceObjectRuntime = advanceObjectRuntime
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||||
?? throw new ArgumentNullException(nameof(advanceObjectRuntime));
|
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||||
_dispatchInboundNetwork = dispatchInboundNetwork
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
?? throw new ArgumentNullException(nameof(dispatchInboundNetwork));
|
_localPlayer = localPlayer ?? throw new ArgumentNullException(nameof(localPlayer));
|
||||||
_runPostNetworkCommands = runPostNetworkCommands
|
_spatialReconciler = spatialReconciler
|
||||||
?? throw new ArgumentNullException(nameof(runPostNetworkCommands));
|
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
||||||
_reconcileSpatialPresentation = reconcileSpatialPresentation
|
|
||||||
?? throw new ArgumentNullException(nameof(reconcileSpatialPresentation));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(float deltaSeconds)
|
public void Tick(float deltaSeconds)
|
||||||
{
|
{
|
||||||
float frameDelta = (float)NormalizeDeltaSeconds(deltaSeconds);
|
float frameDelta = (float)NormalizeDeltaSeconds(deltaSeconds);
|
||||||
_advanceObjectRuntime(frameDelta);
|
_objects.Tick(frameDelta);
|
||||||
_dispatchInboundNetwork();
|
using (_worldState.BeginMutationBatch())
|
||||||
_runPostNetworkCommands();
|
_session.Tick();
|
||||||
_reconcileSpatialPresentation();
|
_localPlayer.RunPostNetworkCommandPhase();
|
||||||
|
_spatialReconciler.Reconcile();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
||||||
|
|
|
||||||
27
tests/AcDream.App.Tests/FramePhaseTestDoubles.cs
Normal file
27
tests/AcDream.App.Tests/FramePhaseTestDoubles.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
using AcDream.App.Update;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests;
|
||||||
|
|
||||||
|
internal sealed class TestLiveObjectFramePhase(Action<float> tick)
|
||||||
|
: ILiveObjectFramePhase
|
||||||
|
{
|
||||||
|
public void Tick(float deltaSeconds) => tick(deltaSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestLiveSessionFramePhase(Action tick)
|
||||||
|
: ILiveSessionFramePhase
|
||||||
|
{
|
||||||
|
public void Tick() => tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestPostNetworkCommandFramePhase(Action run)
|
||||||
|
: IPostNetworkCommandFramePhase
|
||||||
|
{
|
||||||
|
public void RunPostNetworkCommandPhase() => run();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestLiveSpatialReconcilePhase(Action reconcile)
|
||||||
|
: ILiveSpatialReconcilePhase
|
||||||
|
{
|
||||||
|
public void Reconcile() => reconcile();
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
|
@ -33,22 +34,23 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
||||||
postNetwork.Add(owner.State);
|
postNetwork.Add(owner.State);
|
||||||
});
|
});
|
||||||
var live = new RetailLiveFrameCoordinator(
|
var live = new RetailLiveFrameCoordinator(
|
||||||
dt =>
|
new TestLiveObjectFramePhase(dt =>
|
||||||
{
|
{
|
||||||
calls.Add("objects");
|
calls.Add("objects");
|
||||||
local.AdvanceBeforeNetwork(dt);
|
local.AdvanceBeforeNetwork(dt);
|
||||||
},
|
}),
|
||||||
() =>
|
new GpuWorldState(),
|
||||||
|
new TestLiveSessionFramePhase(() =>
|
||||||
{
|
{
|
||||||
calls.Add("f751");
|
calls.Add("f751");
|
||||||
controller.State = PlayerState.PortalSpace;
|
controller.State = PlayerState.PortalSpace;
|
||||||
},
|
}),
|
||||||
() =>
|
new TestPostNetworkCommandFramePhase(() =>
|
||||||
{
|
{
|
||||||
calls.Add("commands");
|
calls.Add("commands");
|
||||||
local.RunPostNetworkCommandPhase();
|
local.RunPostNetworkCommandPhase();
|
||||||
},
|
}),
|
||||||
() => calls.Add("spatial"));
|
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")));
|
||||||
|
|
||||||
// CPhysicsObj::update_object admits manager time only after the strict
|
// CPhysicsObj::update_object admits manager time only after the strict
|
||||||
// minimum quantum. Use one admitted object tick; the test's concern is
|
// minimum quantum. Use one admitted object tick; the test's concern is
|
||||||
|
|
@ -118,10 +120,12 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
||||||
sendPreNetwork: (_, _, _) => { },
|
sendPreNetwork: (_, _, _) => { },
|
||||||
sendPostNetwork: (_, _) => { });
|
sendPostNetwork: (_, _) => { });
|
||||||
var live = new RetailLiveFrameCoordinator(
|
var live = new RetailLiveFrameCoordinator(
|
||||||
local.AdvanceBeforeNetwork,
|
new TestLiveObjectFramePhase(local.AdvanceBeforeNetwork),
|
||||||
() => projectedRoot = new Vector3(999f, 999f, 999f),
|
new GpuWorldState(),
|
||||||
local.RunPostNetworkCommandPhase,
|
new TestLiveSessionFramePhase(
|
||||||
() => { });
|
() => projectedRoot = new Vector3(999f, 999f, 999f)),
|
||||||
|
local,
|
||||||
|
new TestLiveSpatialReconcilePhase(() => { }));
|
||||||
|
|
||||||
live.Tick(1f / 60f);
|
live.Tick(1f / 60f);
|
||||||
float timeAfterOneTick = controller.SimTimeSeconds;
|
float timeAfterOneTick = controller.SimTimeSeconds;
|
||||||
|
|
|
||||||
|
|
@ -1430,6 +1430,24 @@ public sealed class ProjectileControllerTests
|
||||||
|
|
||||||
private sealed class Fixture
|
private sealed class Fixture
|
||||||
{
|
{
|
||||||
|
private sealed class SetupResolver : IProjectileSetupResolver
|
||||||
|
{
|
||||||
|
private readonly Func<Setup?> _resolve;
|
||||||
|
|
||||||
|
internal SetupResolver(Func<Setup?> resolve) => _resolve = resolve;
|
||||||
|
|
||||||
|
public Setup? Resolve(uint setupId) => _resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RootPosePublisher : IEntityRootPosePublisher
|
||||||
|
{
|
||||||
|
private readonly Action<WorldEntity> _publish;
|
||||||
|
|
||||||
|
internal RootPosePublisher(Action<WorldEntity> publish) => _publish = publish;
|
||||||
|
|
||||||
|
public void UpdateRoot(WorldEntity entity) => _publish(entity);
|
||||||
|
}
|
||||||
|
|
||||||
internal Fixture(
|
internal Fixture(
|
||||||
bool loadInitialLandblock = true,
|
bool loadInitialLandblock = true,
|
||||||
PhysicsEngine? physicsEngine = null,
|
PhysicsEngine? physicsEngine = null,
|
||||||
|
|
@ -1439,12 +1457,15 @@ public sealed class ProjectileControllerTests
|
||||||
Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
Live = new LiveEntityRuntime(Spatial, Resources);
|
Live = new LiveEntityRuntime(Spatial, Resources);
|
||||||
Engine = physicsEngine ?? new PhysicsEngine();
|
Engine = physicsEngine ?? new PhysicsEngine();
|
||||||
|
Origin.Recenter(1, 1);
|
||||||
Controller = new ProjectileController(
|
Controller = new ProjectileController(
|
||||||
Live,
|
Live,
|
||||||
Engine,
|
Engine,
|
||||||
_ => ResolvedSetup,
|
new SetupResolver(() => ResolvedSetup),
|
||||||
publishRootPose ?? (entity => Poses.UpdateRoot(entity)),
|
new RootPosePublisher(
|
||||||
() => LiveCenter);
|
publishRootPose ?? new Action<WorldEntity>(
|
||||||
|
entity => { _ = Poses.UpdateRoot(entity); })),
|
||||||
|
Origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal GpuWorldState Spatial { get; } = new();
|
internal GpuWorldState Spatial { get; } = new();
|
||||||
|
|
@ -1452,8 +1473,8 @@ public sealed class ProjectileControllerTests
|
||||||
internal LiveEntityRuntime Live { get; }
|
internal LiveEntityRuntime Live { get; }
|
||||||
internal PhysicsEngine Engine { get; }
|
internal PhysicsEngine Engine { get; }
|
||||||
internal EntityEffectPoseRegistry Poses { get; } = new();
|
internal EntityEffectPoseRegistry Poses { get; } = new();
|
||||||
|
internal LiveWorldOriginState Origin { get; } = new();
|
||||||
internal Setup ResolvedSetup { get; set; } = ProjectileSetup();
|
internal Setup ResolvedSetup { get; set; } = ProjectileSetup();
|
||||||
internal (int X, int Y) LiveCenter { get; set; } = (1, 1);
|
|
||||||
internal ProjectileController Controller { get; }
|
internal ProjectileController Controller { get; }
|
||||||
|
|
||||||
internal LiveEntityRecord Spawn(
|
internal LiveEntityRecord Spawn(
|
||||||
|
|
|
||||||
|
|
@ -364,13 +364,29 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
LiveEntityRuntime live,
|
LiveEntityRuntime live,
|
||||||
EntityEffectPoseRegistry poses,
|
EntityEffectPoseRegistry poses,
|
||||||
Context context) => new(
|
Context context) => new(
|
||||||
() => live,
|
live,
|
||||||
() => null,
|
new EmptyStaticPartFrameSource(),
|
||||||
poses,
|
poses,
|
||||||
context,
|
context,
|
||||||
AnimationPresentationDiagnostics.Disabled,
|
AnimationPresentationDiagnostics.Disabled,
|
||||||
hidePartIndex: -1);
|
hidePartIndex: -1);
|
||||||
|
|
||||||
|
private sealed class EmptyStaticPartFrameSource : ILiveStaticPartFrameSource
|
||||||
|
{
|
||||||
|
public bool TryTakeLivePartFrames(
|
||||||
|
LiveEntityRecord record,
|
||||||
|
WorldEntity entity,
|
||||||
|
LiveEntityAnimationState animation,
|
||||||
|
ulong objectClockEpoch,
|
||||||
|
ulong projectionMutationVersion,
|
||||||
|
ulong presentationRevision,
|
||||||
|
out IReadOnlyList<PartTransform> partFrames)
|
||||||
|
{
|
||||||
|
partFrames = Array.Empty<PartTransform>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Schedules(
|
private static IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Schedules(
|
||||||
Fixture fixture,
|
Fixture fixture,
|
||||||
IReadOnlyList<PartTransform> frames) =>
|
IReadOnlyList<PartTransform> frames) =>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Core.Vfx;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -629,14 +632,42 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
var ordinaryPhysics = new LiveEntityOrdinaryPhysicsUpdater(
|
var ordinaryPhysics = new LiveEntityOrdinaryPhysicsUpdater(
|
||||||
physics,
|
physics,
|
||||||
(_, _) => (0.48f, 1.835f));
|
(_, _) => (0.48f, 1.835f));
|
||||||
|
var identity = new LocalPlayerIdentityState { ServerGuid = localPlayerGuid };
|
||||||
|
var poses = new EntityEffectPoseRegistry();
|
||||||
|
foreach (WorldEntity entity in live.MaterializedWorldEntities.Values)
|
||||||
|
poses.PublishMeshRefs(entity);
|
||||||
|
IAnimationHookCaptureSink animationHooks = captureHooks is null
|
||||||
|
? new AnimationHookCaptureSink(new AnimationHookFrameQueue(
|
||||||
|
new AnimationHookRouter(),
|
||||||
|
poses))
|
||||||
|
: new TestAnimationHookCaptureSink(captureHooks);
|
||||||
|
projectiles ??= new ProjectileController(live, physics);
|
||||||
return new LiveEntityAnimationScheduler(
|
return new LiveEntityAnimationScheduler(
|
||||||
() => live,
|
live,
|
||||||
() => localPlayerGuid,
|
identity,
|
||||||
remotePhysics,
|
remotePhysics,
|
||||||
ordinaryPhysics,
|
ordinaryPhysics,
|
||||||
() => projectiles,
|
projectiles,
|
||||||
publishRootPose ?? (_ => { }),
|
new TestRootPosePublisher(poses, publishRootPose),
|
||||||
captureHooks ?? ((_, _) => { }));
|
animationHooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestAnimationHookCaptureSink(
|
||||||
|
Action<uint, AnimationSequencer> capture) : IAnimationHookCaptureSink
|
||||||
|
{
|
||||||
|
public void Capture(uint ownerLocalId, AnimationSequencer sequencer) =>
|
||||||
|
capture(ownerLocalId, sequencer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestRootPosePublisher(
|
||||||
|
EntityEffectPoseRegistry poses,
|
||||||
|
Action<WorldEntity>? published) : IEntityRootPosePublisher
|
||||||
|
{
|
||||||
|
public void UpdateRoot(WorldEntity entity)
|
||||||
|
{
|
||||||
|
poses.UpdateRoot(entity);
|
||||||
|
published?.Invoke(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (LiveEntityRuntime Live, LiveEntityRecord Record,
|
private static (LiveEntityRuntime Live, LiveEntityRecord Record,
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
[InlineData("DispatchRemoteInboundMotion")]
|
[InlineData("DispatchRemoteInboundMotion")]
|
||||||
[InlineData("CreateRemoteMotion")]
|
[InlineData("CreateRemoteMotion")]
|
||||||
[InlineData("WillAdvanceRemoteMotion")]
|
[InlineData("WillAdvanceRemoteMotion")]
|
||||||
|
[InlineData("AdvanceLiveObjectRuntime")]
|
||||||
|
[InlineData("AdvanceLiveObjectRuntimeCore")]
|
||||||
|
[InlineData("ReconcileLiveObjectSpatialPresentation")]
|
||||||
|
[InlineData("CaptureAnimationHooks")]
|
||||||
public void GameWindow_DoesNotReacquireExtractedLiveEntityBodies(string methodName)
|
public void GameWindow_DoesNotReacquireExtractedLiveEntityBodies(string methodName)
|
||||||
{
|
{
|
||||||
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateImplementation));
|
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateImplementation));
|
||||||
|
|
@ -73,6 +77,13 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
[InlineData(typeof(LiveEntityMotionRuntimeController))]
|
[InlineData(typeof(LiveEntityMotionRuntimeController))]
|
||||||
[InlineData(typeof(LiveEntityInboundAuthorityGate))]
|
[InlineData(typeof(LiveEntityInboundAuthorityGate))]
|
||||||
[InlineData(typeof(DeferredLiveEntityMotionRuntimeBindings))]
|
[InlineData(typeof(DeferredLiveEntityMotionRuntimeBindings))]
|
||||||
|
[InlineData(typeof(AcDream.App.Update.LiveObjectFrameController))]
|
||||||
|
[InlineData(typeof(AcDream.App.Update.LiveEffectFrameController))]
|
||||||
|
[InlineData(typeof(AcDream.App.Update.LiveSpatialPresentationReconciler))]
|
||||||
|
[InlineData(typeof(LiveEntityAnimationPresenter))]
|
||||||
|
[InlineData(typeof(LiveAnimationPresentationContext))]
|
||||||
|
[InlineData(typeof(StaticLiveRootCommitter))]
|
||||||
|
[InlineData(typeof(RetailLiveFrameCoordinator))]
|
||||||
public void ExtractedHelpers_DoNotOwnGuidIndexesOrBackendState(Type helperType)
|
public void ExtractedHelpers_DoNotOwnGuidIndexesOrBackendState(Type helperType)
|
||||||
{
|
{
|
||||||
foreach (FieldInfo field in helperType.GetFields(
|
foreach (FieldInfo field in helperType.GetFields(
|
||||||
|
|
@ -83,6 +94,89 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
string typeName = field.FieldType.FullName ?? field.FieldType.Name;
|
string typeName = field.FieldType.FullName ?? field.FieldType.Name;
|
||||||
Assert.DoesNotContain("Silk.NET", typeName, StringComparison.Ordinal);
|
Assert.DoesNotContain("Silk.NET", typeName, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("OpenGL", typeName, StringComparison.OrdinalIgnoreCase);
|
Assert.DoesNotContain("OpenGL", typeName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.NotEqual(typeof(GameWindow), field.FieldType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractedUpdateOwners_DoNotRetainAnonymousCallbacks()
|
||||||
|
{
|
||||||
|
Type[] owners =
|
||||||
|
[
|
||||||
|
typeof(AcDream.App.Update.LiveObjectFrameController),
|
||||||
|
typeof(AcDream.App.Update.LiveEffectFrameController),
|
||||||
|
typeof(AcDream.App.Update.LiveSpatialPresentationReconciler),
|
||||||
|
typeof(LiveEntityAnimationScheduler),
|
||||||
|
typeof(LiveEntityAnimationPresenter),
|
||||||
|
typeof(LiveAnimationPresentationContext),
|
||||||
|
typeof(StaticLiveRootCommitter),
|
||||||
|
typeof(RetailLiveFrameCoordinator),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (Type owner in owners)
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
owner.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(typeof(ILiveAnimationPresentationContext).IsAssignableFrom(
|
||||||
|
typeof(GameWindow)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeViewsAndProjectileController_RetainTypedSourcesNotWindowClosures()
|
||||||
|
{
|
||||||
|
FieldInfo animationRuntime = Assert.Single(
|
||||||
|
typeof(LiveEntityAnimationRuntimeView<LiveEntityAnimationState>)
|
||||||
|
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.Name == "_runtime");
|
||||||
|
FieldInfo remoteRuntime = Assert.Single(
|
||||||
|
typeof(LiveEntityRemoteMotionRuntimeView<RemoteMotion>)
|
||||||
|
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.Name == "_runtime");
|
||||||
|
Assert.Equal(typeof(ILiveEntityRuntimeSource), animationRuntime.FieldType);
|
||||||
|
Assert.Equal(typeof(ILiveEntityRuntimeSource), remoteRuntime.FieldType);
|
||||||
|
|
||||||
|
FieldInfo[] projectileFields = typeof(ProjectileController).GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
Assert.Equal(
|
||||||
|
typeof(IProjectileSetupResolver),
|
||||||
|
Assert.Single(projectileFields, field => field.Name == "_setupResolver").FieldType);
|
||||||
|
Assert.Equal(
|
||||||
|
typeof(IEntityRootPosePublisher),
|
||||||
|
Assert.Single(projectileFields, field => field.Name == "_rootPoses").FieldType);
|
||||||
|
Assert.Equal(
|
||||||
|
typeof(LiveWorldOriginState),
|
||||||
|
Assert.Single(projectileFields, field => field.Name == "_origin").FieldType);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
projectileFields,
|
||||||
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType)
|
||||||
|
&& !field.Name.Contains("DiagnosticSink", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
string source = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>",
|
||||||
|
source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>",
|
||||||
|
source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("new AcDream.App.Physics.DatProjectileSetupResolver", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||||
|
return directory.FullName;
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,10 +202,7 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
|
||||||
lifecycle);
|
lifecycle);
|
||||||
Projectiles = new ProjectileController(
|
Projectiles = new ProjectileController(
|
||||||
Live,
|
Live,
|
||||||
Physics,
|
Physics);
|
||||||
_ => null,
|
|
||||||
_ => { },
|
|
||||||
() => (0, 0));
|
|
||||||
Controller = new LiveEntityProjectionWithdrawalController(
|
Controller = new LiveEntityProjectionWithdrawalController(
|
||||||
Live,
|
Live,
|
||||||
Projectiles,
|
Projectiles,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,20 @@ namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
public sealed class LiveEntityRuntimeTests
|
public sealed class LiveEntityRuntimeTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeSlot_BindsExactlyOnceWithoutOwningEntityState()
|
||||||
|
{
|
||||||
|
var spatial = new GpuWorldState();
|
||||||
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
|
var slot = new LiveEntityRuntimeSlot();
|
||||||
|
|
||||||
|
Assert.Null(slot.Current);
|
||||||
|
slot.Bind(runtime);
|
||||||
|
|
||||||
|
Assert.Same(runtime, slot.Current);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => slot.Bind(runtime));
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingResources : ILiveEntityResourceLifecycle
|
private sealed class RecordingResources : ILiveEntityResourceLifecycle
|
||||||
{
|
{
|
||||||
public int RegisterCount { get; private set; }
|
public int RegisterCount { get; private set; }
|
||||||
|
|
@ -245,11 +259,18 @@ public sealed class LiveEntityRuntimeTests
|
||||||
landblockId: 0x01010000u,
|
landblockId: 0x01010000u,
|
||||||
seedCellId: 0x01010001u);
|
seedCellId: 0x01010001u);
|
||||||
int poseUpdates = 0;
|
int poseUpdates = 0;
|
||||||
|
var runtimeSlot = new LiveEntityRuntimeSlot();
|
||||||
|
runtimeSlot.Bind(runtime);
|
||||||
|
var origin = new LiveWorldOriginState();
|
||||||
|
origin.Recenter(1, 1);
|
||||||
|
var poses = new EntityEffectPoseRegistry();
|
||||||
|
poses.PublishMeshRefs(entity);
|
||||||
|
poses.EffectPoseChanged += _ => poseUpdates++;
|
||||||
var committer = new StaticLiveRootCommitter(
|
var committer = new StaticLiveRootCommitter(
|
||||||
() => runtime,
|
runtimeSlot,
|
||||||
shadows,
|
shadows,
|
||||||
() => (1, 1),
|
origin,
|
||||||
_ => poseUpdates++);
|
poses);
|
||||||
|
|
||||||
body.SetFrameInCurrentCell(
|
body.SetFrameInCurrentCell(
|
||||||
body.Position,
|
body.Position,
|
||||||
|
|
@ -282,13 +303,14 @@ public sealed class LiveEntityRuntimeTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StaticRootCommit_BeforeLiveRuntimeCompositionIsSafeNoOp()
|
public void StaticRootCommit_BeforeLiveRuntimeCompositionIsSafeNoOp()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime? runtime = null;
|
|
||||||
int poseUpdates = 0;
|
int poseUpdates = 0;
|
||||||
|
var poses = new EntityEffectPoseRegistry();
|
||||||
|
poses.EffectPoseChanged += _ => poseUpdates++;
|
||||||
var committer = new StaticLiveRootCommitter(
|
var committer = new StaticLiveRootCommitter(
|
||||||
() => runtime,
|
new LiveEntityRuntimeSlot(),
|
||||||
new ShadowObjectRegistry(),
|
new ShadowObjectRegistry(),
|
||||||
() => (0, 0),
|
new LiveWorldOriginState(),
|
||||||
_ => poseUpdates++);
|
poses);
|
||||||
|
|
||||||
Assert.False(committer.Commit(
|
Assert.False(committer.Commit(
|
||||||
Entity(0x7F000001u, 0x70000001u),
|
Entity(0x7F000001u, 0x70000001u),
|
||||||
|
|
@ -347,26 +369,33 @@ public sealed class LiveEntityRuntimeTests
|
||||||
worldOffsetY: 0f,
|
worldOffsetY: 0f,
|
||||||
landblockId: 0x01010000u,
|
landblockId: 0x01010000u,
|
||||||
seedCellId: 0x01010001u);
|
seedCellId: 0x01010001u);
|
||||||
|
var runtimeSlot = new LiveEntityRuntimeSlot();
|
||||||
|
runtimeSlot.Bind(runtime);
|
||||||
|
var origin = new LiveWorldOriginState();
|
||||||
|
origin.Recenter(1, 1);
|
||||||
|
var poses = new EntityEffectPoseRegistry();
|
||||||
|
poses.PublishMeshRefs(oldEntity);
|
||||||
|
poses.EffectPoseChanged += _ =>
|
||||||
|
{
|
||||||
|
Assert.True(shadows.Suspend(oldEntity.Id));
|
||||||
|
Assert.True(runtime.UnregisterLiveEntity(
|
||||||
|
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
||||||
|
isLocalPlayer: false));
|
||||||
|
runtime.RegisterLiveEntity(Spawn(
|
||||||
|
guid,
|
||||||
|
instance: 2,
|
||||||
|
positionSequence: 1,
|
||||||
|
cell: 0x01010001u));
|
||||||
|
runtime.MaterializeLiveEntity(
|
||||||
|
guid,
|
||||||
|
0x01010001u,
|
||||||
|
id => Entity(id, guid));
|
||||||
|
};
|
||||||
var committer = new StaticLiveRootCommitter(
|
var committer = new StaticLiveRootCommitter(
|
||||||
() => runtime,
|
runtimeSlot,
|
||||||
shadows,
|
shadows,
|
||||||
() => (1, 1),
|
origin,
|
||||||
_ =>
|
poses);
|
||||||
{
|
|
||||||
Assert.True(shadows.Suspend(oldEntity.Id));
|
|
||||||
Assert.True(runtime.UnregisterLiveEntity(
|
|
||||||
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
|
||||||
isLocalPlayer: false));
|
|
||||||
runtime.RegisterLiveEntity(Spawn(
|
|
||||||
guid,
|
|
||||||
instance: 2,
|
|
||||||
positionSequence: 1,
|
|
||||||
cell: 0x01010001u));
|
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
guid,
|
|
||||||
0x01010001u,
|
|
||||||
id => Entity(id, guid));
|
|
||||||
});
|
|
||||||
|
|
||||||
oldBody.SetFrameInCurrentCell(
|
oldBody.SetFrameInCurrentCell(
|
||||||
oldBody.Position,
|
oldBody.Position,
|
||||||
|
|
@ -453,7 +482,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
var animation = new AnimationRuntime(entity);
|
var animation = new AnimationRuntime(entity);
|
||||||
runtime.SetAnimationRuntime(guid, animation);
|
runtime.SetAnimationRuntime(guid, animation);
|
||||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(() => runtime);
|
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(BoundSlot(runtime));
|
||||||
|
|
||||||
Assert.Equal(1, view.Count);
|
Assert.Equal(1, view.Count);
|
||||||
Assert.Equal(entity.Id, Assert.Single(view.Keys));
|
Assert.Equal(entity.Id, Assert.Single(view.Keys));
|
||||||
|
|
@ -489,7 +518,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
|
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
|
||||||
}
|
}
|
||||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(() => runtime);
|
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(BoundSlot(runtime));
|
||||||
var visited = new List<uint>();
|
var visited = new List<uint>();
|
||||||
|
|
||||||
foreach (KeyValuePair<uint, AnimationRuntime> pair in view)
|
foreach (KeyValuePair<uint, AnimationRuntime> pair in view)
|
||||||
|
|
@ -519,7 +548,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
|
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
|
||||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(
|
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(
|
||||||
() => runtime);
|
BoundSlot(runtime));
|
||||||
var ids = new HashSet<uint>();
|
var ids = new HashSet<uint>();
|
||||||
|
|
||||||
view.CopySpatialIdsTo(ids);
|
view.CopySpatialIdsTo(ids);
|
||||||
|
|
@ -2803,6 +2832,13 @@ public sealed class LiveEntityRuntimeTests
|
||||||
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
||||||
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
||||||
|
|
||||||
|
private static LiveEntityRuntimeSlot BoundSlot(LiveEntityRuntime runtime)
|
||||||
|
{
|
||||||
|
var slot = new LiveEntityRuntimeSlot();
|
||||||
|
slot.Bind(runtime);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
private static void ResolveParent(LiveEntityRuntime runtime, uint childGuid) =>
|
private static void ResolveParent(LiveEntityRuntime runtime, uint childGuid) =>
|
||||||
runtime.ParentAttachments.Resolve(
|
runtime.ParentAttachments.Resolve(
|
||||||
childGuid,
|
childGuid,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Update;
|
||||||
|
using AcDream.UI.Abstractions.Input;
|
||||||
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
|
using Silk.NET.Input;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
|
public sealed class LiveObjectFrameControllerTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ParticleRange.Retail, 1f)]
|
||||||
|
[InlineData(
|
||||||
|
ParticleRange.Extended,
|
||||||
|
ParticleVisibilityController.ExtendedRangeMultiplier)]
|
||||||
|
public void ParticleRange_NullSettingsUsesConfiguredFallback(
|
||||||
|
ParticleRange fallback,
|
||||||
|
float expected)
|
||||||
|
{
|
||||||
|
var source = new SettingsParticleRangeSource(null, fallback);
|
||||||
|
|
||||||
|
Assert.Equal(expected, source.RangeMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParticleRange_LiveSettingsDraftOverridesFallbackAndUpdatesImmediately()
|
||||||
|
{
|
||||||
|
SettingsVM settings = CreateSettings();
|
||||||
|
var source = new SettingsParticleRangeSource(settings, ParticleRange.Retail);
|
||||||
|
|
||||||
|
settings.SetDisplay(settings.DisplayDraft with { ParticleRange = ParticleRange.Retail });
|
||||||
|
Assert.Equal(1f, source.RangeMultiplier);
|
||||||
|
|
||||||
|
settings.SetDisplay(settings.DisplayDraft with { ParticleRange = ParticleRange.Extended });
|
||||||
|
Assert.Equal(
|
||||||
|
ParticleVisibilityController.ExtendedRangeMultiplier,
|
||||||
|
source.RangeMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SettingsVM CreateSettings()
|
||||||
|
{
|
||||||
|
var dispatcher = new InputDispatcher(
|
||||||
|
new NullKeyboardSource(),
|
||||||
|
new NullMouseSource(),
|
||||||
|
new KeyBindings());
|
||||||
|
return new SettingsVM(
|
||||||
|
new KeyBindings(),
|
||||||
|
dispatcher,
|
||||||
|
static _ => { },
|
||||||
|
DisplaySettings.Default,
|
||||||
|
static _ => { },
|
||||||
|
AudioSettings.Default,
|
||||||
|
static _ => { },
|
||||||
|
GameplaySettings.Default,
|
||||||
|
static _ => { },
|
||||||
|
ChatSettings.Default,
|
||||||
|
static _ => { },
|
||||||
|
CharacterSettings.Default,
|
||||||
|
static _ => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullKeyboardSource : IKeyboardSource
|
||||||
|
{
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
public event Action<Key, ModifierMask>? KeyDown;
|
||||||
|
public event Action<Key, ModifierMask>? KeyUp;
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
public bool IsHeld(Key key) => false;
|
||||||
|
public ModifierMask CurrentModifiers => ModifierMask.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullMouseSource : IMouseSource
|
||||||
|
{
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
public event Action<MouseButton, ModifierMask>? MouseDown;
|
||||||
|
public event Action<MouseButton, ModifierMask>? MouseUp;
|
||||||
|
public event Action<float, float>? MouseMove;
|
||||||
|
public event Action<float>? Scroll;
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
public bool IsHeld(MouseButton button) => false;
|
||||||
|
public bool WantCaptureMouse => false;
|
||||||
|
public bool WantCaptureKeyboard => false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content.Vfx;
|
using AcDream.Content.Vfx;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.World;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Options;
|
using DatReaderWriter.Options;
|
||||||
|
|
@ -25,10 +27,11 @@ public sealed class RecallTeleportAnimationTests
|
||||||
{
|
{
|
||||||
var calls = new List<string>();
|
var calls = new List<string>();
|
||||||
var frame = new RetailLiveFrameCoordinator(
|
var frame = new RetailLiveFrameCoordinator(
|
||||||
_ => calls.Add("objects"),
|
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||||
() => calls.Add("network"),
|
new GpuWorldState(),
|
||||||
() => calls.Add("commands"),
|
new TestLiveSessionFramePhase(() => calls.Add("network")),
|
||||||
() => calls.Add("spatial"));
|
new TestPostNetworkCommandFramePhase(() => calls.Add("commands")),
|
||||||
|
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")));
|
||||||
|
|
||||||
frame.Tick(1f / 60f);
|
frame.Tick(1f / 60f);
|
||||||
|
|
||||||
|
|
@ -39,25 +42,96 @@ public sealed class RecallTeleportAnimationTests
|
||||||
public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch()
|
public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch()
|
||||||
{
|
{
|
||||||
var deltas = new List<float>();
|
var deltas = new List<float>();
|
||||||
int networkDispatches = 0;
|
var calls = new List<string>();
|
||||||
var frame = new RetailLiveFrameCoordinator(
|
var frame = new RetailLiveFrameCoordinator(
|
||||||
deltas.Add,
|
new TestLiveObjectFramePhase(delta =>
|
||||||
() => networkDispatches++,
|
{
|
||||||
() => { },
|
deltas.Add(delta);
|
||||||
() => { });
|
calls.Add("objects");
|
||||||
|
}),
|
||||||
|
new GpuWorldState(),
|
||||||
|
new TestLiveSessionFramePhase(() => calls.Add("network")),
|
||||||
|
new TestPostNetworkCommandFramePhase(() => calls.Add("commands")),
|
||||||
|
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")));
|
||||||
|
|
||||||
frame.Tick(float.NaN);
|
frame.Tick(float.NaN);
|
||||||
frame.Tick(float.PositiveInfinity);
|
frame.Tick(float.PositiveInfinity);
|
||||||
frame.Tick(-1f);
|
frame.Tick(-1f);
|
||||||
|
|
||||||
Assert.Equal([0f, 0f, 0f], deltas);
|
Assert.Equal([0f, 0f, 0f], deltas);
|
||||||
Assert.Equal(3, networkDispatches);
|
Assert.Equal(
|
||||||
|
Enumerable.Repeat(new[] { "objects", "network", "commands", "spatial" }, 3)
|
||||||
|
.SelectMany(static frameCalls => frameCalls),
|
||||||
|
calls);
|
||||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NaN));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NaN));
|
||||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NegativeInfinity));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NegativeInfinity));
|
||||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.PositiveInfinity));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.PositiveInfinity));
|
||||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.MaxValue));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.MaxValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LiveFrame_CommitsTheInboundMutationBatchBeforeCommandsAndReconcile()
|
||||||
|
{
|
||||||
|
const uint landblock = 0x0101FFFFu;
|
||||||
|
var calls = new List<string>();
|
||||||
|
var world = new GpuWorldState();
|
||||||
|
long before = world.VisibilityCommitCount;
|
||||||
|
var frame = new RetailLiveFrameCoordinator(
|
||||||
|
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||||
|
world,
|
||||||
|
new TestLiveSessionFramePhase(() =>
|
||||||
|
{
|
||||||
|
calls.Add("network");
|
||||||
|
world.PlaceLiveEntityProjection(landblock, Entity(1));
|
||||||
|
Assert.Equal(before, world.VisibilityCommitCount);
|
||||||
|
}),
|
||||||
|
new TestPostNetworkCommandFramePhase(() =>
|
||||||
|
{
|
||||||
|
calls.Add("commands");
|
||||||
|
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||||
|
}),
|
||||||
|
new TestLiveSpatialReconcilePhase(() =>
|
||||||
|
{
|
||||||
|
calls.Add("spatial");
|
||||||
|
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||||
|
}));
|
||||||
|
|
||||||
|
frame.Tick(1f / 60f);
|
||||||
|
|
||||||
|
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
|
||||||
|
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||||
|
Assert.Equal(1, world.PendingLiveEntityCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LiveFrame_SessionFailureDisposesMutationBatchAndSuppressesLaterPhases()
|
||||||
|
{
|
||||||
|
const uint landblock = 0x0101FFFFu;
|
||||||
|
var calls = new List<string>();
|
||||||
|
var world = new GpuWorldState();
|
||||||
|
long before = world.VisibilityCommitCount;
|
||||||
|
var frame = new RetailLiveFrameCoordinator(
|
||||||
|
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||||
|
world,
|
||||||
|
new TestLiveSessionFramePhase(() =>
|
||||||
|
{
|
||||||
|
calls.Add("network");
|
||||||
|
world.PlaceLiveEntityProjection(landblock, Entity(2));
|
||||||
|
Assert.Equal(before, world.VisibilityCommitCount);
|
||||||
|
throw new InvalidOperationException("inbound failure");
|
||||||
|
}),
|
||||||
|
new TestPostNetworkCommandFramePhase(() => calls.Add("commands")),
|
||||||
|
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")));
|
||||||
|
|
||||||
|
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => frame.Tick(1f / 60f));
|
||||||
|
|
||||||
|
Assert.Equal("inbound failure", error.Message);
|
||||||
|
Assert.Equal(["objects", "network"], calls);
|
||||||
|
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||||
|
Assert.Equal(1, world.PendingLiveEntityCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void InstalledRecall_HiddenEnterWorldPublishesFinishedCyclicPoseWithoutAdvancingTime()
|
public void InstalledRecall_HiddenEnterWorldPublishesFinishedCyclicPoseWithoutAdvancingTime()
|
||||||
{
|
{
|
||||||
|
|
@ -108,4 +182,14 @@ public sealed class RecallTeleportAnimationTests
|
||||||
Quaternion.Normalize(recallTail[index].Orientation),
|
Quaternion.Normalize(recallTail[index].Orientation),
|
||||||
Quaternion.Normalize(finishedPose[index].Orientation))) < 0.999999f);
|
Quaternion.Normalize(finishedPose[index].Orientation))) < 0.999999f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WorldEntity Entity(uint id) => new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
ServerGuid = 0x70000000u + id,
|
||||||
|
SourceGfxObjOrSetupId = 0x02000001u,
|
||||||
|
Position = Vector3.Zero,
|
||||||
|
Rotation = Quaternion.Identity,
|
||||||
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,16 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
FieldInfo[] ownerFields = owner.GetFields(
|
FieldInfo[] ownerFields = owner.GetFields(
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow));
|
Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow));
|
||||||
|
if (owner == typeof(AcDream.App.Input.RetailLocalPlayerFrameController))
|
||||||
|
{
|
||||||
|
// Checkpoint B deliberately leaves this single legacy callback
|
||||||
|
// composition for D/F, which own input capture and the mutable
|
||||||
|
// player-mode/controller slot. No other phase owner may add one.
|
||||||
|
Assert.Contains(
|
||||||
|
ownerFields,
|
||||||
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
ownerFields,
|
ownerFields,
|
||||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
|
@ -274,6 +284,71 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1);
|
Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractedLiveObjectSource_PinsRetailAndRegisteredAdaptationOrder()
|
||||||
|
{
|
||||||
|
string source = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Update",
|
||||||
|
"LiveObjectFrameController.cs"));
|
||||||
|
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
source,
|
||||||
|
"_localPlayerFrame.AdvanceBeforeNetwork",
|
||||||
|
"_selectionInteractions?.DrainOutbound",
|
||||||
|
"_animations.Tick",
|
||||||
|
"_staticAnimations.Tick",
|
||||||
|
"_animationPresenter.Present",
|
||||||
|
"_equippedChildren.Tick",
|
||||||
|
"_staticAnimations.ProcessHooks",
|
||||||
|
"_effects.Tick");
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
source,
|
||||||
|
"_translucencyFades.AdvanceAll",
|
||||||
|
"_animationHooks.Drain",
|
||||||
|
"_entityEffects.RefreshLiveOwnerPoses",
|
||||||
|
"_particleSink.RefreshAttachedEmitters",
|
||||||
|
"_lights.Refresh",
|
||||||
|
"_particleVisibility.Apply",
|
||||||
|
"_particles.Tick",
|
||||||
|
"_scripts.Tick");
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
source,
|
||||||
|
"public void Reconcile()",
|
||||||
|
"_entityEffects.RefreshLiveOwnerPoses",
|
||||||
|
"_equippedChildren.Tick",
|
||||||
|
"_particleSink.RefreshAttachedEmitters",
|
||||||
|
"_lights.Refresh");
|
||||||
|
|
||||||
|
Assert.Equal(1, CountOccurrences(source, "_staticAnimations.Tick("));
|
||||||
|
Assert.Equal(1, CountOccurrences(source, "_staticAnimations.ProcessHooks("));
|
||||||
|
Assert.Equal(1, CountOccurrences(source, "_effects.Tick("));
|
||||||
|
Assert.Equal(1, CountOccurrences(source, "_particles.Tick("));
|
||||||
|
Assert.Equal(1, CountOccurrences(source, "_scripts.Tick("));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GameWindow_ComposesTheLiveFrameOwnersWithoutOwningTheirBodies()
|
||||||
|
{
|
||||||
|
string source = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Rendering",
|
||||||
|
"GameWindow.cs"));
|
||||||
|
|
||||||
|
Assert.Contains("new AcDream.App.Update.LiveObjectFrameController(", source);
|
||||||
|
Assert.Contains("new AcDream.App.Update.LiveSpatialPresentationReconciler(", source);
|
||||||
|
Assert.Contains("new AcDream.App.World.RetailLiveFrameCoordinator(", source);
|
||||||
|
Assert.DoesNotContain("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("ILiveAnimationPresentationContext", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static UpdateFrameOrchestrator Create(
|
private static UpdateFrameOrchestrator Create(
|
||||||
List<string> calls,
|
List<string> calls,
|
||||||
RecordingTeardown? teardown = null,
|
RecordingTeardown? teardown = null,
|
||||||
|
|
@ -432,4 +507,19 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AssertAppearsInOrder(string source, params string[] markers)
|
||||||
|
{
|
||||||
|
int previous = -1;
|
||||||
|
foreach (string marker in markers)
|
||||||
|
{
|
||||||
|
int current = source.IndexOf(marker, previous + 1, StringComparison.Ordinal);
|
||||||
|
Assert.True(current >= 0, $"Missing source marker: {marker}");
|
||||||
|
Assert.True(current > previous, $"Out-of-order source marker: {marker}");
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountOccurrences(string source, string marker) =>
|
||||||
|
source.Split(marker, StringSplitOptions.None).Length - 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue