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

@ -1,11 +1,14 @@
using System;
using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.World;
namespace AcDream.App.Input;
/// <summary>
/// Phase K.2 — one-shot guard that auto-enters player mode after a
/// successful login once every prerequisite is satisfied. Owned by
/// <c>GameWindow</c> and ticked each frame from <c>OnUpdate</c>.
/// successful login once every prerequisite is satisfied. The update-frame
/// orchestrator ticks it through a typed production context.
///
/// <para>
/// Why is this its own class? The auto-entry has four independent
@ -33,20 +36,108 @@ namespace AcDream.App.Input;
/// </para>
///
/// <para>
/// All preconditions are passed in as predicates so the class doesn't
/// pull in <c>WorldSession</c>, <c>PlayerMovementController</c>, or
/// any GameWindow-internal types — the unit test wires them to plain
/// boolean fields.
/// Production preconditions come from focused runtime owners rather than the
/// window host. The public delegate constructor remains a narrow test seam for
/// plain boolean fixtures.
/// </para>
/// </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
{
private readonly Func<bool> _isLiveInWorld;
private readonly Func<bool> _isPlayerEntityPresent;
private readonly Func<bool> _isPlayerControllerReady;
private readonly Func<bool> _isWorldReady;
private readonly Func<bool> _isPlayerModeActive;
private readonly Action _enterPlayerMode;
private sealed class DelegateContext : IPlayerModeAutoEntryContext
{
private readonly Func<bool> _isLiveInWorld;
private readonly Func<bool> _isPlayerEntityPresent;
private readonly Func<bool> _isPlayerControllerReady;
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;
@ -72,20 +163,24 @@ public sealed class PlayerModeAutoEntry
/// player transition). Must construct the controller + chase
/// camera and switch the active camera; the auto-entry doesn't
/// reach inside.</param>
internal PlayerModeAutoEntry(IPlayerModeAutoEntryContext context) =>
_context = context ?? throw new ArgumentNullException(nameof(context));
public PlayerModeAutoEntry(
Func<bool> isLiveInWorld,
Func<bool> isPlayerEntityPresent,
Func<bool> isPlayerControllerReady,
Func<bool> isWorldReady,
Action enterPlayerMode,
Action enterPlayerMode,
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
@ -115,18 +210,18 @@ public sealed class PlayerModeAutoEntry
public bool TryEnter()
{
if (!_armed) return false;
if (_isPlayerModeActive())
if (_context.IsPlayerModeActive)
{
_armed = false;
return false;
}
if (!_isLiveInWorld()) return false;
if (!_isPlayerEntityPresent()) return false;
if (!_isPlayerControllerReady()) return false;
if (!_isWorldReady()) return false;
if (!_context.IsLiveInWorld) return false;
if (!_context.IsPlayerEntityPresent) return false;
if (!_context.IsPlayerControllerReady) return false;
if (!_context.IsWorldReady) return false;
_armed = false;
_enterPlayerMode();
_context.EnterPlayerMode();
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.Input;
using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
@ -50,18 +51,18 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly LiveWorldOriginState _origin;
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
_localPlayerTeleport;
private readonly Func<PlayerMovementController?> _playerControllerSource;
private readonly ILocalPlayerControllerSource _playerControllerSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly Func<EntityPhysicsHost?> _playerHostSource;
private readonly Func<uint> _playerGuid;
private readonly ILocalPlayerPhysicsHostSource _playerHostSource;
private readonly ILocalPlayerIdentitySource _playerIdentity;
private readonly IPhysicsScriptTimeSource _gameTime;
private readonly Func<WorldSession?> _session;
private readonly ILiveWorldSessionSource _session;
private readonly LiveEntityInboundAuthorityGate _authorityGate;
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
private PlayerMovementController? _playerController => _playerControllerSource();
private EntityPhysicsHost? _playerHost => _playerHostSource();
private uint _playerServerGuid => _playerGuid();
private PlayerMovementController? _playerController => _playerControllerSource.Controller;
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
private uint _playerServerGuid => _playerIdentity.ServerGuid;
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
_liveEntities.MaterializedWorldEntities;
@ -96,12 +97,12 @@ internal sealed class LiveEntityNetworkUpdateController
CombatTargetController? combatTargetController,
LiveWorldOriginState origin,
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
Func<PlayerMovementController?> playerControllerSource,
ILocalPlayerControllerSource playerControllerSource,
LocalPlayerOutboundController localPlayerOutbound,
Func<EntityPhysicsHost?> playerHostSource,
Func<uint> playerGuid,
ILocalPlayerPhysicsHostSource playerHostSource,
ILocalPlayerIdentitySource playerIdentity,
IPhysicsScriptTimeSource gameTime,
Func<WorldSession?> session,
ILiveWorldSessionSource session,
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
IMovementTruthDiagnosticSink movementTruthDiagnostics)
{
@ -131,7 +132,7 @@ internal sealed class LiveEntityNetworkUpdateController
_localPlayerOutbound = localPlayerOutbound
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
_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));
_session = session ?? throw new ArgumentNullException(nameof(session));
_authorityGate = new LiveEntityInboundAuthorityGate(
@ -1029,7 +1030,7 @@ internal sealed class LiveEntityNetworkUpdateController
p.PositionY,
p.PositionZ)),
() => _localPlayerOutbound.SendImmediatePosition(
_session(),
_session.CurrentSession,
_playerController)))
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.StreamingOriginRecenterCoordinator?
_streamingOriginRecenter;
private AcDream.App.Update.IStreamingFramePhase _streamingFrame = null!;
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
_localPlayerTeleportSink = new();
@ -173,10 +172,7 @@ public sealed class GameWindow : IDisposable
private readonly AcDream.App.Input.MovementTruthDiagnosticController
_movementTruthDiagnostics;
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
private AcDream.App.Update.ICameraFramePhase _cameraFrame = null!;
private AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator = null!;
private AcDream.App.Update.LiveSpatialPresentationReconciler
_liveSpatialReconciler = null!;
private AcDream.App.Update.UpdateFrameOrchestrator _updateFrameOrchestrator = 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;
@ -186,6 +182,7 @@ public sealed class GameWindow : IDisposable
_liveEntityProjectionWithdrawal;
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!;
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
_liveEntityMotionBindings = new();
@ -2170,36 +2167,32 @@ public sealed class GameWindow : IDisposable
_entityEffects = entityEffects;
entityEffects.DiagnosticSink = message =>
Console.Error.WriteLine($"vfx: {message}");
var partArrayLifecycle =
new AcDream.App.Rendering.LiveEntityPartArrayLifecycle(
_animatedEntities);
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
_liveEntities,
_physicsEngine.ShadowObjects,
entityEffects.PlayTypedFromHiddenTransition,
_equippedChildRenderer.SetDirectChildrenNoDraw,
_liveEntityMotionBindings.ClearTargetForHiddenEntity,
() => (_liveCenterX, _liveCenterY),
handlePartArrayEnterWorld: localEntityId =>
{
if (_animatedEntities.TryGetValue(localEntityId, out var animated))
animated.Sequencer?.Manager.HandleEnterWorld();
});
_liveWorldOrigin.GetCenter,
handlePartArrayEnterWorld: partArrayLifecycle.HandleEnterWorld);
var remoteShadowPlacement =
new AcDream.App.Physics.RemoteShadowPlacementSynchronizer(
_remotePhysicsUpdater,
_liveWorldOrigin);
var remoteTeleportPresentation =
new AcDream.App.Physics.RemoteTeleportPlacementPresentation(
_liveEntityPresentation);
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
_physicsEngine,
_liveEntities,
_liveEntityMotionBindings.GetSetupCylinder,
CellLocalForSeed,
(entity, remote, cellId) => _remotePhysicsUpdater.SyncRemoteShadowToBody(
entity.Id,
remote,
_liveCenterX,
_liveCenterY,
cellId),
(guid, generation, deferShadowRestore) =>
_liveEntityPresentation.CompleteAuthoritativePlacement(
guid,
generation,
deferShadowRestore),
(guid, generation) =>
_liveEntityPresentation.BeginAuthoritativePlacement(guid, generation));
_liveWorldOrigin.CellLocalForSeed,
remoteShadowPlacement.Sync,
remoteTeleportPresentation.Complete,
remoteTeleportPresentation.Begin);
_equippedChildRenderer.ProjectionPoseReady += guid =>
_liveEntityLights.OnAttachedPoseReady(guid);
_hookRouter.Register(entityEffects);
@ -2484,9 +2477,14 @@ public sealed class GameWindow : IDisposable
_streamingController,
_worldState,
_worldReveal,
() => _playerServerGuid,
_localPlayerIdentity,
sealedDungeonCells,
Console.WriteLine);
_liveSessionController = new AcDream.App.Net.LiveSessionController();
var localPhysicsTimestamps =
new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
_localPlayerIdentity,
_liveSessionController);
var liveEntityTeardown =
new AcDream.App.World.LiveEntityRuntimeTeardownController(
_liveEntities,
@ -2503,8 +2501,15 @@ public sealed class GameWindow : IDisposable
_physicsEngine.ShadowObjects,
_liveEntityLights!,
_classificationCache,
() => _playerServerGuid);
_localPlayerIdentity);
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(
_liveEntities,
Objects,
@ -2518,9 +2523,9 @@ public sealed class GameWindow : IDisposable
_liveEntityPresentation!),
originCoordinator,
networkUpdateBridge,
liveEntityTeardown,
PublishLocalPhysicsTimestamps,
() => _playerServerGuid,
localPhysicsTimestamps,
_localPlayerIdentity,
liveEntityDeletion,
_options.DumpLiveSpawns ? Console.WriteLine : null);
_liveEntityNetworkUpdates =
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
@ -2546,18 +2551,24 @@ public sealed class GameWindow : IDisposable
_combatTargetController,
_liveWorldOrigin,
_localPlayerTeleportSink,
() => _playerController,
_playerControllerSlot,
_localPlayerOutbound,
() => _playerHost,
() => _playerServerGuid,
_playerHostSlot,
_localPlayerIdentity,
_updateFrameClock,
() => LiveSession,
PublishLocalPhysicsTimestamps,
_liveSessionController,
localPhysicsTimestamps.Publish,
_movementTruthDiagnostics);
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
_liveEntities,
() => _playerServerGuid,
candidate => _liveEntityHydration.OnPrune(candidate));
_localPlayerIdentity,
liveEntityDeletion);
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
_inboundEntityEvents,
_liveEntityHydration,
_liveEntityNetworkUpdates,
_localPlayerTeleportSink,
_entityEffects!);
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
_equippedChildRenderer.EntityReady += candidate =>
@ -2573,7 +2584,6 @@ public sealed class GameWindow : IDisposable
// CreateObject messages into _worldGameState as they arrive. Entirely
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
_liveSessionController = new AcDream.App.Net.LiveSessionController();
_combatAttackOperations.Bind(
new AcDream.App.Combat.LiveCombatAttackOperations(
Combat,
@ -2609,7 +2619,7 @@ public sealed class GameWindow : IDisposable
mouseLookController,
new AcDream.App.Input.CombatAttackInputFrameAdapter(
_combatAttackController!));
_streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
_options.LiveMode,
_localPlayerMode,
_playerControllerSlot,
@ -2638,7 +2648,7 @@ public sealed class GameWindow : IDisposable
new AcDream.App.Update.SettingsParticleRangeSource(
_settingsVm,
_persistedDisplay.ParticleRange));
_liveSpatialReconciler =
var liveSpatialReconciler =
new AcDream.App.Update.LiveSpatialPresentationReconciler(
_entityEffects!,
_equippedChildRenderer!,
@ -2721,16 +2731,15 @@ public sealed class GameWindow : IDisposable
_localPlayerSkills,
_viewportAspect);
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
isLiveInWorld: () => _liveSessionController.IsInWorld,
isPlayerEntityPresent: () =>
_liveEntities.MaterializedWorldEntities.ContainsKey(
_playerServerGuid),
isPlayerControllerReady: () => true,
// Retail SmartBox::UseTime @ 0x00455410 completes player
// position only after the destination cell load clears.
isWorldReady: LoginWorldReady,
enterPlayerMode: _playerModeController.EnterFromAutoEntry,
isPlayerModeActive: () => _localPlayerMode.IsPlayerMode);
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
_liveSessionController,
_liveEntities,
_localPlayerIdentity,
_worldReveal
?? throw new InvalidOperationException(
"World reveal was not composed before player auto-entry."),
_localPlayerMode,
_playerModeController));
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
var localTeleportPresentation =
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
@ -2758,7 +2767,7 @@ public sealed class GameWindow : IDisposable
_playerHostSlot,
_chaseCameraInput,
_liveWorldOrigin,
_liveSpatialReconciler),
liveSpatialReconciler),
new AcDream.App.Streaming.LocalPlayerTeleportSession(
_liveSessionController),
localTeleportPresentation);
@ -2767,25 +2776,41 @@ public sealed class GameWindow : IDisposable
// remains only as the staged-startup fallback used by shutdown when
// composition fails before the transfer.
_portalTunnel = null;
_liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
liveObjectFrame,
_worldState,
_liveSessionController,
localPlayerFrame,
_liveSpatialReconciler);
_cameraFrame = new AcDream.App.Rendering.CameraFrameController(
liveSpatialReconciler);
var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
_cameraController!,
_inputCapture,
_cameraInput,
localPlayerFrameRuntime,
_chaseCameraInput,
localPlayerFrame,
_liveSpatialReconciler,
liveSpatialReconciler,
new AcDream.App.Combat.CombatCameraTargetSource(
_gameplaySettings,
Combat,
_selection,
_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 =
_liveSessionController.Start(
_options,
@ -2958,7 +2983,7 @@ public sealed class GameWindow : IDisposable
{
events = new AcDream.App.Net.LiveSessionEventRouter(
session,
CreateLiveEntitySessionSink(),
_liveEntitySessionEvents.CreateSink(),
new AcDream.App.Net.LiveEnvironmentSessionSink(
OnEnvironChanged,
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
CreateLiveInventorySessionBindings() => new(
Objects,
@ -3276,47 +3263,6 @@ public sealed class GameWindow : IDisposable
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)
{
cell = 0;
@ -3329,20 +3275,6 @@ public sealed class GameWindow : IDisposable
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(
float dayFraction,
AcDream.Core.World.DayGroupData? dayGroup,
@ -3512,76 +3444,10 @@ public sealed class GameWindow : IDisposable
}
private void OnUpdate(double dt)
{
using var _updStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Update);
// A resource callback can request deletion while the presentation
// 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(
using var _updStage = _frameProfiler.BeginStage(
AcDream.App.Diagnostics.FrameStage.Update);
_updateFrameOrchestrator.Tick(
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)

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,5 @@
using System.Numerics;
namespace AcDream.App.World;
/// <summary>
@ -40,6 +42,23 @@ internal sealed class LiveWorldOriginState
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>
/// Ends the accepted-origin lifetime while retaining the last coordinates
/// 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);
}
}