refactor(update): cut over the frame orchestrator

Reduce GameWindow.OnUpdate to one typed orchestration call and remove transitive window callbacks from teardown, liveness, teleport placement, live presentation, auto-entry, and entity packet routing. Preserve the frozen retail object/network order while making the production owner graph explicit and testable.
This commit is contained in:
Erik 2026-07-22 03:31:38 +02:00
parent 947c61d2d7
commit e91f310279
18 changed files with 864 additions and 348 deletions

View file

@ -27,7 +27,7 @@ never hidden behind a retry, delay, suppression flag, or reordered callback.
- [x] F — extract the shared player-mode lifecycle plus fly/chase/player - [x] F — extract the shared player-mode lifecycle plus fly/chase/player
camera presentation and cut production over to one camera presentation and cut production over to one
`UpdateFrameOrchestrator`. `UpdateFrameOrchestrator`.
- [ ] G — delete the old `OnUpdate` bodies, callback facades, and obsolete - [x] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
frame state; run focused corrected-diff reviews after each ownership edge. frame state; run focused corrected-diff reviews after each ownership edge.
- [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized - [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized
resource soak, documentation, durable memory, and line/field/method closeout. resource soak, documentation, durable memory, and line/field/method closeout.
@ -469,6 +469,19 @@ passed / 5 skipped. All three corrected-diff reviews are clean.
`GameWindow.cs` is 7,160 lines, down 8,563 lines (54.5%) from the 15,723-line `GameWindow.cs` is 7,160 lines, down 8,563 lines (54.5%) from the 15,723-line
slice baseline. slice baseline.
**Checkpoint G completed 2026-07-22.** `GameWindow.OnUpdate` now owns only the
update profiler scope and one `UpdateFrameOrchestrator.Tick` call. Teardown,
absolute liveness time, local timestamp publication, expiry deletion,
player-mode auto-entry, live-origin initialization, Hidden PartArray
boundaries, remote teleport placement, and all twelve live-entity session
packet routes resolve through typed runtime owners; the former transitive
window callbacks and obsolete frame fields are gone. The App suite is green
at 2,823 passed / 3 skipped. Focused orchestration, lifecycle, presentation,
teleport, origin, and session tests are green, and all three corrected-diff
reviews are clean. `GameWindow.cs` is 7,026 raw lines / 241 fields / 108
methods, down 1,785 lines and 45 methods from the Slice-6 baseline and 8,697
lines (55.3%) from the 15,723-line pre-extraction class.
### H — release and connected gates ### H — release and connected gates
After all three independent corrected-diff reviews are clean: After all three independent corrected-diff reviews are clean:

View file

@ -1,11 +1,14 @@
using System; using System;
using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.World;
namespace AcDream.App.Input; namespace AcDream.App.Input;
/// <summary> /// <summary>
/// Phase K.2 — one-shot guard that auto-enters player mode after a /// Phase K.2 — one-shot guard that auto-enters player mode after a
/// successful login once every prerequisite is satisfied. Owned by /// successful login once every prerequisite is satisfied. The update-frame
/// <c>GameWindow</c> and ticked each frame from <c>OnUpdate</c>. /// orchestrator ticks it through a typed production context.
/// ///
/// <para> /// <para>
/// Why is this its own class? The auto-entry has four independent /// Why is this its own class? The auto-entry has four independent
@ -33,20 +36,108 @@ namespace AcDream.App.Input;
/// </para> /// </para>
/// ///
/// <para> /// <para>
/// All preconditions are passed in as predicates so the class doesn't /// Production preconditions come from focused runtime owners rather than the
/// pull in <c>WorldSession</c>, <c>PlayerMovementController</c>, or /// window host. The public delegate constructor remains a narrow test seam for
/// any GameWindow-internal types — the unit test wires them to plain /// plain boolean fixtures.
/// boolean fields.
/// </para> /// </para>
/// </summary> /// </summary>
internal interface IPlayerModeAutoEntryContext
{
bool IsLiveInWorld { get; }
bool IsPlayerEntityPresent { get; }
bool IsPlayerControllerReady { get; }
bool IsWorldReady { get; }
bool IsPlayerModeActive { get; }
void EnterPlayerMode();
}
/// <summary>Production auto-entry context over canonical runtime owners.</summary>
internal sealed class LivePlayerModeAutoEntryContext
: IPlayerModeAutoEntryContext
{
private readonly ILiveInWorldSource _session;
private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _identity;
private readonly WorldRevealCoordinator _worldReveal;
private readonly ILocalPlayerModeSource _mode;
private readonly PlayerModeController _playerMode;
public LivePlayerModeAutoEntryContext(
ILiveInWorldSource session,
LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource identity,
WorldRevealCoordinator worldReveal,
ILocalPlayerModeSource mode,
PlayerModeController playerMode)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
}
public bool IsLiveInWorld => _session.IsInWorld;
public bool IsPlayerEntityPresent =>
_liveEntities.MaterializedWorldEntities.ContainsKey(_identity.ServerGuid);
public bool IsPlayerControllerReady => true;
public bool IsWorldReady =>
_liveEntities.TryGetSnapshot(
_identity.ServerGuid,
out AcDream.Core.Net.WorldSession.EntitySpawn player)
&& player.Position is { LandblockId: not 0u } position
&& _worldReveal.Evaluate(position.LandblockId).IsReady;
public bool IsPlayerModeActive => _mode.IsPlayerMode;
public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
}
public sealed class PlayerModeAutoEntry public sealed class PlayerModeAutoEntry
{ {
private readonly Func<bool> _isLiveInWorld; private sealed class DelegateContext : IPlayerModeAutoEntryContext
private readonly Func<bool> _isPlayerEntityPresent; {
private readonly Func<bool> _isPlayerControllerReady; private readonly Func<bool> _isLiveInWorld;
private readonly Func<bool> _isWorldReady; private readonly Func<bool> _isPlayerEntityPresent;
private readonly Func<bool> _isPlayerModeActive; private readonly Func<bool> _isPlayerControllerReady;
private readonly Action _enterPlayerMode; private readonly Func<bool> _isWorldReady;
private readonly Action _enterPlayerMode;
private readonly Func<bool> _isPlayerModeActive;
public DelegateContext(
Func<bool> isLiveInWorld,
Func<bool> isPlayerEntityPresent,
Func<bool> isPlayerControllerReady,
Func<bool> isWorldReady,
Action enterPlayerMode,
Func<bool>? isPlayerModeActive)
{
_isLiveInWorld = isLiveInWorld
?? throw new ArgumentNullException(nameof(isLiveInWorld));
_isPlayerEntityPresent = isPlayerEntityPresent
?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
_isPlayerControllerReady = isPlayerControllerReady
?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
_isWorldReady = isWorldReady
?? throw new ArgumentNullException(nameof(isWorldReady));
_enterPlayerMode = enterPlayerMode
?? throw new ArgumentNullException(nameof(enterPlayerMode));
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
}
public bool IsLiveInWorld => _isLiveInWorld();
public bool IsPlayerEntityPresent => _isPlayerEntityPresent();
public bool IsPlayerControllerReady => _isPlayerControllerReady();
public bool IsWorldReady => _isWorldReady();
public bool IsPlayerModeActive => _isPlayerModeActive();
public void EnterPlayerMode() => _enterPlayerMode();
}
private readonly IPlayerModeAutoEntryContext _context;
private bool _armed; private bool _armed;
@ -72,20 +163,24 @@ public sealed class PlayerModeAutoEntry
/// player transition). Must construct the controller + chase /// player transition). Must construct the controller + chase
/// camera and switch the active camera; the auto-entry doesn't /// camera and switch the active camera; the auto-entry doesn't
/// reach inside.</param> /// reach inside.</param>
internal PlayerModeAutoEntry(IPlayerModeAutoEntryContext context) =>
_context = context ?? throw new ArgumentNullException(nameof(context));
public PlayerModeAutoEntry( public PlayerModeAutoEntry(
Func<bool> isLiveInWorld, Func<bool> isLiveInWorld,
Func<bool> isPlayerEntityPresent, Func<bool> isPlayerEntityPresent,
Func<bool> isPlayerControllerReady, Func<bool> isPlayerControllerReady,
Func<bool> isWorldReady, Func<bool> isWorldReady,
Action enterPlayerMode, Action enterPlayerMode,
Func<bool>? isPlayerModeActive = null) Func<bool>? isPlayerModeActive = null)
: this(new DelegateContext(
isLiveInWorld,
isPlayerEntityPresent,
isPlayerControllerReady,
isWorldReady,
enterPlayerMode,
isPlayerModeActive))
{ {
_isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld));
_isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
_isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
_isWorldReady = isWorldReady ?? throw new ArgumentNullException(nameof(isWorldReady));
_enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode));
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
} }
/// <summary>True iff <see cref="TryEnter"/> would still fire if the /// <summary>True iff <see cref="TryEnter"/> would still fire if the
@ -115,18 +210,18 @@ public sealed class PlayerModeAutoEntry
public bool TryEnter() public bool TryEnter()
{ {
if (!_armed) return false; if (!_armed) return false;
if (_isPlayerModeActive()) if (_context.IsPlayerModeActive)
{ {
_armed = false; _armed = false;
return false; return false;
} }
if (!_isLiveInWorld()) return false; if (!_context.IsLiveInWorld) return false;
if (!_isPlayerEntityPresent()) return false; if (!_context.IsPlayerEntityPresent) return false;
if (!_isPlayerControllerReady()) return false; if (!_context.IsPlayerControllerReady) return false;
if (!_isWorldReady()) return false; if (!_context.IsWorldReady) return false;
_armed = false; _armed = false;
_enterPlayerMode(); _context.EnterPlayerMode();
return true; return true;
} }
} }

View file

@ -0,0 +1,97 @@
using AcDream.App.Physics;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.App.Net;
/// <summary>
/// Routes one live session's entity packets through the non-reentrant retail
/// inbound envelope without retaining the window host.
/// </summary>
internal sealed class LiveEntitySessionController
{
private readonly RetailInboundEventDispatcher _inbound;
private readonly LiveEntityHydrationController _hydration;
private readonly LiveEntityNetworkUpdateController _updates;
private readonly ILocalPlayerTeleportNetworkSink _teleport;
private readonly EntityEffectController _effects;
public LiveEntitySessionController(
RetailInboundEventDispatcher inbound,
LiveEntityHydrationController hydration,
LiveEntityNetworkUpdateController updates,
ILocalPlayerTeleportNetworkSink teleport,
EntityEffectController effects)
{
_inbound = inbound ?? throw new ArgumentNullException(nameof(inbound));
_hydration = hydration ?? throw new ArgumentNullException(nameof(hydration));
_updates = updates ?? throw new ArgumentNullException(nameof(updates));
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
}
public LiveEntitySessionSink CreateSink() => new(
OnSpawned,
OnDeleted,
OnPickedUp,
OnMotionUpdated,
OnPositionUpdated,
OnVectorUpdated,
OnStateUpdated,
OnParentUpdated,
OnTeleportStarted,
OnAppearanceUpdated,
OnPlayPhysicsScript,
OnPlayPhysicsScriptType);
private void OnSpawned(WorldSession.EntitySpawn value) =>
_inbound.Run(_hydration, value,
static (owner, message) => owner.OnCreate(message));
private void OnDeleted(DeleteObject.Parsed value) =>
_inbound.Run(_hydration, value,
static (owner, message) => owner.OnDelete(message));
private void OnPickedUp(PickupEvent.Parsed value) =>
_inbound.Run(_hydration, value,
static (owner, message) => owner.OnPickup(message));
private void OnMotionUpdated(WorldSession.EntityMotionUpdate value) =>
_inbound.Run(_updates, value,
static (owner, message) => owner.OnMotion(message));
private void OnPositionUpdated(WorldSession.EntityPositionUpdate value) =>
_inbound.Run(_updates, value,
static (owner, message) => owner.OnPosition(message));
private void OnVectorUpdated(VectorUpdate.Parsed value) =>
_inbound.Run(_updates, value,
static (owner, message) => owner.OnVector(message));
private void OnStateUpdated(SetState.Parsed value) =>
_inbound.Run(_updates, value,
static (owner, message) => owner.OnState(message));
private void OnParentUpdated(ParentEvent.Parsed value) =>
_inbound.Run(_hydration, value,
static (owner, message) => owner.OnParent(message));
private void OnTeleportStarted(uint value) =>
_inbound.Run(_teleport, value,
static (owner, message) => owner.OnTeleportStarted(message));
private void OnAppearanceUpdated(ObjDescEvent.Parsed value) =>
_inbound.Run(_hydration, value,
static (owner, message) => owner.OnAppearance(message));
private void OnPlayPhysicsScript(PlayPhysicsScript value) =>
_inbound.Run(_effects, value,
static (owner, message) => owner.HandleDirect(message));
private void OnPlayPhysicsScriptType(PlayPhysicsScriptType value) =>
_inbound.Run(_effects, value,
static (owner, message) => owner.HandleTyped(message));
}

View file

@ -1,6 +1,7 @@
using AcDream.App.Combat; using AcDream.App.Combat;
using AcDream.App.Input; using AcDream.App.Input;
using AcDream.App.Interaction; using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.App.Physics; using AcDream.App.Physics;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx; using AcDream.App.Rendering.Vfx;
@ -50,18 +51,18 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly LiveWorldOriginState _origin; private readonly LiveWorldOriginState _origin;
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
_localPlayerTeleport; _localPlayerTeleport;
private readonly Func<PlayerMovementController?> _playerControllerSource; private readonly ILocalPlayerControllerSource _playerControllerSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound; private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly Func<EntityPhysicsHost?> _playerHostSource; private readonly ILocalPlayerPhysicsHostSource _playerHostSource;
private readonly Func<uint> _playerGuid; private readonly ILocalPlayerIdentitySource _playerIdentity;
private readonly IPhysicsScriptTimeSource _gameTime; private readonly IPhysicsScriptTimeSource _gameTime;
private readonly Func<WorldSession?> _session; private readonly ILiveWorldSessionSource _session;
private readonly LiveEntityInboundAuthorityGate _authorityGate; private readonly LiveEntityInboundAuthorityGate _authorityGate;
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics; private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
private PlayerMovementController? _playerController => _playerControllerSource(); private PlayerMovementController? _playerController => _playerControllerSource.Controller;
private EntityPhysicsHost? _playerHost => _playerHostSource(); private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
private uint _playerServerGuid => _playerGuid(); private uint _playerServerGuid => _playerIdentity.ServerGuid;
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime; private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid => private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
_liveEntities.MaterializedWorldEntities; _liveEntities.MaterializedWorldEntities;
@ -96,12 +97,12 @@ internal sealed class LiveEntityNetworkUpdateController
CombatTargetController? combatTargetController, CombatTargetController? combatTargetController,
LiveWorldOriginState origin, LiveWorldOriginState origin,
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport, AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
Func<PlayerMovementController?> playerControllerSource, ILocalPlayerControllerSource playerControllerSource,
LocalPlayerOutboundController localPlayerOutbound, LocalPlayerOutboundController localPlayerOutbound,
Func<EntityPhysicsHost?> playerHostSource, ILocalPlayerPhysicsHostSource playerHostSource,
Func<uint> playerGuid, ILocalPlayerIdentitySource playerIdentity,
IPhysicsScriptTimeSource gameTime, IPhysicsScriptTimeSource gameTime,
Func<WorldSession?> session, ILiveWorldSessionSource session,
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps, Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
IMovementTruthDiagnosticSink movementTruthDiagnostics) IMovementTruthDiagnosticSink movementTruthDiagnostics)
{ {
@ -131,7 +132,7 @@ internal sealed class LiveEntityNetworkUpdateController
_localPlayerOutbound = localPlayerOutbound _localPlayerOutbound = localPlayerOutbound
?? throw new ArgumentNullException(nameof(localPlayerOutbound)); ?? throw new ArgumentNullException(nameof(localPlayerOutbound));
_playerHostSource = playerHostSource ?? throw new ArgumentNullException(nameof(playerHostSource)); _playerHostSource = playerHostSource ?? throw new ArgumentNullException(nameof(playerHostSource));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid)); _playerIdentity = playerIdentity ?? throw new ArgumentNullException(nameof(playerIdentity));
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime)); _gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
_session = session ?? throw new ArgumentNullException(nameof(session)); _session = session ?? throw new ArgumentNullException(nameof(session));
_authorityGate = new LiveEntityInboundAuthorityGate( _authorityGate = new LiveEntityInboundAuthorityGate(
@ -1029,7 +1030,7 @@ internal sealed class LiveEntityNetworkUpdateController
p.PositionY, p.PositionY,
p.PositionZ)), p.PositionZ)),
() => _localPlayerOutbound.SendImmediatePosition( () => _localPlayerOutbound.SendImmediatePosition(
_session(), _session.CurrentSession,
_playerController))) _playerController)))
return; return;

View file

@ -0,0 +1,49 @@
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Physics;
/// <summary>Synchronizes a resolved remote placement in the current live origin.</summary>
internal sealed class RemoteShadowPlacementSynchronizer
{
private readonly RemotePhysicsUpdater _remotePhysics;
private readonly LiveWorldOriginState _origin;
public RemoteShadowPlacementSynchronizer(
RemotePhysicsUpdater remotePhysics,
LiveWorldOriginState origin)
{
_remotePhysics = remotePhysics
?? throw new ArgumentNullException(nameof(remotePhysics));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
}
public void Sync(WorldEntity entity, PhysicsBody body, uint cellId) =>
_remotePhysics.SyncRemoteShadowToBody(
entity.Id,
body,
_origin.CenterX,
_origin.CenterY,
cellId);
}
/// <summary>Bridges remote placement ownership to live presentation state.</summary>
internal sealed class RemoteTeleportPlacementPresentation
{
private readonly LiveEntityPresentationController _presentation;
public RemoteTeleportPlacementPresentation(
LiveEntityPresentationController presentation) =>
_presentation = presentation
?? throw new ArgumentNullException(nameof(presentation));
public void Complete(uint serverGuid, ushort generation, bool deferShadowRestore) =>
_presentation.CompleteAuthoritativePlacement(
serverGuid,
generation,
deferShadowRestore);
public void Begin(uint serverGuid, ushort generation) =>
_presentation.BeginAuthoritativePlacement(serverGuid, generation);
}

View file

@ -134,7 +134,6 @@ public sealed class GameWindow : IDisposable
private AcDream.App.Streaming.StreamingController? _streamingController; private AcDream.App.Streaming.StreamingController? _streamingController;
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator? private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
_streamingOriginRecenter; _streamingOriginRecenter;
private AcDream.App.Update.IStreamingFramePhase _streamingFrame = null!;
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal; private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
_localPlayerTeleportSink = new(); _localPlayerTeleportSink = new();
@ -173,10 +172,7 @@ public sealed class GameWindow : IDisposable
private readonly AcDream.App.Input.MovementTruthDiagnosticController private readonly AcDream.App.Input.MovementTruthDiagnosticController
_movementTruthDiagnostics; _movementTruthDiagnostics;
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound; private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
private AcDream.App.Update.ICameraFramePhase _cameraFrame = null!; private AcDream.App.Update.UpdateFrameOrchestrator _updateFrameOrchestrator = null!;
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;
@ -186,6 +182,7 @@ public sealed class GameWindow : IDisposable
_liveEntityProjectionWithdrawal; _liveEntityProjectionWithdrawal;
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration; private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates; private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!;
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
_liveEntityMotionBindings = new(); _liveEntityMotionBindings = new();
@ -2170,36 +2167,32 @@ public sealed class GameWindow : IDisposable
_entityEffects = entityEffects; _entityEffects = entityEffects;
entityEffects.DiagnosticSink = message => entityEffects.DiagnosticSink = message =>
Console.Error.WriteLine($"vfx: {message}"); Console.Error.WriteLine($"vfx: {message}");
var partArrayLifecycle =
new AcDream.App.Rendering.LiveEntityPartArrayLifecycle(
_animatedEntities);
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController( _liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
_liveEntities, _liveEntities,
_physicsEngine.ShadowObjects, _physicsEngine.ShadowObjects,
entityEffects.PlayTypedFromHiddenTransition, entityEffects.PlayTypedFromHiddenTransition,
_equippedChildRenderer.SetDirectChildrenNoDraw, _equippedChildRenderer.SetDirectChildrenNoDraw,
_liveEntityMotionBindings.ClearTargetForHiddenEntity, _liveEntityMotionBindings.ClearTargetForHiddenEntity,
() => (_liveCenterX, _liveCenterY), _liveWorldOrigin.GetCenter,
handlePartArrayEnterWorld: localEntityId => handlePartArrayEnterWorld: partArrayLifecycle.HandleEnterWorld);
{ var remoteShadowPlacement =
if (_animatedEntities.TryGetValue(localEntityId, out var animated)) new AcDream.App.Physics.RemoteShadowPlacementSynchronizer(
animated.Sequencer?.Manager.HandleEnterWorld(); _remotePhysicsUpdater,
}); _liveWorldOrigin);
var remoteTeleportPresentation =
new AcDream.App.Physics.RemoteTeleportPlacementPresentation(
_liveEntityPresentation);
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController( _remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
_physicsEngine, _physicsEngine,
_liveEntities, _liveEntities,
_liveEntityMotionBindings.GetSetupCylinder, _liveEntityMotionBindings.GetSetupCylinder,
CellLocalForSeed, _liveWorldOrigin.CellLocalForSeed,
(entity, remote, cellId) => _remotePhysicsUpdater.SyncRemoteShadowToBody( remoteShadowPlacement.Sync,
entity.Id, remoteTeleportPresentation.Complete,
remote, remoteTeleportPresentation.Begin);
_liveCenterX,
_liveCenterY,
cellId),
(guid, generation, deferShadowRestore) =>
_liveEntityPresentation.CompleteAuthoritativePlacement(
guid,
generation,
deferShadowRestore),
(guid, generation) =>
_liveEntityPresentation.BeginAuthoritativePlacement(guid, generation));
_equippedChildRenderer.ProjectionPoseReady += guid => _equippedChildRenderer.ProjectionPoseReady += guid =>
_liveEntityLights.OnAttachedPoseReady(guid); _liveEntityLights.OnAttachedPoseReady(guid);
_hookRouter.Register(entityEffects); _hookRouter.Register(entityEffects);
@ -2484,9 +2477,14 @@ public sealed class GameWindow : IDisposable
_streamingController, _streamingController,
_worldState, _worldState,
_worldReveal, _worldReveal,
() => _playerServerGuid, _localPlayerIdentity,
sealedDungeonCells, sealedDungeonCells,
Console.WriteLine); Console.WriteLine);
_liveSessionController = new AcDream.App.Net.LiveSessionController();
var localPhysicsTimestamps =
new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
_localPlayerIdentity,
_liveSessionController);
var liveEntityTeardown = var liveEntityTeardown =
new AcDream.App.World.LiveEntityRuntimeTeardownController( new AcDream.App.World.LiveEntityRuntimeTeardownController(
_liveEntities, _liveEntities,
@ -2503,8 +2501,15 @@ public sealed class GameWindow : IDisposable
_physicsEngine.ShadowObjects, _physicsEngine.ShadowObjects,
_liveEntityLights!, _liveEntityLights!,
_classificationCache, _classificationCache,
() => _playerServerGuid); _localPlayerIdentity);
liveEntityComponentLifecycle.Bind(liveEntityTeardown); liveEntityComponentLifecycle.Bind(liveEntityTeardown);
var liveEntityDeletion =
new AcDream.App.World.LiveEntityDeletionController(
_liveEntities,
Objects,
liveEntityTeardown,
_localPlayerIdentity,
_options.DumpLiveSpawns ? Console.WriteLine : null);
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController( _liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
_liveEntities, _liveEntities,
Objects, Objects,
@ -2518,9 +2523,9 @@ public sealed class GameWindow : IDisposable
_liveEntityPresentation!), _liveEntityPresentation!),
originCoordinator, originCoordinator,
networkUpdateBridge, networkUpdateBridge,
liveEntityTeardown, localPhysicsTimestamps,
PublishLocalPhysicsTimestamps, _localPlayerIdentity,
() => _playerServerGuid, liveEntityDeletion,
_options.DumpLiveSpawns ? Console.WriteLine : null); _options.DumpLiveSpawns ? Console.WriteLine : null);
_liveEntityNetworkUpdates = _liveEntityNetworkUpdates =
new AcDream.App.Physics.LiveEntityNetworkUpdateController( new AcDream.App.Physics.LiveEntityNetworkUpdateController(
@ -2546,18 +2551,24 @@ public sealed class GameWindow : IDisposable
_combatTargetController, _combatTargetController,
_liveWorldOrigin, _liveWorldOrigin,
_localPlayerTeleportSink, _localPlayerTeleportSink,
() => _playerController, _playerControllerSlot,
_localPlayerOutbound, _localPlayerOutbound,
() => _playerHost, _playerHostSlot,
() => _playerServerGuid, _localPlayerIdentity,
_updateFrameClock, _updateFrameClock,
() => LiveSession, _liveSessionController,
PublishLocalPhysicsTimestamps, localPhysicsTimestamps.Publish,
_movementTruthDiagnostics); _movementTruthDiagnostics);
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController( _liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
_liveEntities, _liveEntities,
() => _playerServerGuid, _localPlayerIdentity,
candidate => _liveEntityHydration.OnPrune(candidate)); liveEntityDeletion);
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
_inboundEntityEvents,
_liveEntityHydration,
_liveEntityNetworkUpdates,
_localPlayerTeleportSink,
_entityEffects!);
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection); parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
networkUpdateBridge.Bind(_liveEntityNetworkUpdates); networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
_equippedChildRenderer.EntityReady += candidate => _equippedChildRenderer.EntityReady += candidate =>
@ -2573,7 +2584,6 @@ public sealed class GameWindow : IDisposable
// CreateObject messages into _worldGameState as they arrive. Entirely // CreateObject messages into _worldGameState as they arrive. Entirely
// 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();
_combatAttackOperations.Bind( _combatAttackOperations.Bind(
new AcDream.App.Combat.LiveCombatAttackOperations( new AcDream.App.Combat.LiveCombatAttackOperations(
Combat, Combat,
@ -2609,7 +2619,7 @@ public sealed class GameWindow : IDisposable
mouseLookController, mouseLookController,
new AcDream.App.Input.CombatAttackInputFrameAdapter( new AcDream.App.Input.CombatAttackInputFrameAdapter(
_combatAttackController!)); _combatAttackController!));
_streamingFrame = new AcDream.App.Streaming.StreamingFrameController( var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
_options.LiveMode, _options.LiveMode,
_localPlayerMode, _localPlayerMode,
_playerControllerSlot, _playerControllerSlot,
@ -2638,7 +2648,7 @@ public sealed class GameWindow : IDisposable
new AcDream.App.Update.SettingsParticleRangeSource( new AcDream.App.Update.SettingsParticleRangeSource(
_settingsVm, _settingsVm,
_persistedDisplay.ParticleRange)); _persistedDisplay.ParticleRange));
_liveSpatialReconciler = var liveSpatialReconciler =
new AcDream.App.Update.LiveSpatialPresentationReconciler( new AcDream.App.Update.LiveSpatialPresentationReconciler(
_entityEffects!, _entityEffects!,
_equippedChildRenderer!, _equippedChildRenderer!,
@ -2721,16 +2731,15 @@ public sealed class GameWindow : IDisposable
_localPlayerSkills, _localPlayerSkills,
_viewportAspect); _viewportAspect);
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry( _playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
isLiveInWorld: () => _liveSessionController.IsInWorld, new AcDream.App.Input.LivePlayerModeAutoEntryContext(
isPlayerEntityPresent: () => _liveSessionController,
_liveEntities.MaterializedWorldEntities.ContainsKey( _liveEntities,
_playerServerGuid), _localPlayerIdentity,
isPlayerControllerReady: () => true, _worldReveal
// Retail SmartBox::UseTime @ 0x00455410 completes player ?? throw new InvalidOperationException(
// position only after the destination cell load clears. "World reveal was not composed before player auto-entry."),
isWorldReady: LoginWorldReady, _localPlayerMode,
enterPlayerMode: _playerModeController.EnterFromAutoEntry, _playerModeController));
isPlayerModeActive: () => _localPlayerMode.IsPlayerMode);
_playerModeController.BindAutoEntry(_playerModeAutoEntry); _playerModeController.BindAutoEntry(_playerModeAutoEntry);
var localTeleportPresentation = var localTeleportPresentation =
new AcDream.App.Streaming.LocalPlayerTeleportPresentation( new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
@ -2758,7 +2767,7 @@ public sealed class GameWindow : IDisposable
_playerHostSlot, _playerHostSlot,
_chaseCameraInput, _chaseCameraInput,
_liveWorldOrigin, _liveWorldOrigin,
_liveSpatialReconciler), liveSpatialReconciler),
new AcDream.App.Streaming.LocalPlayerTeleportSession( new AcDream.App.Streaming.LocalPlayerTeleportSession(
_liveSessionController), _liveSessionController),
localTeleportPresentation); localTeleportPresentation);
@ -2767,25 +2776,41 @@ public sealed class GameWindow : IDisposable
// remains only as the staged-startup fallback used by shutdown when // remains only as the staged-startup fallback used by shutdown when
// composition fails before the transfer. // composition fails before the transfer.
_portalTunnel = null; _portalTunnel = null;
_liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator( var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
liveObjectFrame, liveObjectFrame,
_worldState, _worldState,
_liveSessionController, _liveSessionController,
localPlayerFrame, localPlayerFrame,
_liveSpatialReconciler); liveSpatialReconciler);
_cameraFrame = new AcDream.App.Rendering.CameraFrameController( var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
_cameraController!, _cameraController!,
_inputCapture, _inputCapture,
_cameraInput, _cameraInput,
localPlayerFrameRuntime, localPlayerFrameRuntime,
_chaseCameraInput, _chaseCameraInput,
localPlayerFrame, localPlayerFrame,
_liveSpatialReconciler, liveSpatialReconciler,
new AcDream.App.Combat.CombatCameraTargetSource( new AcDream.App.Combat.CombatCameraTargetSource(
_gameplaySettings, _gameplaySettings,
Combat, Combat,
_selection, _selection,
_worldSelectionQuery!)); _worldSelectionQuery!));
_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
new AcDream.App.Update.LiveEntityTeardownFramePhase(
_liveEntities),
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
_updateFrameClock,
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
streamingFrame,
_gameplayInputFrame,
liveFrameCoordinator,
new AcDream.App.Update.LiveEntityLivenessFramePhase(
_liveEntityLiveness,
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
_localPlayerTeleport,
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
_playerModeAutoEntry),
cameraFrame);
AcDream.App.Net.LiveSessionStartResult liveStart = AcDream.App.Net.LiveSessionStartResult liveStart =
_liveSessionController.Start( _liveSessionController.Start(
_options, _options,
@ -2958,7 +2983,7 @@ public sealed class GameWindow : IDisposable
{ {
events = new AcDream.App.Net.LiveSessionEventRouter( events = new AcDream.App.Net.LiveSessionEventRouter(
session, session,
CreateLiveEntitySessionSink(), _liveEntitySessionEvents.CreateSink(),
new AcDream.App.Net.LiveEnvironmentSessionSink( new AcDream.App.Net.LiveEnvironmentSessionSink(
OnEnvironChanged, OnEnvironChanged,
ticks => ticks =>
@ -2987,44 +3012,6 @@ public sealed class GameWindow : IDisposable
} }
} }
private AcDream.App.Net.LiveEntitySessionSink CreateLiveEntitySessionSink() => new(
Spawned: spawn => _inboundEntityEvents.Run(
_liveEntityHydration!,
spawn,
static (hydration, value) => hydration.OnCreate(value)),
Deleted: deletion => _inboundEntityEvents.Run(
_liveEntityHydration!, deletion,
static (hydration, value) => hydration.OnDelete(value)),
PickedUp: pickup => _inboundEntityEvents.Run(
_liveEntityHydration!, pickup,
static (hydration, value) => hydration.OnPickup(value)),
MotionUpdated: motion => _inboundEntityEvents.Run(
_liveEntityNetworkUpdates!, motion,
static (updates, value) => updates.OnMotion(value)),
PositionUpdated: position => _inboundEntityEvents.Run(
_liveEntityNetworkUpdates!, position,
static (updates, value) => updates.OnPosition(value)),
VectorUpdated: vector => _inboundEntityEvents.Run(
_liveEntityNetworkUpdates!, vector,
static (updates, value) => updates.OnVector(value)),
StateUpdated: state => _inboundEntityEvents.Run(
_liveEntityNetworkUpdates!, state,
static (updates, value) => updates.OnState(value)),
ParentUpdated: parent => _inboundEntityEvents.Run(
_liveEntityHydration!, parent,
static (hydration, value) => hydration.OnParent(value)),
TeleportStarted: teleport => _inboundEntityEvents.Run(
_localPlayerTeleportSink,
teleport,
static (sink, value) => sink.OnTeleportStarted(value)),
AppearanceUpdated: appearance => _inboundEntityEvents.Run(
_liveEntityHydration!, appearance,
static (hydration, value) => hydration.OnAppearance(value)),
PlayPhysicsScript: script => _inboundEntityEvents.Run(
this, script, static (window, value) => window.OnPlayScriptReceived(value)),
PlayPhysicsScriptType: message => _inboundEntityEvents.Run(
this, message, static (window, value) => window._entityEffects?.HandleTyped(value)));
private AcDream.App.Net.LiveInventorySessionBindings private AcDream.App.Net.LiveInventorySessionBindings
CreateLiveInventorySessionBindings() => new( CreateLiveInventorySessionBindings() => new(
Objects, Objects,
@ -3276,47 +3263,6 @@ public sealed class GameWindow : IDisposable
doll.MeshRefs = reposed; doll.MeshRefs = reposed;
} }
private void PublishLocalPhysicsTimestamps(
uint guid,
AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
{
AcDream.Core.Net.WorldSession? session = LiveSession;
if (guid != _playerServerGuid || session is null) return;
session.PublishAcceptedLocalPhysicsTimestamps(
timestamps.Instance,
timestamps.ServerControlledMove,
timestamps.Teleport,
timestamps.ForcePosition);
}
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
// body's cell-relative CellPosition. This is the ONE place the streaming center
// (_liveCenter) is allowed to touch the physics frame — at the placement seam,
// converting the render-frame world position into the wire's (cell, local). After
// seeding, physics carries (cell, local) forward without ever reading _liveCenter.
private System.Numerics.Vector3 CellLocalForSeed(System.Numerics.Vector3 worldPos, uint cellId)
{
int lbX = (int)((cellId >> 24) & 0xFFu);
int lbY = (int)((cellId >> 16) & 0xFFu);
var origin = new System.Numerics.Vector3(
(lbX - _liveCenterX) * 192f,
(lbY - _liveCenterY) * 192f,
0f);
return worldPos - origin;
}
private bool LoginWorldReady()
{
return TryGetLoginWorldCell(out uint cell)
&& EvaluateWorldRevealReadiness(cell).IsReady;
}
private AcDream.App.Streaming.WorldRevealReadinessSnapshot EvaluateWorldRevealReadiness(
uint destinationCell)
{
return _worldReveal?.Evaluate(destinationCell) ?? default;
}
private bool TryGetLoginWorldCell(out uint cell) private bool TryGetLoginWorldCell(out uint cell)
{ {
cell = 0; cell = 0;
@ -3329,20 +3275,6 @@ public sealed class GameWindow : IDisposable
return cell != 0; return cell != 0;
} }
/// <summary>
/// Server-sent direct PhysicsScript (F754). EntityEffectController owns
/// GUID translation and pre-materialization queueing.
///
/// <para>
/// F754 remains a direct PhysicsScript DID. It is never resolved through a
/// typed PhysicsScriptTable.
/// </para>
/// </summary>
private void OnPlayScriptReceived(AcDream.Core.Net.Messages.PlayPhysicsScript message)
{
_entityEffects?.HandleDirect(message);
}
private void UpdateSkyPes( private void UpdateSkyPes(
float dayFraction, float dayFraction,
AcDream.Core.World.DayGroupData? dayGroup, AcDream.Core.World.DayGroupData? dayGroup,
@ -3512,76 +3444,10 @@ public sealed class GameWindow : IDisposable
} }
private void OnUpdate(double dt) private void OnUpdate(double dt)
{ {
using var _updStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Update); using var _updStage = _frameProfiler.BeginStage(
AcDream.App.Diagnostics.FrameStage.Update);
// A resource callback can request deletion while the presentation _updateFrameOrchestrator.Tick(
// owner is inside a synchronous resume/suspend transition. The first
// unregister leaves a canonical teardown tombstone; drain it only now,
// after that callback stack has unwound on the same update thread.
try
{
_liveEntityHydration?.RetryPendingTeardowns();
}
catch (AggregateException error)
{
// The tombstone retains every unfinished owner step. Report this
// attempt, but keep the frame advancing so the next update can
// retry after transient renderer/plugin teardown failures.
Console.Error.WriteLine($"[live-entity-teardown] {error}");
}
AcDream.App.Update.UpdateFrameTiming frameTiming = _updateFrameClock.Advance(
new AcDream.App.Update.UpdateFrameInput(dt)); new AcDream.App.Update.UpdateFrameInput(dt));
float frameDelta = frameTiming.SimulationDeltaSecondsSingle;
// Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
// Timer::cur_time at the packet/default-script call site. Publish this
// update frame's clock before streaming and network dispatch, then
// reuse the same timestamp for animation hooks and the later drain.
_scriptRunner?.PublishTime(frameTiming.ScriptTime);
// Streaming publishes before inbound CreateObject dispatch so newly
// accepted live projections can enter a resident bucket this frame.
_streamingFrame.Tick();
// Input callbacks feed the current object tick. Packets already read
// by Core.Net remain queued until the retail object/physics phase has
// consumed that input and published its final pose.
_gameplayInputFrame!.Tick(frameTiming);
// Drain pending live-session traffic AFTER streaming so any incoming
// CreateObject events find their landblock already loaded in
// GpuWorldState. Non-blocking — returns immediately if no datagrams
// are in the kernel buffer. Fires EntitySpawned events synchronously.
// Step 2: routed through the controller; functionally identical.
// Retail SmartBox::UseTime (0x00455410) advances CObjectMaint and
// CPhysics before it drains the inbound event queue. Keep the complete
// live-object phase on that side of the barrier too. In particular,
// the recall action retires before ACE's Hidden SetState freezes its
// PartArray at the teleport boundary.
_liveFrameCoordinator.Tick(frameDelta);
_liveEntityLiveness?.Tick(ClientTimerNow());
// Retail teleport transit runs after streaming and inbound state so a
// newly resident destination can materialize in this same frame. The
// focused owner also converges an F751 that arrived before the local
// player projection/controller became available.
_localPlayerTeleport!.Tick(frameDelta);
// Phase K.1a — tick the input dispatcher so Hold-type bindings
// re-fire while their chord is held. K.1b adds the subscribers
// that actually consume the events.
// Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse
// on selection, then revert). No-op when nothing is flashing.
// Retained panel-local timers advance through RetailUiRuntime.Tick on the draw seam.
// Phase K.2 — auto-enter player mode at login. The guard
// returns true on the firing tick (one-shot); subsequent ticks
// are no-ops. Skipped offline (no active session → IsLiveInWorld
// predicate stays false). Cancelled by manual fly-toggle in
// OnInputAction (Ctrl+Tab) or DebugPanel.
_playerModeAutoEntry?.TryEnter();
_cameraFrame.Tick(frameTiming);
} }
private void OnCameraModeChanged(bool _modeBool) private void OnCameraModeChanged(bool _modeBool)

View file

@ -0,0 +1,19 @@
using AcDream.App.World;
namespace AcDream.App.Rendering;
/// <summary>Routes retail PartArray world-entry boundaries to live animation owners.</summary>
internal sealed class LiveEntityPartArrayLifecycle
{
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
public LiveEntityPartArrayLifecycle(
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations) =>
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
public void HandleEnterWorld(uint localEntityId)
{
if (_animations.TryGetValue(localEntityId, out LiveEntityAnimationState? animation))
animation.Sequencer?.Manager.HandleEnterWorld();
}
}

View file

@ -0,0 +1,78 @@
using AcDream.App.Input;
using AcDream.App.World;
using AcDream.Core.Vfx;
namespace AcDream.App.Update;
/// <summary>Bridges canonical live-entity teardown into the host frame.</summary>
internal sealed class LiveEntityTeardownFramePhase : IUpdateFrameTeardownPhase
{
private readonly LiveEntityRuntime _runtime;
public LiveEntityTeardownFramePhase(LiveEntityRuntime runtime) =>
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
public void RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
}
internal sealed class ConsoleUpdateFrameFailureSink : IUpdateFrameFailureSink
{
public void ReportTeardownFailure(AggregateException error) =>
Console.Error.WriteLine($"[live-entity-teardown] {error}");
}
/// <summary>Publishes the frame clock to the retail PhysicsScript owner.</summary>
internal sealed class PhysicsScriptClockPublisher : IUpdateFrameScriptClockPublisher
{
private readonly PhysicsScriptRunner _runner;
public PhysicsScriptClockPublisher(PhysicsScriptRunner runner) =>
_runner = runner ?? throw new ArgumentNullException(nameof(runner));
public void PublishTime(double scriptTime) => _runner.PublishTime(scriptTime);
}
internal interface IClientMonotonicTimeSource
{
double Now { get; }
}
internal sealed class StopwatchClientMonotonicTimeSource
: IClientMonotonicTimeSource
{
public double Now =>
System.Diagnostics.Stopwatch.GetTimestamp()
/ (double)System.Diagnostics.Stopwatch.Frequency;
}
/// <summary>
/// Supplies the absolute client timer to liveness independently of the
/// normalized simulation delta.
/// </summary>
internal sealed class LiveEntityLivenessFramePhase
: ILiveEntityLivenessFramePhase
{
private readonly LiveEntityLivenessController _liveness;
private readonly IClientMonotonicTimeSource _clock;
public LiveEntityLivenessFramePhase(
LiveEntityLivenessController liveness,
IClientMonotonicTimeSource clock)
{
_liveness = liveness ?? throw new ArgumentNullException(nameof(liveness));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
}
public void Tick() => _liveness.Tick(_clock.Now);
}
internal sealed class PlayerModeAutoEntryFramePhase
: IPlayerModeAutoEntryFramePhase
{
private readonly PlayerModeAutoEntry _autoEntry;
public PlayerModeAutoEntryFramePhase(PlayerModeAutoEntry autoEntry) =>
_autoEntry = autoEntry ?? throw new ArgumentNullException(nameof(autoEntry));
public void TryEnter() => _autoEntry.TryEnter();
}

View file

@ -0,0 +1,69 @@
using AcDream.App.Input;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.App.World;
internal interface ILiveEntityPruneSink
{
bool Prune(LiveEntityPruneCandidate candidate);
}
/// <summary>
/// Owns authoritative and expiry-driven live-object deletion through one
/// generation-safe runtime transaction.
/// </summary>
internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink
{
private readonly LiveEntityRuntime _runtime;
private readonly ClientObjectTable _objects;
private readonly ILiveEntityTeardownCoordinator _teardown;
private readonly ILocalPlayerIdentitySource _identity;
private readonly Action<string>? _diagnostic;
public LiveEntityDeletionController(
LiveEntityRuntime runtime,
ClientObjectTable objects,
ILiveEntityTeardownCoordinator teardown,
ILocalPlayerIdentitySource identity,
Action<string>? diagnostic = null)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
_diagnostic = diagnostic;
}
/// <summary>
/// Retail <c>SmartBox::HandleDeleteObject @ 0x00451EA0</c> rejects the
/// player, requires the exact Instance timestamp, then performs one
/// symmetric logical teardown.
/// </summary>
public bool Delete(DeleteObject.Parsed delete)
{
if (delete.Guid == _identity.ServerGuid)
return false;
if (!_runtime.TryGetRecord(delete.Guid, out _))
_teardown.ForgetUnknownOwner(delete.Guid);
bool removed = _runtime.UnregisterLiveEntity(
delete,
isLocalPlayer: false,
beforeTeardown: () =>
ObjectTableWiring.ApplyEntityDelete(_objects, delete));
if (removed)
{
_diagnostic?.Invoke(
$"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
}
return removed;
}
public bool Prune(LiveEntityPruneCandidate candidate) =>
Delete(new DeleteObject.Parsed(
candidate.ServerGuid,
candidate.Generation));
}

View file

@ -1,4 +1,5 @@
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Input;
using AcDream.Core.Items; using AcDream.Core.Items;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
@ -146,9 +147,9 @@ internal sealed class LiveEntityHydrationController
private readonly ILiveEntityReadyPublisher _ready; private readonly ILiveEntityReadyPublisher _ready;
private readonly ILiveEntityWorldOriginCoordinator _origin; private readonly ILiveEntityWorldOriginCoordinator _origin;
private readonly ILiveEntityNetworkUpdateSink _networkUpdates; private readonly ILiveEntityNetworkUpdateSink _networkUpdates;
private readonly ILiveEntityTeardownCoordinator _teardown; private readonly IAcceptedLocalPhysicsTimestampPublisher _timestamps;
private readonly Action<uint, AcceptedPhysicsTimestamps> _publishTimestamps; private readonly ILocalPlayerIdentitySource _identity;
private readonly Func<uint> _playerGuid; private readonly LiveEntityDeletionController _deletion;
private readonly Action<string>? _diagnostic; private readonly Action<string>? _diagnostic;
public LiveEntityHydrationController( public LiveEntityHydrationController(
@ -160,9 +161,9 @@ internal sealed class LiveEntityHydrationController
ILiveEntityReadyPublisher ready, ILiveEntityReadyPublisher ready,
ILiveEntityWorldOriginCoordinator origin, ILiveEntityWorldOriginCoordinator origin,
ILiveEntityNetworkUpdateSink networkUpdates, ILiveEntityNetworkUpdateSink networkUpdates,
ILiveEntityTeardownCoordinator teardown, IAcceptedLocalPhysicsTimestampPublisher timestamps,
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps, ILocalPlayerIdentitySource identity,
Func<uint> playerGuid, LiveEntityDeletionController deletion,
Action<string>? diagnostic = null) Action<string>? diagnostic = null)
{ {
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
@ -173,10 +174,9 @@ internal sealed class LiveEntityHydrationController
_ready = ready ?? throw new ArgumentNullException(nameof(ready)); _ready = ready ?? throw new ArgumentNullException(nameof(ready));
_origin = origin ?? throw new ArgumentNullException(nameof(origin)); _origin = origin ?? throw new ArgumentNullException(nameof(origin));
_networkUpdates = networkUpdates ?? throw new ArgumentNullException(nameof(networkUpdates)); _networkUpdates = networkUpdates ?? throw new ArgumentNullException(nameof(networkUpdates));
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown)); _timestamps = timestamps ?? throw new ArgumentNullException(nameof(timestamps));
_publishTimestamps = publishTimestamps _identity = identity ?? throw new ArgumentNullException(nameof(identity));
?? throw new ArgumentNullException(nameof(publishTimestamps)); _deletion = deletion ?? throw new ArgumentNullException(nameof(deletion));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
_diagnostic = diagnostic; _diagnostic = diagnostic;
} }
@ -207,7 +207,7 @@ internal sealed class LiveEntityHydrationController
try try
{ {
_publishTimestamps(spawn.Guid, result.Timestamps); _timestamps.Publish(spawn.Guid, result.Timestamps);
if (_runtime.IsCurrentCreateIntegration( if (_runtime.IsCurrentCreateIntegration(
record, record,
createIntegrationVersion) createIntegrationVersion)
@ -353,23 +353,7 @@ internal sealed class LiveEntityHydrationController
// SmartBox::HandleDeleteObject rejects the player before resolving an // SmartBox::HandleDeleteObject rejects the player before resolving an
// object or touching any queued owner state. A pre-Create player // object or touching any queued owner state. A pre-Create player
// Delete must therefore be side-effect-free too. // Delete must therefore be side-effect-free too.
if (delete.Guid == _playerGuid()) return _deletion.Delete(delete);
return false;
if (!_runtime.TryGetRecord(delete.Guid, out _))
_teardown.ForgetUnknownOwner(delete.Guid);
bool removed = _runtime.UnregisterLiveEntity(
delete,
isLocalPlayer: false,
beforeTeardown: () =>
ObjectTableWiring.ApplyEntityDelete(_objects, delete));
if (removed)
{
_diagnostic?.Invoke(
$"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
}
return removed;
} }
/// <summary> /// <summary>
@ -377,9 +361,7 @@ internal sealed class LiveEntityHydrationController
/// generation gate and teardown transaction as an authoritative F747. /// generation gate and teardown transaction as an authoritative F747.
/// </summary> /// </summary>
public bool OnPrune(LiveEntityPruneCandidate candidate) => public bool OnPrune(LiveEntityPruneCandidate candidate) =>
OnDelete(new DeleteObject.Parsed( _deletion.Prune(candidate);
candidate.ServerGuid,
candidate.Generation));
public int RetryPendingTeardowns() => _runtime.RetryPendingTeardowns(); public int RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
@ -415,7 +397,7 @@ internal sealed class LiveEntityHydrationController
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu; uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
LiveEntityRecord[] records = _runtime.Records LiveEntityRecord[] records = _runtime.Records
.Where(record => (record.ServerGuid != _playerGuid() .Where(record => (record.ServerGuid != _identity.ServerGuid
|| !record.InitialHydrationCompleted || !record.InitialHydrationCompleted
|| record.CreateProjectionSynchronizationPending || record.CreateProjectionSynchronizationPending
|| record.AppearanceProjectionSynchronizationPending) || record.AppearanceProjectionSynchronizationPending)

View file

@ -1,3 +1,4 @@
using AcDream.App.Input;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx; using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming; using AcDream.App.Streaming;
@ -113,7 +114,17 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
private readonly StreamingController _streaming; private readonly StreamingController _streaming;
private readonly GpuWorldState _worldState; private readonly GpuWorldState _worldState;
private readonly WorldRevealCoordinator _worldReveal; private readonly WorldRevealCoordinator _worldReveal;
private readonly Func<uint> _playerGuid; private sealed class DelegateIdentitySource : ILocalPlayerIdentitySource
{
private readonly Func<uint> _read;
public DelegateIdentitySource(Func<uint> read) =>
_read = read ?? throw new ArgumentNullException(nameof(read));
public uint ServerGuid => _read();
}
private readonly ILocalPlayerIdentitySource _identity;
private readonly ISealedDungeonCellClassifier _sealedDungeonCells; private readonly ISealedDungeonCellClassifier _sealedDungeonCells;
private readonly Action<string>? _diagnostic; private readonly Action<string>? _diagnostic;
@ -122,7 +133,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
StreamingController streaming, StreamingController streaming,
GpuWorldState worldState, GpuWorldState worldState,
WorldRevealCoordinator worldReveal, WorldRevealCoordinator worldReveal,
Func<uint> playerGuid, ILocalPlayerIdentitySource identity,
ISealedDungeonCellClassifier sealedDungeonCells, ISealedDungeonCellClassifier sealedDungeonCells,
Action<string>? diagnostic = null) Action<string>? diagnostic = null)
{ {
@ -130,18 +141,37 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming)); _streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState)); _worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal)); _worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid)); _identity = identity ?? throw new ArgumentNullException(nameof(identity));
_sealedDungeonCells = sealedDungeonCells _sealedDungeonCells = sealedDungeonCells
?? throw new ArgumentNullException(nameof(sealedDungeonCells)); ?? throw new ArgumentNullException(nameof(sealedDungeonCells));
_diagnostic = diagnostic; _diagnostic = diagnostic;
} }
internal LiveEntityWorldOriginCoordinator(
LiveWorldOriginState origin,
StreamingController streaming,
GpuWorldState worldState,
WorldRevealCoordinator worldReveal,
Func<uint> playerGuid,
ISealedDungeonCellClassifier sealedDungeonCells,
Action<string>? diagnostic = null)
: this(
origin,
streaming,
worldState,
worldReveal,
new DelegateIdentitySource(playerGuid),
sealedDungeonCells,
diagnostic)
{
}
public bool IsKnown => _origin.IsKnown; public bool IsKnown => _origin.IsKnown;
public LiveEntityOriginInitialization TryInitialize( public LiveEntityOriginInitialization TryInitialize(
WorldSession.EntitySpawn spawn) WorldSession.EntitySpawn spawn)
{ {
if (spawn.Guid != _playerGuid() if (spawn.Guid != _identity.ServerGuid
|| spawn.Position is not { } position) || spawn.Position is not { } position)
{ {
return new(_origin.IsKnown, Array.Empty<uint>()); return new(_origin.IsKnown, Array.Empty<uint>());

View file

@ -1,4 +1,5 @@
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.App.Input;
namespace AcDream.App.World; namespace AcDream.App.World;
@ -86,19 +87,19 @@ internal sealed class LiveEntityLivenessController
private const double MaintenanceIntervalSeconds = 1.0; private const double MaintenanceIntervalSeconds = 1.0;
private readonly LiveEntityRuntime _runtime; private readonly LiveEntityRuntime _runtime;
private readonly Func<uint> _playerGuid; private readonly ILocalPlayerIdentitySource _identity;
private readonly Action<LiveEntityPruneCandidate> _prune; private readonly ILiveEntityPruneSink _prune;
private readonly LiveEntityLivenessTracker _tracker = new(); private readonly LiveEntityLivenessTracker _tracker = new();
private readonly List<LiveEntityLivenessSample> _samples = new(); private readonly List<LiveEntityLivenessSample> _samples = new();
private double _nextMaintenanceAt; private double _nextMaintenanceAt;
public LiveEntityLivenessController( public LiveEntityLivenessController(
LiveEntityRuntime runtime, LiveEntityRuntime runtime,
Func<uint> playerGuid, ILocalPlayerIdentitySource identity,
Action<LiveEntityPruneCandidate> prune) ILiveEntityPruneSink prune)
{ {
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid)); _identity = identity ?? throw new ArgumentNullException(nameof(identity));
_prune = prune ?? throw new ArgumentNullException(nameof(prune)); _prune = prune ?? throw new ArgumentNullException(nameof(prune));
} }
@ -108,7 +109,7 @@ internal sealed class LiveEntityLivenessController
return; return;
_nextMaintenanceAt = now + MaintenanceIntervalSeconds; _nextMaintenanceAt = now + MaintenanceIntervalSeconds;
uint playerGuid = _playerGuid(); uint playerGuid = _identity.ServerGuid;
if (playerGuid == 0 if (playerGuid == 0
|| !_runtime.TryGetRecord(playerGuid, out LiveEntityRecord player) || !_runtime.TryGetRecord(playerGuid, out LiveEntityRecord player)
|| player.Snapshot.Position is not { } playerPosition) || player.Snapshot.Position is not { } playerPosition)
@ -144,7 +145,7 @@ internal sealed class LiveEntityLivenessController
if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current) if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current)
&& current.Generation == candidate.Generation) && current.Generation == candidate.Generation)
{ {
_prune(candidate); _prune.Prune(candidate);
} }
} }
} }

View file

@ -1,4 +1,5 @@
using AcDream.App.Interaction; using AcDream.App.Interaction;
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.Rendering.Vfx;
@ -38,7 +39,7 @@ internal sealed class LiveEntityRuntimeTeardownController
private readonly ShadowObjectRegistry? _shadows; private readonly ShadowObjectRegistry? _shadows;
private readonly LiveEntityLightController? _lights; private readonly LiveEntityLightController? _lights;
private readonly EntityClassificationCache? _classification; private readonly EntityClassificationCache? _classification;
private readonly Func<uint>? _playerGuid; private readonly ILocalPlayerIdentitySource? _identity;
private readonly Func<LiveEntityRecord, LiveEntityTeardownPlan> _createPlan; private readonly Func<LiveEntityRecord, LiveEntityTeardownPlan> _createPlan;
private readonly Action<uint> _forgetUnknownOwner; private readonly Action<uint> _forgetUnknownOwner;
@ -57,7 +58,7 @@ internal sealed class LiveEntityRuntimeTeardownController
ShadowObjectRegistry shadows, ShadowObjectRegistry shadows,
LiveEntityLightController lights, LiveEntityLightController lights,
EntityClassificationCache classification, EntityClassificationCache classification,
Func<uint> playerGuid) ILocalPlayerIdentitySource identity)
{ {
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation)); _presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
@ -77,7 +78,7 @@ internal sealed class LiveEntityRuntimeTeardownController
_lights = lights ?? throw new ArgumentNullException(nameof(lights)); _lights = lights ?? throw new ArgumentNullException(nameof(lights));
_classification = classification _classification = classification
?? throw new ArgumentNullException(nameof(classification)); ?? throw new ArgumentNullException(nameof(classification));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid)); _identity = identity ?? throw new ArgumentNullException(nameof(identity));
_createPlan = CreatePlan; _createPlan = CreatePlan;
_forgetUnknownOwner = effects.ForgetUnknownOwner; _forgetUnknownOwner = effects.ForgetUnknownOwner;
} }
@ -158,7 +159,7 @@ internal sealed class LiveEntityRuntimeTeardownController
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id)); cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld( cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
record, record,
_playerGuid!())); _identity!.ServerGuid));
cleanups.Add(() => _children!.OnLogicalTeardown(record)); cleanups.Add(() => _children!.OnLogicalTeardown(record));
cleanups.Add(() => _shadows!.Deregister(existingEntity.Id)); cleanups.Add(() => _shadows!.Deregister(existingEntity.Id));
cleanups.Add(() => _lights!.Forget(existingEntity.Id)); cleanups.Add(() => _lights!.Forget(existingEntity.Id));

View file

@ -1,3 +1,5 @@
using System.Numerics;
namespace AcDream.App.World; namespace AcDream.App.World;
/// <summary> /// <summary>
@ -40,6 +42,23 @@ internal sealed class LiveWorldOriginState
CenterY = centerY; CenterY = centerY;
} }
public (int X, int Y) GetCenter() => (CenterX, CenterY);
/// <summary>
/// Converts the streamed render frame back into retail's landblock-local
/// physics frame at the one placement seed boundary.
/// </summary>
public Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
{
int landblockX = (int)((cellId >> 24) & 0xFFu);
int landblockY = (int)((cellId >> 16) & 0xFFu);
var origin = new Vector3(
(landblockX - CenterX) * 192f,
(landblockY - CenterY) * 192f,
0f);
return worldPosition - origin;
}
/// <summary> /// <summary>
/// Ends the accepted-origin lifetime while retaining the last coordinates /// Ends the accepted-origin lifetime while retaining the last coordinates
/// as the next placeholder. Consumers must gate them on /// as the next placeholder. Consumers must gate them on

View file

@ -0,0 +1,43 @@
using AcDream.App.Input;
using AcDream.App.Net;
namespace AcDream.App.World;
internal interface IAcceptedLocalPhysicsTimestampPublisher
{
void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps);
}
/// <summary>
/// Publishes accepted local-player physics timestamps to the current exact
/// session without retaining the window host.
/// </summary>
internal sealed class LiveSessionLocalPhysicsTimestampPublisher
: IAcceptedLocalPhysicsTimestampPublisher
{
private readonly ILocalPlayerIdentitySource _identity;
private readonly ILiveWorldSessionSource _session;
public LiveSessionLocalPhysicsTimestampPublisher(
ILocalPlayerIdentitySource identity,
ILiveWorldSessionSource session)
{
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
_session = session ?? throw new ArgumentNullException(nameof(session));
}
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
{
if (serverGuid != _identity.ServerGuid
|| _session.CurrentSession is not { } session)
{
return;
}
session.PublishAcceptedLocalPhysicsTimestamps(
timestamps.Instance,
timestamps.ServerControlledMove,
timestamps.Teleport,
timestamps.ForcePosition);
}
}

View file

@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.World; using AcDream.App.World;
@ -1594,9 +1595,14 @@ public sealed class LiveEntityHydrationControllerTests
public readonly RecordingOrigin Origin; public readonly RecordingOrigin Origin;
public readonly RecordingNetworkSink Network = new(); public readonly RecordingNetworkSink Network = new();
public readonly RecordingTeardownCoordinator Teardown = new(); public readonly RecordingTeardownCoordinator Teardown = new();
public readonly RecordingTimestampPublisher Timestamps;
public readonly LiveEntityHydrationController Controller; public readonly LiveEntityHydrationController Controller;
public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction { get; set; } public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction
{
get => Timestamps.PublishAction;
set => Timestamps.PublishAction = value;
}
public Fixture( public Fixture(
bool originKnown, bool originKnown,
@ -1617,6 +1623,7 @@ public sealed class LiveEntityHydrationControllerTests
Relationships = new RecordingRelationships(Operations); Relationships = new RecordingRelationships(Operations);
Ready = new RecordingReadyPublisher(Operations); Ready = new RecordingReadyPublisher(Operations);
Origin = new RecordingOrigin(originKnown); Origin = new RecordingOrigin(originKnown);
Timestamps = new RecordingTimestampPublisher(Operations);
Network.ApplyAction = events => Network.ApplyAction = events =>
{ {
if (events.Position is not { } position) if (events.Position is not { } position)
@ -1631,6 +1638,15 @@ public sealed class LiveEntityHydrationControllerTests
out _, out _,
out _); out _);
}; };
var identity = new LocalPlayerIdentityState
{
ServerGuid = playerGuid,
};
var deletion = new LiveEntityDeletionController(
Runtime,
Objects,
Teardown,
identity);
Controller = new LiveEntityHydrationController( Controller = new LiveEntityHydrationController(
Runtime, Runtime,
Objects, Objects,
@ -1640,13 +1656,9 @@ public sealed class LiveEntityHydrationControllerTests
Ready, Ready,
Origin, Origin,
Network, Network,
Teardown, Timestamps,
(guid, timestamps) => identity,
{ deletion);
Operations.Add($"timestamps:{timestamps.Instance}");
PublishTimestampsAction?.Invoke(guid, timestamps);
},
() => playerGuid);
} }
public LiveEntityRecord Record public LiveEntityRecord Record
@ -1671,6 +1683,23 @@ public sealed class LiveEntityHydrationControllerTests
} }
} }
private sealed class RecordingTimestampPublisher
: IAcceptedLocalPhysicsTimestampPublisher
{
private readonly List<string> _operations;
public RecordingTimestampPublisher(List<string> operations) =>
_operations = operations;
public Action<uint, AcceptedPhysicsTimestamps>? PublishAction { get; set; }
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
{
_operations.Add($"timestamps:{timestamps.Instance}");
PublishAction?.Invoke(serverGuid, timestamps);
}
}
private class RecordingResources : ILiveEntityResourceLifecycle private class RecordingResources : ILiveEntityResourceLifecycle
{ {
public int RegisterCount { get; protected set; } public int RegisterCount { get; protected set; }

View file

@ -1,3 +1,4 @@
using System.Numerics;
using AcDream.App.World; using AcDream.App.World;
namespace AcDream.App.Tests.World; namespace AcDream.App.Tests.World;
@ -59,4 +60,17 @@ public sealed class LiveWorldOriginStateTests
Assert.Equal(50, state.CenterX); Assert.Equal(50, state.CenterX);
Assert.Equal(51, state.CenterY); Assert.Equal(51, state.CenterY);
} }
[Fact]
public void CellLocalForSeed_UsesTheCurrentLandblockOrigin()
{
var state = new LiveWorldOriginState();
state.SetPlaceholder(0x30, 0x32);
Vector3 local = state.CellLocalForSeed(
new Vector3(200f, -300f, 5f),
0x3130_0001u);
Assert.Equal(new Vector3(8f, 84f, 5f), local);
}
} }

View file

@ -36,9 +36,8 @@ public sealed class UpdateFrameOrchestratorTests
[Fact] [Fact]
public void ConditionalReconciles_RemainAtTheirOwningEdges() public void ConditionalReconciles_RemainAtTheirOwningEdges()
{ {
// Checkpoint A freezes only the outer ownership contract. Checkpoints // The outer ownership oracle is complemented by the production
// E and F must replace these probes with production teleport/camera // teleport and camera tests that pin both conditional reconcile edges.
// owner tests before the conditional edges can be claimed guarded.
var calls = new List<string>(); var calls = new List<string>();
UpdateFrameOrchestrator frame = Create( UpdateFrameOrchestrator frame = Create(
calls, calls,
@ -228,19 +227,6 @@ 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 D replaced input capture with its typed owner.
// Checkpoint F still owns the remaining player presentation
// callback composition. No other phase owner may add one.
Assert.Contains(
ownerFields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
Assert.DoesNotContain(
ownerFields,
field => field.FieldType == typeof(Func<AcDream.App.Input.MovementInput>));
continue;
}
Assert.DoesNotContain( Assert.DoesNotContain(
ownerFields, ownerFields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType)); field => typeof(Delegate).IsAssignableFrom(field.FieldType));
@ -277,14 +263,24 @@ public sealed class UpdateFrameOrchestratorTests
[Fact] [Fact]
public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce() public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce()
{ {
string root = FindRepoRoot();
string source = File.ReadAllText(Path.Combine( string source = File.ReadAllText(Path.Combine(
FindRepoRoot(), root,
"src", "src",
"AcDream.App", "AcDream.App",
"Rendering", "Rendering",
"GameWindow.cs")); "GameWindow.cs"));
string adapters = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Update",
"UpdateFrameRuntimeAdapters.cs"));
Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1); Assert.Equal(0, CountOccurrences(source, "PublishTime("));
Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime("));
Assert.Contains("new AcDream.App.Update.PhysicsScriptClockPublisher(", source,
StringComparison.Ordinal);
} }
[Fact] [Fact]
@ -363,12 +359,10 @@ public sealed class UpdateFrameOrchestratorTests
"GameWindow.cs")); "GameWindow.cs"));
Assert.Contains("new AcDream.App.Streaming.StreamingFrameController(", source); Assert.Contains("new AcDream.App.Streaming.StreamingFrameController(", source);
Assert.Equal(1, CountOccurrences(source, "_streamingFrame.Tick();")); Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
AssertAppearsInOrder( Assert.Equal(1, CountOccurrences(
source, source,
"_streamingFrame.Tick();", "_updateFrameOrchestrator.Tick("));
"_gameplayInputFrame!.Tick(frameTiming);",
"_liveFrameCoordinator.Tick(frameDelta);");
Assert.DoesNotContain("_streamingController.Tick(observerCx", source, Assert.DoesNotContain("_streamingController.Tick(observerCx", source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain("DungeonStreamingGate.Compute", source, Assert.DoesNotContain("DungeonStreamingGate.Compute", source,
@ -387,7 +381,8 @@ public sealed class UpdateFrameOrchestratorTests
"GameWindow.cs")); "GameWindow.cs"));
Assert.Contains("new AcDream.App.Input.GameplayInputFrameController(", source); Assert.Contains("new AcDream.App.Input.GameplayInputFrameController(", source);
Assert.Equal(1, CountOccurrences(source, "_gameplayInputFrame!.Tick(frameTiming);")); Assert.DoesNotContain("_gameplayInputFrame!.Tick", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_inputDispatcher?.Tick()", source, Assert.DoesNotContain("_inputDispatcher?.Tick()", source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain("TryTakeRawSample", source, Assert.DoesNotContain("TryTakeRawSample", source,
@ -468,9 +463,8 @@ public sealed class UpdateFrameOrchestratorTests
"new AcDream.App.Streaming.LocalPlayerTeleportController(", "new AcDream.App.Streaming.LocalPlayerTeleportController(",
source, source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences( Assert.DoesNotContain("_localPlayerTeleport!.Tick", source,
source, StringComparison.Ordinal);
"_localPlayerTeleport!.Tick(frameDelta);"));
Assert.DoesNotContain("_teleportTransit", source, StringComparison.Ordinal); Assert.DoesNotContain("_teleportTransit", source, StringComparison.Ordinal);
Assert.DoesNotContain("_teleportAnim", source, StringComparison.Ordinal); Assert.DoesNotContain("_teleportAnim", source, StringComparison.Ordinal);
Assert.DoesNotContain("_teleportViewPlane", source, StringComparison.Ordinal); Assert.DoesNotContain("_teleportViewPlane", source, StringComparison.Ordinal);
@ -486,9 +480,10 @@ public sealed class UpdateFrameOrchestratorTests
StringComparison.Ordinal); StringComparison.Ordinal);
AssertAppearsInOrder( AssertAppearsInOrder(
source, source,
"_liveEntityLiveness?.Tick(ClientTimerNow());", "new AcDream.App.Update.LiveEntityLivenessFramePhase(",
"_localPlayerTeleport!.Tick(frameDelta);", "_localPlayerTeleport,",
"_playerModeAutoEntry?.TryEnter();"); "new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
"cameraFrame);");
} }
[Fact] [Fact]
@ -649,7 +644,9 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App", "AcDream.App",
"Rendering", "Rendering",
"GameWindow.cs")); "GameWindow.cs"));
Assert.Equal(1, CountOccurrences(source, "_cameraFrame.Tick(frameTiming);")); Assert.Equal(1, CountOccurrences(
source,
"_updateFrameOrchestrator.Tick("));
Assert.DoesNotContain("CanAdvanceLocalPlayer", source, StringComparison.Ordinal); Assert.DoesNotContain("CanAdvanceLocalPlayer", source, StringComparison.Ordinal);
Assert.DoesNotContain("GetCombatCameraTargetPoint()", source, Assert.DoesNotContain("GetCombatCameraTargetPoint()", source,
StringComparison.Ordinal); StringComparison.Ordinal);
@ -657,11 +654,7 @@ public sealed class UpdateFrameOrchestratorTests
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain("_localPlayerFrame.TryGetPresentationAfterNetwork", source, Assert.DoesNotContain("_localPlayerFrame.TryGetPresentationAfterNetwork", source,
StringComparison.Ordinal); StringComparison.Ordinal);
AssertAppearsInOrder( Assert.DoesNotContain("_cameraFrame", source, StringComparison.Ordinal);
source,
"_localPlayerTeleport!.Tick(frameDelta);",
"_playerModeAutoEntry?.TryEnter();",
"_cameraFrame.Tick(frameTiming);");
string cameraSource = File.ReadAllText(Path.Combine( string cameraSource = File.ReadAllText(Path.Combine(
root, root,
@ -677,6 +670,123 @@ public sealed class UpdateFrameOrchestratorTests
"retail?.Update("); "retail?.Update(");
} }
[Fact]
public void GameWindow_OnUpdateOwnsOnlyProfilingAndOneOrchestratorTick()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Equal(1, CountOccurrences(
source,
"_updateFrameOrchestrator.Tick("));
Assert.DoesNotContain("_updateFrameClock.Advance(", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_liveFrameCoordinator", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_liveEntityLiveness?.Tick", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_playerModeAutoEntry?.TryEnter", source,
StringComparison.Ordinal);
AssertAppearsInOrder(
source,
"private void OnUpdate(double dt)",
"_frameProfiler.BeginStage(",
"_updateFrameOrchestrator.Tick(",
"private void OnCameraModeChanged");
}
[Fact]
public void ProductionFrameAdaptersRetainTypedOwnersWithoutWindowCallbacks()
{
Type[] adapters =
[
typeof(LiveEntityTeardownFramePhase),
typeof(ConsoleUpdateFrameFailureSink),
typeof(PhysicsScriptClockPublisher),
typeof(LiveEntityLivenessFramePhase),
typeof(PlayerModeAutoEntryFramePhase),
];
foreach (Type adapter in adapters)
{
FieldInfo[] fields = adapter.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
FieldInfo clock = Assert.Single(
typeof(LiveEntityLivenessFramePhase).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_clock");
Assert.Equal(typeof(IClientMonotonicTimeSource), clock.FieldType);
FieldInfo teardownRuntime = Assert.Single(
typeof(LiveEntityTeardownFramePhase).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic));
Assert.Equal(typeof(LiveEntityRuntime), teardownRuntime.FieldType);
Type[] typedProductionOwners =
[
typeof(LiveEntityLivenessController),
typeof(AcDream.App.Input.LivePlayerModeAutoEntryContext),
typeof(LiveSessionLocalPhysicsTimestampPublisher),
typeof(AcDream.App.Physics.LiveEntityNetworkUpdateController),
typeof(AcDream.App.Rendering.LiveEntityPartArrayLifecycle),
typeof(AcDream.App.Physics.RemoteShadowPlacementSynchronizer),
typeof(AcDream.App.Physics.RemoteTeleportPlacementPresentation),
typeof(AcDream.App.Net.LiveEntitySessionController),
];
foreach (Type owner in typedProductionOwners)
{
FieldInfo[] fields = owner.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
FieldInfo originIdentity = Assert.Single(
typeof(LiveEntityWorldOriginCoordinator).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_identity");
Assert.Equal(
typeof(AcDream.App.Input.ILocalPlayerIdentitySource),
originIdentity.FieldType);
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
StringComparison.Ordinal);
Assert.DoesNotContain("LoginWorldReady", source,
StringComparison.Ordinal);
Assert.DoesNotContain("candidate => _liveEntityHydration.OnPrune", source,
StringComparison.Ordinal);
Assert.DoesNotContain("CreateLiveEntitySessionSink", source,
StringComparison.Ordinal);
Assert.DoesNotContain("private System.Numerics.Vector3 CellLocalForSeed", source,
StringComparison.Ordinal);
Assert.DoesNotContain("OnPlayScriptReceived", source,
StringComparison.Ordinal);
Assert.Contains("_liveWorldOrigin.GetCenter", source,
StringComparison.Ordinal);
Assert.Contains("_liveWorldOrigin.CellLocalForSeed", source,
StringComparison.Ordinal);
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
StringComparison.Ordinal);
}
private static UpdateFrameOrchestrator Create( private static UpdateFrameOrchestrator Create(
List<string> calls, List<string> calls,
RecordingTeardown? teardown = null, RecordingTeardown? teardown = null,