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:
parent
947c61d2d7
commit
e91f310279
18 changed files with 864 additions and 348 deletions
|
|
@ -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
|
||||
camera presentation and cut production over to one
|
||||
`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.
|
||||
- [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized
|
||||
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
|
||||
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
|
||||
|
||||
After all three independent corrected-diff reviews are clean:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
97
src/AcDream.App/Net/LiveEntitySessionController.cs
Normal file
97
src/AcDream.App/Net/LiveEntitySessionController.cs
Normal 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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
49
src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs
Normal file
49
src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
19
src/AcDream.App/Rendering/LiveEntityPartArrayLifecycle.cs
Normal file
19
src/AcDream.App/Rendering/LiveEntityPartArrayLifecycle.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
78
src/AcDream.App/Update/UpdateFrameRuntimeAdapters.cs
Normal file
78
src/AcDream.App/Update/UpdateFrameRuntimeAdapters.cs
Normal 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();
|
||||
}
|
||||
69
src/AcDream.App/World/LiveEntityDeletionController.cs
Normal file
69
src/AcDream.App/World/LiveEntityDeletionController.cs
Normal 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));
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
43
src/AcDream.App/World/LocalPhysicsTimestampPublisher.cs
Normal file
43
src/AcDream.App/World/LocalPhysicsTimestampPublisher.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -1594,9 +1595,14 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
public readonly RecordingOrigin Origin;
|
||||
public readonly RecordingNetworkSink Network = new();
|
||||
public readonly RecordingTeardownCoordinator Teardown = new();
|
||||
public readonly RecordingTimestampPublisher Timestamps;
|
||||
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(
|
||||
bool originKnown,
|
||||
|
|
@ -1617,6 +1623,7 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Relationships = new RecordingRelationships(Operations);
|
||||
Ready = new RecordingReadyPublisher(Operations);
|
||||
Origin = new RecordingOrigin(originKnown);
|
||||
Timestamps = new RecordingTimestampPublisher(Operations);
|
||||
Network.ApplyAction = events =>
|
||||
{
|
||||
if (events.Position is not { } position)
|
||||
|
|
@ -1631,6 +1638,15 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
out _,
|
||||
out _);
|
||||
};
|
||||
var identity = new LocalPlayerIdentityState
|
||||
{
|
||||
ServerGuid = playerGuid,
|
||||
};
|
||||
var deletion = new LiveEntityDeletionController(
|
||||
Runtime,
|
||||
Objects,
|
||||
Teardown,
|
||||
identity);
|
||||
Controller = new LiveEntityHydrationController(
|
||||
Runtime,
|
||||
Objects,
|
||||
|
|
@ -1640,13 +1656,9 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Ready,
|
||||
Origin,
|
||||
Network,
|
||||
Teardown,
|
||||
(guid, timestamps) =>
|
||||
{
|
||||
Operations.Add($"timestamps:{timestamps.Instance}");
|
||||
PublishTimestampsAction?.Invoke(guid, timestamps);
|
||||
},
|
||||
() => playerGuid);
|
||||
Timestamps,
|
||||
identity,
|
||||
deletion);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
public int RegisterCount { get; protected set; }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
|
@ -59,4 +60,17 @@ public sealed class LiveWorldOriginStateTests
|
|||
Assert.Equal(50, state.CenterX);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
[Fact]
|
||||
public void ConditionalReconciles_RemainAtTheirOwningEdges()
|
||||
{
|
||||
// Checkpoint A freezes only the outer ownership contract. Checkpoints
|
||||
// E and F must replace these probes with production teleport/camera
|
||||
// owner tests before the conditional edges can be claimed guarded.
|
||||
// The outer ownership oracle is complemented by the production
|
||||
// teleport and camera tests that pin both conditional reconcile edges.
|
||||
var calls = new List<string>();
|
||||
UpdateFrameOrchestrator frame = Create(
|
||||
calls,
|
||||
|
|
@ -228,19 +227,6 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
FieldInfo[] ownerFields = owner.GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
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(
|
||||
ownerFields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
|
|
@ -277,14 +263,24 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
[Fact]
|
||||
public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce()
|
||||
{
|
||||
string root = FindRepoRoot();
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"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]
|
||||
|
|
@ -363,12 +359,10 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"GameWindow.cs"));
|
||||
|
||||
Assert.Contains("new AcDream.App.Streaming.StreamingFrameController(", source);
|
||||
Assert.Equal(1, CountOccurrences(source, "_streamingFrame.Tick();"));
|
||||
AssertAppearsInOrder(
|
||||
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
source,
|
||||
"_streamingFrame.Tick();",
|
||||
"_gameplayInputFrame!.Tick(frameTiming);",
|
||||
"_liveFrameCoordinator.Tick(frameDelta);");
|
||||
"_updateFrameOrchestrator.Tick("));
|
||||
Assert.DoesNotContain("_streamingController.Tick(observerCx", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("DungeonStreamingGate.Compute", source,
|
||||
|
|
@ -387,7 +381,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"GameWindow.cs"));
|
||||
|
||||
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,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("TryTakeRawSample", source,
|
||||
|
|
@ -468,9 +463,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
source,
|
||||
"_localPlayerTeleport!.Tick(frameDelta);"));
|
||||
Assert.DoesNotContain("_localPlayerTeleport!.Tick", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_teleportTransit", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_teleportAnim", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_teleportViewPlane", source, StringComparison.Ordinal);
|
||||
|
|
@ -486,9 +480,10 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_liveEntityLiveness?.Tick(ClientTimerNow());",
|
||||
"_localPlayerTeleport!.Tick(frameDelta);",
|
||||
"_playerModeAutoEntry?.TryEnter();");
|
||||
"new AcDream.App.Update.LiveEntityLivenessFramePhase(",
|
||||
"_localPlayerTeleport,",
|
||||
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
|
||||
"cameraFrame);");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -649,7 +644,9 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"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("GetCombatCameraTargetPoint()", source,
|
||||
StringComparison.Ordinal);
|
||||
|
|
@ -657,11 +654,7 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_localPlayerFrame.TryGetPresentationAfterNetwork", source,
|
||||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_localPlayerTeleport!.Tick(frameDelta);",
|
||||
"_playerModeAutoEntry?.TryEnter();",
|
||||
"_cameraFrame.Tick(frameTiming);");
|
||||
Assert.DoesNotContain("_cameraFrame", source, StringComparison.Ordinal);
|
||||
|
||||
string cameraSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
|
|
@ -677,6 +670,123 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"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(
|
||||
List<string> calls,
|
||||
RecordingTeardown? teardown = null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue