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
|
|
@ -10,7 +10,7 @@ using Silk.NET.Windowing;
|
|||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||
public sealed class GameWindow : IDisposable
|
||||
{
|
||||
private static double ClientTimerNow() =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
|
|
@ -152,12 +152,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_remoteInboundMotion;
|
||||
private readonly AcDream.App.World.RetailInboundEventDispatcher
|
||||
_inboundEntityEvents = new();
|
||||
private readonly LiveEntityAnimationScheduler _liveAnimationScheduler;
|
||||
private readonly LiveEntityAnimationPresenter _animationPresenter;
|
||||
private LiveEntityAnimationScheduler _liveAnimationScheduler = null!;
|
||||
private LiveEntityAnimationPresenter _animationPresenter = null!;
|
||||
private readonly AcDream.App.Input.LocalPlayerProjectionController _localPlayerProjection;
|
||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||
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 AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||
// Step 7 projectile presentation. The controller owns no identity map;
|
||||
|
|
@ -247,6 +249,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
/// appear in this map.
|
||||
/// </summary>
|
||||
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.
|
||||
// WbDrawDispatcher.WalkEntitiesInto treats null and an empty set identically
|
||||
|
|
@ -520,11 +523,21 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
private int _weatherDebugStep = 0;
|
||||
|
||||
// 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.RetailChaseCamera? _retailChaseCamera;
|
||||
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
|
||||
// replaces this before any in-world drag can normally occur.
|
||||
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));
|
||||
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
||||
_uiRegistry = uiRegistry;
|
||||
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() => _liveEntities);
|
||||
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
|
||||
_liveEntityRuntimeSlot);
|
||||
SpellBook = new AcDream.Core.Spells.Spellbook();
|
||||
LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook);
|
||||
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
||||
|
|
@ -761,25 +775,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
movement, cellId, update),
|
||||
(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(
|
||||
resolveEntity: () =>
|
||||
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var entity)
|
||||
|
|
@ -824,16 +819,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
objectClockDisposition: () =>
|
||||
_liveEntities?.GetRootObjectClockDisposition(_playerServerGuid)
|
||||
?? 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()
|
||||
|
|
@ -2080,34 +2065,20 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// dat-lookup throws — see C.1.5a spec §6 (error handling) for rationale.
|
||||
AcDream.App.Rendering.Vfx.EntityEffectController? entityEffects = null;
|
||||
var staticLiveRootCommitter = new StaticLiveRootCommitter(
|
||||
() => _liveEntities,
|
||||
_liveEntityRuntimeSlot,
|
||||
_physicsEngine.ShadowObjects,
|
||||
() => (_liveCenterX, _liveCenterY),
|
||||
entity => _effectPoses.UpdateRoot(entity));
|
||||
_liveWorldOrigin,
|
||||
_effectPoses);
|
||||
var staticAnimationResidency = new LiveStaticAnimationResidency(
|
||||
_liveEntityRuntimeSlot);
|
||||
_staticAnimationScheduler = new RetailStaticAnimatingObjectScheduler(
|
||||
capturedAnimLoader!,
|
||||
CaptureAnimationHooks,
|
||||
(entity, poses, availability) =>
|
||||
_effectPoses.Publish(entity, poses, availability),
|
||||
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),
|
||||
_animationHookFrames!.Capture,
|
||||
_effectPoses.Publish,
|
||||
staticAnimationResidency.IsResident,
|
||||
(entity, body) =>
|
||||
_ = staticLiveRootCommitter.Commit(entity, body),
|
||||
entity => entity.ServerGuid == 0
|
||||
? 0UL
|
||||
: _liveEntities?.TryGetRecord(
|
||||
entity.ServerGuid,
|
||||
out LiveEntityRecord resident) == true
|
||||
&& ReferenceEquals(resident.WorldEntity, entity)
|
||||
? resident.ProjectionMutationVersion
|
||||
: ulong.MaxValue);
|
||||
staticAnimationResidency.ProjectionVersion);
|
||||
AcDream.App.Rendering.Vfx.ScriptActivationInfo? ResolveActivation(AcDream.Core.World.WorldEntity e)
|
||||
{
|
||||
try
|
||||
|
|
@ -2183,6 +2154,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
entityScriptActivator.OnCreate,
|
||||
entityScriptActivator.OnRemove)),
|
||||
liveEntityComponentLifecycle);
|
||||
_liveEntityRuntimeSlot.Bind(_liveEntities);
|
||||
liveEntityMotionRuntime =
|
||||
new AcDream.App.Physics.LiveEntityMotionRuntimeController(
|
||||
_liveEntities,
|
||||
|
|
@ -2204,17 +2176,9 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_projectileController = new AcDream.App.Physics.ProjectileController(
|
||||
_liveEntities,
|
||||
_physicsEngine,
|
||||
setupId =>
|
||||
{
|
||||
// State can classify an already-live object as Missile
|
||||
// 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))
|
||||
new AcDream.App.Physics.DatProjectileSetupResolver(_dats!, _datLock),
|
||||
new EntityRootPosePublisher(_effectPoses),
|
||||
_liveWorldOrigin)
|
||||
{
|
||||
DiagnosticSink = message => Console.Error.WriteLine($"projectile: {message}"),
|
||||
};
|
||||
|
|
@ -2233,6 +2197,29 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_lightingSink!,
|
||||
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();
|
||||
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
|
||||
_dats!,
|
||||
|
|
@ -2627,7 +2614,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_remoteTeleportController!,
|
||||
_animatedEntities,
|
||||
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(
|
||||
() => _liveEntities),
|
||||
_liveEntityRuntimeSlot),
|
||||
_remoteMovementObservations,
|
||||
_remotePhysicsUpdater,
|
||||
_remoteInboundMotion,
|
||||
|
|
@ -2667,6 +2654,44 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
||||
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
||||
_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 =
|
||||
_liveSessionController.Start(
|
||||
_options,
|
||||
|
|
@ -3520,7 +3545,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// camera path re-extends the boom. Updating directly from the stale
|
||||
// source-world viewer made the destination bend across the reveal.
|
||||
_retailChaseCamera?.ResetViewerToPlayer(snappedPos, _playerController.Yaw);
|
||||
ReconcileLiveObjectSpatialPresentation();
|
||||
_liveSpatialReconciler.Reconcile();
|
||||
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"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.
|
||||
// Its non-advancing initial projection must publish child and
|
||||
// effect anchors before the first draw too.
|
||||
ReconcileLiveObjectSpatialPresentation();
|
||||
_liveSpatialReconciler.Reconcile();
|
||||
}
|
||||
|
||||
// Update chase camera(s). The CameraController exposes whichever
|
||||
|
|
@ -5715,102 +5740,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
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>
|
||||
/// Phase 6.4: advance every animated entity's frame counter by
|
||||
/// <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)
|
||||
&& 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
|
||||
// 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
|
||||
|
|
|
|||
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>
|
||||
internal sealed class LiveEntityAnimationPresenter
|
||||
{
|
||||
private readonly Func<LiveEntityRuntime?> _liveEntities;
|
||||
private readonly Func<ILiveStaticPartFrameSource?> _staticFrames;
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly ILiveStaticPartFrameSource _staticFrames;
|
||||
private readonly EntityEffectPoseRegistry _effectPoses;
|
||||
private readonly ILiveAnimationPresentationContext _context;
|
||||
private readonly AnimationPresentationDiagnostics _diagnostics;
|
||||
|
|
@ -22,8 +22,8 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
private bool _isPresenting;
|
||||
|
||||
public LiveEntityAnimationPresenter(
|
||||
Func<LiveEntityRuntime?> liveEntities,
|
||||
Func<ILiveStaticPartFrameSource?> staticFrames,
|
||||
LiveEntityRuntime liveEntities,
|
||||
ILiveStaticPartFrameSource staticFrames,
|
||||
EntityEffectPoseRegistry effectPoses,
|
||||
ILiveAnimationPresentationContext context,
|
||||
AnimationPresentationDiagnostics diagnostics,
|
||||
|
|
@ -43,9 +43,8 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(animation);
|
||||
LiveEntityRuntime? runtime = _liveEntities();
|
||||
if (runtime is null
|
||||
|| !runtime.IsCurrentSpatialAnimation(record, animation)
|
||||
LiveEntityRuntime runtime = _liveEntities;
|
||||
if (!runtime.IsCurrentSpatialAnimation(record, animation)
|
||||
|| !ReferenceEquals(record.WorldEntity, animation.Entity)
|
||||
|| animation.Sequencer is not { MotionDoneTarget: null } sequencer)
|
||||
{
|
||||
|
|
@ -55,9 +54,8 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
WorldEntity capturedEntity = animation.Entity;
|
||||
sequencer.MotionDoneTarget = (motion, success) =>
|
||||
{
|
||||
LiveEntityRuntime? current = _liveEntities();
|
||||
if (current is null
|
||||
|| !current.IsCurrentSpatialAnimation(record, animation)
|
||||
LiveEntityRuntime current = _liveEntities;
|
||||
if (!current.IsCurrentSpatialAnimation(record, animation)
|
||||
|| !ReferenceEquals(record.WorldEntity, capturedEntity)
|
||||
|| !ReferenceEquals(animation.Entity, capturedEntity))
|
||||
{
|
||||
|
|
@ -79,8 +77,8 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schedules);
|
||||
LiveEntityRuntime? runtime = _liveEntities();
|
||||
if (runtime is null || _isPresenting)
|
||||
LiveEntityRuntime runtime = _liveEntities;
|
||||
if (_isPresenting)
|
||||
return;
|
||||
|
||||
// EffectPoseChanged is synchronous. Presentation is one consuming
|
||||
|
|
@ -112,7 +110,7 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
ulong projectionVersion = record.ProjectionMutationVersion;
|
||||
ulong presentationRevision = animation.PresentationRevision;
|
||||
|
||||
if (_staticFrames()?.TryTakeLivePartFrames(
|
||||
if (_staticFrames.TryTakeLivePartFrames(
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -17,41 +19,38 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
internal sealed class LiveEntityAnimationScheduler
|
||||
{
|
||||
private readonly Func<LiveEntityRuntime?> _liveEntities;
|
||||
private readonly Func<uint> _localPlayerGuid;
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly ILocalPlayerIdentitySource _localPlayer;
|
||||
private readonly RemotePhysicsUpdater _remotePhysics;
|
||||
private readonly LiveEntityOrdinaryPhysicsUpdater _ordinaryPhysics;
|
||||
private readonly Func<ProjectileController?> _projectiles;
|
||||
private readonly Action<WorldEntity> _publishRootPose;
|
||||
private readonly Action<uint, AnimationSequencer> _captureAnimationHooks;
|
||||
private readonly ProjectileController _projectiles;
|
||||
private readonly IEntityRootPosePublisher _rootPoses;
|
||||
private readonly IAnimationHookCaptureSink _animationHooks;
|
||||
private readonly List<LiveEntityRecord> _rootSnapshot = new();
|
||||
private readonly Dictionary<uint, LiveEntityAnimationSchedule> _schedules = new();
|
||||
private readonly Frame _rootFrameScratch = new();
|
||||
private readonly MotionDeltaFrame _rootDeltaScratch = new();
|
||||
|
||||
public LiveEntityAnimationScheduler(
|
||||
Func<LiveEntityRuntime?> liveEntities,
|
||||
Func<uint> localPlayerGuid,
|
||||
LiveEntityRuntime liveEntities,
|
||||
ILocalPlayerIdentitySource localPlayer,
|
||||
RemotePhysicsUpdater remotePhysics,
|
||||
LiveEntityOrdinaryPhysicsUpdater ordinaryPhysics,
|
||||
Func<ProjectileController?> projectiles,
|
||||
Action<WorldEntity> publishRootPose,
|
||||
Action<uint, AnimationSequencer> captureAnimationHooks)
|
||||
ProjectileController projectiles,
|
||||
IEntityRootPosePublisher rootPoses,
|
||||
IAnimationHookCaptureSink animationHooks)
|
||||
{
|
||||
_liveEntities = liveEntities
|
||||
?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_localPlayerGuid = localPlayerGuid
|
||||
?? throw new ArgumentNullException(nameof(localPlayerGuid));
|
||||
_localPlayer = localPlayer
|
||||
?? throw new ArgumentNullException(nameof(localPlayer));
|
||||
_remotePhysics = remotePhysics
|
||||
?? throw new ArgumentNullException(nameof(remotePhysics));
|
||||
_ordinaryPhysics = ordinaryPhysics
|
||||
?? throw new ArgumentNullException(nameof(ordinaryPhysics));
|
||||
_projectiles = projectiles
|
||||
?? throw new ArgumentNullException(nameof(projectiles));
|
||||
_publishRootPose = publishRootPose
|
||||
?? throw new ArgumentNullException(nameof(publishRootPose));
|
||||
_captureAnimationHooks = captureAnimationHooks
|
||||
?? throw new ArgumentNullException(nameof(captureAnimationHooks));
|
||||
_projectiles = projectiles ?? throw new ArgumentNullException(nameof(projectiles));
|
||||
_rootPoses = rootPoses ?? throw new ArgumentNullException(nameof(rootPoses));
|
||||
_animationHooks = animationHooks ?? throw new ArgumentNullException(nameof(animationHooks));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -69,9 +68,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
Action<LiveEntityRecord, LiveEntityAnimationState>? prepareAnimation = null)
|
||||
{
|
||||
_schedules.Clear();
|
||||
LiveEntityRuntime? runtime = _liveEntities();
|
||||
if (runtime is null)
|
||||
return _schedules;
|
||||
LiveEntityRuntime runtime = _liveEntities;
|
||||
|
||||
runtime.CopySpatialRootObjectRecordsTo(_rootSnapshot);
|
||||
foreach (LiveEntityRecord record in _rootSnapshot)
|
||||
|
|
@ -180,7 +177,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
// PlayerMovementController owns this exact record clock and advances
|
||||
// its PartArray before local collision. Consume that prepared pose;
|
||||
// never tick the same clock or sequence a second time here.
|
||||
if (serverGuid == _localPlayerGuid())
|
||||
if (serverGuid == _localPlayer.ServerGuid)
|
||||
{
|
||||
IReadOnlyList<PartTransform>? prepared =
|
||||
animation?.PreparedSequenceFrames;
|
||||
|
|
@ -208,7 +205,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
// that are smaller than an admitted object quantum. Publish that
|
||||
// current root independently of PartArray recomposition so local
|
||||
// attached effects and lights never trail the rendered character.
|
||||
_publishRootPose(entity);
|
||||
_rootPoses.UpdateRoot(entity);
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
|
|
@ -249,7 +246,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
IReadOnlyList<PartTransform>? frames = null;
|
||||
float legacyElapsed = 0f;
|
||||
bool completed = true;
|
||||
ProjectileController? projectileController = _projectiles();
|
||||
ProjectileController projectileController = _projectiles;
|
||||
bool projectileHandlesMovement = projectile is not null
|
||||
&& projectileController?.HandlesMovement(serverGuid) == true;
|
||||
float objectScale = animation?.Scale
|
||||
|
|
@ -275,7 +272,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
entity,
|
||||
quantum,
|
||||
sequencer?.Manager,
|
||||
_captureAnimationHooks,
|
||||
_animationHooks.Capture,
|
||||
sequencer,
|
||||
runtime,
|
||||
record,
|
||||
|
|
@ -289,7 +286,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
{
|
||||
if (sequencer is not null)
|
||||
{
|
||||
_captureAnimationHooks(entity.Id, sequencer);
|
||||
_animationHooks.Capture(entity.Id, sequencer);
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
|
|
@ -355,7 +352,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
rootDelta,
|
||||
liveCenterX,
|
||||
liveCenterY,
|
||||
_captureAnimationHooks,
|
||||
_animationHooks.Capture,
|
||||
runtime,
|
||||
record,
|
||||
objectClockEpoch))
|
||||
|
|
@ -381,7 +378,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
liveCenterY,
|
||||
objectClockEpoch,
|
||||
sequencer,
|
||||
_captureAnimationHooks))
|
||||
_animationHooks.Capture))
|
||||
{
|
||||
completed = false;
|
||||
break;
|
||||
|
|
@ -405,7 +402,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
out projectileStep) == true;
|
||||
|
||||
if (!ordinaryBodyHandlesMovement && sequencer is not null)
|
||||
_captureAnimationHooks(entity.Id, sequencer);
|
||||
_animationHooks.Capture(entity.Id, sequencer);
|
||||
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
|
||||
{
|
||||
completed = false;
|
||||
|
|
@ -443,7 +440,7 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
return default;
|
||||
}
|
||||
|
||||
_publishRootPose(entity);
|
||||
_rootPoses.UpdateRoot(entity);
|
||||
if (!IsCurrent(runtime, record, entity, animation, remote, projectile, objectClockEpoch))
|
||||
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.Rendering.Vfx;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
|
|
@ -12,30 +13,28 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
internal sealed class StaticLiveRootCommitter
|
||||
{
|
||||
private readonly Func<LiveEntityRuntime?> _runtime;
|
||||
private readonly ILiveEntityRuntimeSource _runtime;
|
||||
private readonly ShadowObjectRegistry _shadows;
|
||||
private readonly Func<(int X, int Y)> _liveCenter;
|
||||
private readonly Action<WorldEntity> _updateEffectRoot;
|
||||
private readonly LiveWorldOriginState _origin;
|
||||
private readonly EntityEffectPoseRegistry _effectPoses;
|
||||
|
||||
public StaticLiveRootCommitter(
|
||||
Func<LiveEntityRuntime?> runtime,
|
||||
ILiveEntityRuntimeSource runtime,
|
||||
ShadowObjectRegistry shadows,
|
||||
Func<(int X, int Y)> liveCenter,
|
||||
Action<WorldEntity> updateEffectRoot)
|
||||
LiveWorldOriginState origin,
|
||||
EntityEffectPoseRegistry effectPoses)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
||||
_liveCenter = liveCenter
|
||||
?? throw new ArgumentNullException(nameof(liveCenter));
|
||||
_updateEffectRoot = updateEffectRoot
|
||||
?? throw new ArgumentNullException(nameof(updateEffectRoot));
|
||||
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||
_effectPoses = effectPoses ?? throw new ArgumentNullException(nameof(effectPoses));
|
||||
}
|
||||
|
||||
public bool Commit(WorldEntity entity, PhysicsBody body)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
LiveEntityRuntime? runtime = _runtime();
|
||||
LiveEntityRuntime? runtime = _runtime.Current;
|
||||
if (runtime is null
|
||||
|| !runtime.TryGetRecord(entity.ServerGuid, out LiveEntityRecord record)
|
||||
|| !ReferenceEquals(record.WorldEntity, entity)
|
||||
|
|
@ -46,15 +45,14 @@ internal sealed class StaticLiveRootCommitter
|
|||
|
||||
// The retained pose owner follows the CPhysicsObj root even while its
|
||||
// mesh is hidden. Hidden is presentation state, not logical teardown.
|
||||
_updateEffectRoot(entity);
|
||||
_effectPoses.UpdateRoot(entity);
|
||||
|
||||
// EffectPoseChanged observers run synchronously and may delete this
|
||||
// incarnation (or replace the GUID) before returning. Do not let the
|
||||
// stale root restore collision shadows for an owner which no longer
|
||||
// exists. This is the same callback-boundary lifetime rule used by the
|
||||
// live schedulers and movement controllers.
|
||||
if (!ReferenceEquals(_runtime(), runtime)
|
||||
|| !runtime.TryGetRecord(entity.ServerGuid, out LiveEntityRecord current)
|
||||
if (!runtime.TryGetRecord(entity.ServerGuid, out LiveEntityRecord current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|| !ReferenceEquals(current.WorldEntity, entity)
|
||||
|| !ReferenceEquals(current.PhysicsBody, body))
|
||||
|
|
@ -76,15 +74,14 @@ internal sealed class StaticLiveRootCommitter
|
|||
uint cellId = body.CellPosition.ObjCellId != 0
|
||||
? body.CellPosition.ObjCellId
|
||||
: record.FullCellId;
|
||||
(int centerX, int centerY) = _liveCenter();
|
||||
ShadowPositionSynchronizer.Sync(
|
||||
_shadows,
|
||||
entity.Id,
|
||||
body.Position,
|
||||
body.Orientation,
|
||||
cellId,
|
||||
centerX,
|
||||
centerY);
|
||||
_origin.CenterX,
|
||||
_origin.CenterY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue