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
|
- [x] F — extract the shared player-mode lifecycle plus fly/chase/player
|
||||||
camera presentation and cut production over to one
|
camera presentation and cut production over to one
|
||||||
`UpdateFrameOrchestrator`.
|
`UpdateFrameOrchestrator`.
|
||||||
- [ ] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
|
- [x] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
|
||||||
frame state; run focused corrected-diff reviews after each ownership edge.
|
frame state; run focused corrected-diff reviews after each ownership edge.
|
||||||
- [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized
|
- [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized
|
||||||
resource soak, documentation, durable memory, and line/field/method closeout.
|
resource soak, documentation, durable memory, and line/field/method closeout.
|
||||||
|
|
@ -469,6 +469,19 @@ passed / 5 skipped. All three corrected-diff reviews are clean.
|
||||||
`GameWindow.cs` is 7,160 lines, down 8,563 lines (54.5%) from the 15,723-line
|
`GameWindow.cs` is 7,160 lines, down 8,563 lines (54.5%) from the 15,723-line
|
||||||
slice baseline.
|
slice baseline.
|
||||||
|
|
||||||
|
**Checkpoint G completed 2026-07-22.** `GameWindow.OnUpdate` now owns only the
|
||||||
|
update profiler scope and one `UpdateFrameOrchestrator.Tick` call. Teardown,
|
||||||
|
absolute liveness time, local timestamp publication, expiry deletion,
|
||||||
|
player-mode auto-entry, live-origin initialization, Hidden PartArray
|
||||||
|
boundaries, remote teleport placement, and all twelve live-entity session
|
||||||
|
packet routes resolve through typed runtime owners; the former transitive
|
||||||
|
window callbacks and obsolete frame fields are gone. The App suite is green
|
||||||
|
at 2,823 passed / 3 skipped. Focused orchestration, lifecycle, presentation,
|
||||||
|
teleport, origin, and session tests are green, and all three corrected-diff
|
||||||
|
reviews are clean. `GameWindow.cs` is 7,026 raw lines / 241 fields / 108
|
||||||
|
methods, down 1,785 lines and 45 methods from the Slice-6 baseline and 8,697
|
||||||
|
lines (55.3%) from the 15,723-line pre-extraction class.
|
||||||
|
|
||||||
### H — release and connected gates
|
### H — release and connected gates
|
||||||
|
|
||||||
After all three independent corrected-diff reviews are clean:
|
After all three independent corrected-diff reviews are clean:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
|
using AcDream.App.Net;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.App.World;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase K.2 — one-shot guard that auto-enters player mode after a
|
/// Phase K.2 — one-shot guard that auto-enters player mode after a
|
||||||
/// successful login once every prerequisite is satisfied. Owned by
|
/// successful login once every prerequisite is satisfied. The update-frame
|
||||||
/// <c>GameWindow</c> and ticked each frame from <c>OnUpdate</c>.
|
/// orchestrator ticks it through a typed production context.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Why is this its own class? The auto-entry has four independent
|
/// Why is this its own class? The auto-entry has four independent
|
||||||
|
|
@ -33,20 +36,108 @@ namespace AcDream.App.Input;
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// All preconditions are passed in as predicates so the class doesn't
|
/// Production preconditions come from focused runtime owners rather than the
|
||||||
/// pull in <c>WorldSession</c>, <c>PlayerMovementController</c>, or
|
/// window host. The public delegate constructor remains a narrow test seam for
|
||||||
/// any GameWindow-internal types — the unit test wires them to plain
|
/// plain boolean fixtures.
|
||||||
/// boolean fields.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
internal interface IPlayerModeAutoEntryContext
|
||||||
|
{
|
||||||
|
bool IsLiveInWorld { get; }
|
||||||
|
bool IsPlayerEntityPresent { get; }
|
||||||
|
bool IsPlayerControllerReady { get; }
|
||||||
|
bool IsWorldReady { get; }
|
||||||
|
bool IsPlayerModeActive { get; }
|
||||||
|
void EnterPlayerMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Production auto-entry context over canonical runtime owners.</summary>
|
||||||
|
internal sealed class LivePlayerModeAutoEntryContext
|
||||||
|
: IPlayerModeAutoEntryContext
|
||||||
|
{
|
||||||
|
private readonly ILiveInWorldSource _session;
|
||||||
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
|
private readonly ILocalPlayerIdentitySource _identity;
|
||||||
|
private readonly WorldRevealCoordinator _worldReveal;
|
||||||
|
private readonly ILocalPlayerModeSource _mode;
|
||||||
|
private readonly PlayerModeController _playerMode;
|
||||||
|
|
||||||
|
public LivePlayerModeAutoEntryContext(
|
||||||
|
ILiveInWorldSource session,
|
||||||
|
LiveEntityRuntime liveEntities,
|
||||||
|
ILocalPlayerIdentitySource identity,
|
||||||
|
WorldRevealCoordinator worldReveal,
|
||||||
|
ILocalPlayerModeSource mode,
|
||||||
|
PlayerModeController playerMode)
|
||||||
|
{
|
||||||
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||||
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||||
|
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
||||||
|
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
||||||
|
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLiveInWorld => _session.IsInWorld;
|
||||||
|
|
||||||
|
public bool IsPlayerEntityPresent =>
|
||||||
|
_liveEntities.MaterializedWorldEntities.ContainsKey(_identity.ServerGuid);
|
||||||
|
|
||||||
|
public bool IsPlayerControllerReady => true;
|
||||||
|
|
||||||
|
public bool IsWorldReady =>
|
||||||
|
_liveEntities.TryGetSnapshot(
|
||||||
|
_identity.ServerGuid,
|
||||||
|
out AcDream.Core.Net.WorldSession.EntitySpawn player)
|
||||||
|
&& player.Position is { LandblockId: not 0u } position
|
||||||
|
&& _worldReveal.Evaluate(position.LandblockId).IsReady;
|
||||||
|
|
||||||
|
public bool IsPlayerModeActive => _mode.IsPlayerMode;
|
||||||
|
|
||||||
|
public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class PlayerModeAutoEntry
|
public sealed class PlayerModeAutoEntry
|
||||||
{
|
{
|
||||||
private readonly Func<bool> _isLiveInWorld;
|
private sealed class DelegateContext : IPlayerModeAutoEntryContext
|
||||||
private readonly Func<bool> _isPlayerEntityPresent;
|
{
|
||||||
private readonly Func<bool> _isPlayerControllerReady;
|
private readonly Func<bool> _isLiveInWorld;
|
||||||
private readonly Func<bool> _isWorldReady;
|
private readonly Func<bool> _isPlayerEntityPresent;
|
||||||
private readonly Func<bool> _isPlayerModeActive;
|
private readonly Func<bool> _isPlayerControllerReady;
|
||||||
private readonly Action _enterPlayerMode;
|
private readonly Func<bool> _isWorldReady;
|
||||||
|
private readonly Action _enterPlayerMode;
|
||||||
|
private readonly Func<bool> _isPlayerModeActive;
|
||||||
|
|
||||||
|
public DelegateContext(
|
||||||
|
Func<bool> isLiveInWorld,
|
||||||
|
Func<bool> isPlayerEntityPresent,
|
||||||
|
Func<bool> isPlayerControllerReady,
|
||||||
|
Func<bool> isWorldReady,
|
||||||
|
Action enterPlayerMode,
|
||||||
|
Func<bool>? isPlayerModeActive)
|
||||||
|
{
|
||||||
|
_isLiveInWorld = isLiveInWorld
|
||||||
|
?? throw new ArgumentNullException(nameof(isLiveInWorld));
|
||||||
|
_isPlayerEntityPresent = isPlayerEntityPresent
|
||||||
|
?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
|
||||||
|
_isPlayerControllerReady = isPlayerControllerReady
|
||||||
|
?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
|
||||||
|
_isWorldReady = isWorldReady
|
||||||
|
?? throw new ArgumentNullException(nameof(isWorldReady));
|
||||||
|
_enterPlayerMode = enterPlayerMode
|
||||||
|
?? throw new ArgumentNullException(nameof(enterPlayerMode));
|
||||||
|
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLiveInWorld => _isLiveInWorld();
|
||||||
|
public bool IsPlayerEntityPresent => _isPlayerEntityPresent();
|
||||||
|
public bool IsPlayerControllerReady => _isPlayerControllerReady();
|
||||||
|
public bool IsWorldReady => _isWorldReady();
|
||||||
|
public bool IsPlayerModeActive => _isPlayerModeActive();
|
||||||
|
public void EnterPlayerMode() => _enterPlayerMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IPlayerModeAutoEntryContext _context;
|
||||||
|
|
||||||
private bool _armed;
|
private bool _armed;
|
||||||
|
|
||||||
|
|
@ -72,20 +163,24 @@ public sealed class PlayerModeAutoEntry
|
||||||
/// player transition). Must construct the controller + chase
|
/// player transition). Must construct the controller + chase
|
||||||
/// camera and switch the active camera; the auto-entry doesn't
|
/// camera and switch the active camera; the auto-entry doesn't
|
||||||
/// reach inside.</param>
|
/// reach inside.</param>
|
||||||
|
internal PlayerModeAutoEntry(IPlayerModeAutoEntryContext context) =>
|
||||||
|
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||||
|
|
||||||
public PlayerModeAutoEntry(
|
public PlayerModeAutoEntry(
|
||||||
Func<bool> isLiveInWorld,
|
Func<bool> isLiveInWorld,
|
||||||
Func<bool> isPlayerEntityPresent,
|
Func<bool> isPlayerEntityPresent,
|
||||||
Func<bool> isPlayerControllerReady,
|
Func<bool> isPlayerControllerReady,
|
||||||
Func<bool> isWorldReady,
|
Func<bool> isWorldReady,
|
||||||
Action enterPlayerMode,
|
Action enterPlayerMode,
|
||||||
Func<bool>? isPlayerModeActive = null)
|
Func<bool>? isPlayerModeActive = null)
|
||||||
|
: this(new DelegateContext(
|
||||||
|
isLiveInWorld,
|
||||||
|
isPlayerEntityPresent,
|
||||||
|
isPlayerControllerReady,
|
||||||
|
isWorldReady,
|
||||||
|
enterPlayerMode,
|
||||||
|
isPlayerModeActive))
|
||||||
{
|
{
|
||||||
_isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld));
|
|
||||||
_isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
|
|
||||||
_isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
|
|
||||||
_isWorldReady = isWorldReady ?? throw new ArgumentNullException(nameof(isWorldReady));
|
|
||||||
_enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode));
|
|
||||||
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>True iff <see cref="TryEnter"/> would still fire if the
|
/// <summary>True iff <see cref="TryEnter"/> would still fire if the
|
||||||
|
|
@ -115,18 +210,18 @@ public sealed class PlayerModeAutoEntry
|
||||||
public bool TryEnter()
|
public bool TryEnter()
|
||||||
{
|
{
|
||||||
if (!_armed) return false;
|
if (!_armed) return false;
|
||||||
if (_isPlayerModeActive())
|
if (_context.IsPlayerModeActive)
|
||||||
{
|
{
|
||||||
_armed = false;
|
_armed = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!_isLiveInWorld()) return false;
|
if (!_context.IsLiveInWorld) return false;
|
||||||
if (!_isPlayerEntityPresent()) return false;
|
if (!_context.IsPlayerEntityPresent) return false;
|
||||||
if (!_isPlayerControllerReady()) return false;
|
if (!_context.IsPlayerControllerReady) return false;
|
||||||
if (!_isWorldReady()) return false;
|
if (!_context.IsWorldReady) return false;
|
||||||
|
|
||||||
_armed = false;
|
_armed = false;
|
||||||
_enterPlayerMode();
|
_context.EnterPlayerMode();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.Combat;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Interaction;
|
using AcDream.App.Interaction;
|
||||||
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
|
@ -50,18 +51,18 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
private readonly LiveWorldOriginState _origin;
|
private readonly LiveWorldOriginState _origin;
|
||||||
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
|
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
|
||||||
_localPlayerTeleport;
|
_localPlayerTeleport;
|
||||||
private readonly Func<PlayerMovementController?> _playerControllerSource;
|
private readonly ILocalPlayerControllerSource _playerControllerSource;
|
||||||
private readonly LocalPlayerOutboundController _localPlayerOutbound;
|
private readonly LocalPlayerOutboundController _localPlayerOutbound;
|
||||||
private readonly Func<EntityPhysicsHost?> _playerHostSource;
|
private readonly ILocalPlayerPhysicsHostSource _playerHostSource;
|
||||||
private readonly Func<uint> _playerGuid;
|
private readonly ILocalPlayerIdentitySource _playerIdentity;
|
||||||
private readonly IPhysicsScriptTimeSource _gameTime;
|
private readonly IPhysicsScriptTimeSource _gameTime;
|
||||||
private readonly Func<WorldSession?> _session;
|
private readonly ILiveWorldSessionSource _session;
|
||||||
private readonly LiveEntityInboundAuthorityGate _authorityGate;
|
private readonly LiveEntityInboundAuthorityGate _authorityGate;
|
||||||
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
|
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
|
||||||
|
|
||||||
private PlayerMovementController? _playerController => _playerControllerSource();
|
private PlayerMovementController? _playerController => _playerControllerSource.Controller;
|
||||||
private EntityPhysicsHost? _playerHost => _playerHostSource();
|
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
|
||||||
private uint _playerServerGuid => _playerGuid();
|
private uint _playerServerGuid => _playerIdentity.ServerGuid;
|
||||||
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
|
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
|
||||||
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
||||||
_liveEntities.MaterializedWorldEntities;
|
_liveEntities.MaterializedWorldEntities;
|
||||||
|
|
@ -96,12 +97,12 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
CombatTargetController? combatTargetController,
|
CombatTargetController? combatTargetController,
|
||||||
LiveWorldOriginState origin,
|
LiveWorldOriginState origin,
|
||||||
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
|
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
|
||||||
Func<PlayerMovementController?> playerControllerSource,
|
ILocalPlayerControllerSource playerControllerSource,
|
||||||
LocalPlayerOutboundController localPlayerOutbound,
|
LocalPlayerOutboundController localPlayerOutbound,
|
||||||
Func<EntityPhysicsHost?> playerHostSource,
|
ILocalPlayerPhysicsHostSource playerHostSource,
|
||||||
Func<uint> playerGuid,
|
ILocalPlayerIdentitySource playerIdentity,
|
||||||
IPhysicsScriptTimeSource gameTime,
|
IPhysicsScriptTimeSource gameTime,
|
||||||
Func<WorldSession?> session,
|
ILiveWorldSessionSource session,
|
||||||
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
||||||
IMovementTruthDiagnosticSink movementTruthDiagnostics)
|
IMovementTruthDiagnosticSink movementTruthDiagnostics)
|
||||||
{
|
{
|
||||||
|
|
@ -131,7 +132,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
_localPlayerOutbound = localPlayerOutbound
|
_localPlayerOutbound = localPlayerOutbound
|
||||||
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
|
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
|
||||||
_playerHostSource = playerHostSource ?? throw new ArgumentNullException(nameof(playerHostSource));
|
_playerHostSource = playerHostSource ?? throw new ArgumentNullException(nameof(playerHostSource));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_playerIdentity = playerIdentity ?? throw new ArgumentNullException(nameof(playerIdentity));
|
||||||
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
|
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
|
||||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
_authorityGate = new LiveEntityInboundAuthorityGate(
|
_authorityGate = new LiveEntityInboundAuthorityGate(
|
||||||
|
|
@ -1029,7 +1030,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
p.PositionY,
|
p.PositionY,
|
||||||
p.PositionZ)),
|
p.PositionZ)),
|
||||||
() => _localPlayerOutbound.SendImmediatePosition(
|
() => _localPlayerOutbound.SendImmediatePosition(
|
||||||
_session(),
|
_session.CurrentSession,
|
||||||
_playerController)))
|
_playerController)))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
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.StreamingController? _streamingController;
|
||||||
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
|
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
|
||||||
_streamingOriginRecenter;
|
_streamingOriginRecenter;
|
||||||
private AcDream.App.Update.IStreamingFramePhase _streamingFrame = null!;
|
|
||||||
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
||||||
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
|
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
|
||||||
_localPlayerTeleportSink = new();
|
_localPlayerTeleportSink = new();
|
||||||
|
|
@ -173,10 +172,7 @@ public sealed class GameWindow : IDisposable
|
||||||
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
||||||
_movementTruthDiagnostics;
|
_movementTruthDiagnostics;
|
||||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||||
private AcDream.App.Update.ICameraFramePhase _cameraFrame = null!;
|
private AcDream.App.Update.UpdateFrameOrchestrator _updateFrameOrchestrator = null!;
|
||||||
private AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator = null!;
|
|
||||||
private AcDream.App.Update.LiveSpatialPresentationReconciler
|
|
||||||
_liveSpatialReconciler = null!;
|
|
||||||
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
||||||
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||||
// Step 7 projectile presentation. The controller owns no identity map;
|
// Step 7 projectile presentation. The controller owns no identity map;
|
||||||
|
|
@ -186,6 +182,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_liveEntityProjectionWithdrawal;
|
_liveEntityProjectionWithdrawal;
|
||||||
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
|
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
|
||||||
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
|
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
|
||||||
|
private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!;
|
||||||
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
|
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
|
||||||
_liveEntityMotionBindings = new();
|
_liveEntityMotionBindings = new();
|
||||||
|
|
||||||
|
|
@ -2170,36 +2167,32 @@ public sealed class GameWindow : IDisposable
|
||||||
_entityEffects = entityEffects;
|
_entityEffects = entityEffects;
|
||||||
entityEffects.DiagnosticSink = message =>
|
entityEffects.DiagnosticSink = message =>
|
||||||
Console.Error.WriteLine($"vfx: {message}");
|
Console.Error.WriteLine($"vfx: {message}");
|
||||||
|
var partArrayLifecycle =
|
||||||
|
new AcDream.App.Rendering.LiveEntityPartArrayLifecycle(
|
||||||
|
_animatedEntities);
|
||||||
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
|
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
_physicsEngine.ShadowObjects,
|
_physicsEngine.ShadowObjects,
|
||||||
entityEffects.PlayTypedFromHiddenTransition,
|
entityEffects.PlayTypedFromHiddenTransition,
|
||||||
_equippedChildRenderer.SetDirectChildrenNoDraw,
|
_equippedChildRenderer.SetDirectChildrenNoDraw,
|
||||||
_liveEntityMotionBindings.ClearTargetForHiddenEntity,
|
_liveEntityMotionBindings.ClearTargetForHiddenEntity,
|
||||||
() => (_liveCenterX, _liveCenterY),
|
_liveWorldOrigin.GetCenter,
|
||||||
handlePartArrayEnterWorld: localEntityId =>
|
handlePartArrayEnterWorld: partArrayLifecycle.HandleEnterWorld);
|
||||||
{
|
var remoteShadowPlacement =
|
||||||
if (_animatedEntities.TryGetValue(localEntityId, out var animated))
|
new AcDream.App.Physics.RemoteShadowPlacementSynchronizer(
|
||||||
animated.Sequencer?.Manager.HandleEnterWorld();
|
_remotePhysicsUpdater,
|
||||||
});
|
_liveWorldOrigin);
|
||||||
|
var remoteTeleportPresentation =
|
||||||
|
new AcDream.App.Physics.RemoteTeleportPlacementPresentation(
|
||||||
|
_liveEntityPresentation);
|
||||||
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
|
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
_liveEntityMotionBindings.GetSetupCylinder,
|
_liveEntityMotionBindings.GetSetupCylinder,
|
||||||
CellLocalForSeed,
|
_liveWorldOrigin.CellLocalForSeed,
|
||||||
(entity, remote, cellId) => _remotePhysicsUpdater.SyncRemoteShadowToBody(
|
remoteShadowPlacement.Sync,
|
||||||
entity.Id,
|
remoteTeleportPresentation.Complete,
|
||||||
remote,
|
remoteTeleportPresentation.Begin);
|
||||||
_liveCenterX,
|
|
||||||
_liveCenterY,
|
|
||||||
cellId),
|
|
||||||
(guid, generation, deferShadowRestore) =>
|
|
||||||
_liveEntityPresentation.CompleteAuthoritativePlacement(
|
|
||||||
guid,
|
|
||||||
generation,
|
|
||||||
deferShadowRestore),
|
|
||||||
(guid, generation) =>
|
|
||||||
_liveEntityPresentation.BeginAuthoritativePlacement(guid, generation));
|
|
||||||
_equippedChildRenderer.ProjectionPoseReady += guid =>
|
_equippedChildRenderer.ProjectionPoseReady += guid =>
|
||||||
_liveEntityLights.OnAttachedPoseReady(guid);
|
_liveEntityLights.OnAttachedPoseReady(guid);
|
||||||
_hookRouter.Register(entityEffects);
|
_hookRouter.Register(entityEffects);
|
||||||
|
|
@ -2484,9 +2477,14 @@ public sealed class GameWindow : IDisposable
|
||||||
_streamingController,
|
_streamingController,
|
||||||
_worldState,
|
_worldState,
|
||||||
_worldReveal,
|
_worldReveal,
|
||||||
() => _playerServerGuid,
|
_localPlayerIdentity,
|
||||||
sealedDungeonCells,
|
sealedDungeonCells,
|
||||||
Console.WriteLine);
|
Console.WriteLine);
|
||||||
|
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
||||||
|
var localPhysicsTimestamps =
|
||||||
|
new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
|
||||||
|
_localPlayerIdentity,
|
||||||
|
_liveSessionController);
|
||||||
var liveEntityTeardown =
|
var liveEntityTeardown =
|
||||||
new AcDream.App.World.LiveEntityRuntimeTeardownController(
|
new AcDream.App.World.LiveEntityRuntimeTeardownController(
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
|
|
@ -2503,8 +2501,15 @@ public sealed class GameWindow : IDisposable
|
||||||
_physicsEngine.ShadowObjects,
|
_physicsEngine.ShadowObjects,
|
||||||
_liveEntityLights!,
|
_liveEntityLights!,
|
||||||
_classificationCache,
|
_classificationCache,
|
||||||
() => _playerServerGuid);
|
_localPlayerIdentity);
|
||||||
liveEntityComponentLifecycle.Bind(liveEntityTeardown);
|
liveEntityComponentLifecycle.Bind(liveEntityTeardown);
|
||||||
|
var liveEntityDeletion =
|
||||||
|
new AcDream.App.World.LiveEntityDeletionController(
|
||||||
|
_liveEntities,
|
||||||
|
Objects,
|
||||||
|
liveEntityTeardown,
|
||||||
|
_localPlayerIdentity,
|
||||||
|
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||||
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
|
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
Objects,
|
Objects,
|
||||||
|
|
@ -2518,9 +2523,9 @@ public sealed class GameWindow : IDisposable
|
||||||
_liveEntityPresentation!),
|
_liveEntityPresentation!),
|
||||||
originCoordinator,
|
originCoordinator,
|
||||||
networkUpdateBridge,
|
networkUpdateBridge,
|
||||||
liveEntityTeardown,
|
localPhysicsTimestamps,
|
||||||
PublishLocalPhysicsTimestamps,
|
_localPlayerIdentity,
|
||||||
() => _playerServerGuid,
|
liveEntityDeletion,
|
||||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||||
_liveEntityNetworkUpdates =
|
_liveEntityNetworkUpdates =
|
||||||
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
|
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
|
||||||
|
|
@ -2546,18 +2551,24 @@ public sealed class GameWindow : IDisposable
|
||||||
_combatTargetController,
|
_combatTargetController,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
_localPlayerTeleportSink,
|
_localPlayerTeleportSink,
|
||||||
() => _playerController,
|
_playerControllerSlot,
|
||||||
_localPlayerOutbound,
|
_localPlayerOutbound,
|
||||||
() => _playerHost,
|
_playerHostSlot,
|
||||||
() => _playerServerGuid,
|
_localPlayerIdentity,
|
||||||
_updateFrameClock,
|
_updateFrameClock,
|
||||||
() => LiveSession,
|
_liveSessionController,
|
||||||
PublishLocalPhysicsTimestamps,
|
localPhysicsTimestamps.Publish,
|
||||||
_movementTruthDiagnostics);
|
_movementTruthDiagnostics);
|
||||||
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
() => _playerServerGuid,
|
_localPlayerIdentity,
|
||||||
candidate => _liveEntityHydration.OnPrune(candidate));
|
liveEntityDeletion);
|
||||||
|
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
|
||||||
|
_inboundEntityEvents,
|
||||||
|
_liveEntityHydration,
|
||||||
|
_liveEntityNetworkUpdates,
|
||||||
|
_localPlayerTeleportSink,
|
||||||
|
_entityEffects!);
|
||||||
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
|
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
|
||||||
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
|
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
|
||||||
_equippedChildRenderer.EntityReady += candidate =>
|
_equippedChildRenderer.EntityReady += candidate =>
|
||||||
|
|
@ -2573,7 +2584,6 @@ public sealed class GameWindow : IDisposable
|
||||||
// CreateObject messages into _worldGameState as they arrive. Entirely
|
// CreateObject messages into _worldGameState as they arrive. Entirely
|
||||||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
||||||
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
||||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
|
||||||
_combatAttackOperations.Bind(
|
_combatAttackOperations.Bind(
|
||||||
new AcDream.App.Combat.LiveCombatAttackOperations(
|
new AcDream.App.Combat.LiveCombatAttackOperations(
|
||||||
Combat,
|
Combat,
|
||||||
|
|
@ -2609,7 +2619,7 @@ public sealed class GameWindow : IDisposable
|
||||||
mouseLookController,
|
mouseLookController,
|
||||||
new AcDream.App.Input.CombatAttackInputFrameAdapter(
|
new AcDream.App.Input.CombatAttackInputFrameAdapter(
|
||||||
_combatAttackController!));
|
_combatAttackController!));
|
||||||
_streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
|
var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
|
||||||
_options.LiveMode,
|
_options.LiveMode,
|
||||||
_localPlayerMode,
|
_localPlayerMode,
|
||||||
_playerControllerSlot,
|
_playerControllerSlot,
|
||||||
|
|
@ -2638,7 +2648,7 @@ public sealed class GameWindow : IDisposable
|
||||||
new AcDream.App.Update.SettingsParticleRangeSource(
|
new AcDream.App.Update.SettingsParticleRangeSource(
|
||||||
_settingsVm,
|
_settingsVm,
|
||||||
_persistedDisplay.ParticleRange));
|
_persistedDisplay.ParticleRange));
|
||||||
_liveSpatialReconciler =
|
var liveSpatialReconciler =
|
||||||
new AcDream.App.Update.LiveSpatialPresentationReconciler(
|
new AcDream.App.Update.LiveSpatialPresentationReconciler(
|
||||||
_entityEffects!,
|
_entityEffects!,
|
||||||
_equippedChildRenderer!,
|
_equippedChildRenderer!,
|
||||||
|
|
@ -2721,16 +2731,15 @@ public sealed class GameWindow : IDisposable
|
||||||
_localPlayerSkills,
|
_localPlayerSkills,
|
||||||
_viewportAspect);
|
_viewportAspect);
|
||||||
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
||||||
isLiveInWorld: () => _liveSessionController.IsInWorld,
|
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
|
||||||
isPlayerEntityPresent: () =>
|
_liveSessionController,
|
||||||
_liveEntities.MaterializedWorldEntities.ContainsKey(
|
_liveEntities,
|
||||||
_playerServerGuid),
|
_localPlayerIdentity,
|
||||||
isPlayerControllerReady: () => true,
|
_worldReveal
|
||||||
// Retail SmartBox::UseTime @ 0x00455410 completes player
|
?? throw new InvalidOperationException(
|
||||||
// position only after the destination cell load clears.
|
"World reveal was not composed before player auto-entry."),
|
||||||
isWorldReady: LoginWorldReady,
|
_localPlayerMode,
|
||||||
enterPlayerMode: _playerModeController.EnterFromAutoEntry,
|
_playerModeController));
|
||||||
isPlayerModeActive: () => _localPlayerMode.IsPlayerMode);
|
|
||||||
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
|
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
|
||||||
var localTeleportPresentation =
|
var localTeleportPresentation =
|
||||||
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
|
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
|
||||||
|
|
@ -2758,7 +2767,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_playerHostSlot,
|
_playerHostSlot,
|
||||||
_chaseCameraInput,
|
_chaseCameraInput,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
_liveSpatialReconciler),
|
liveSpatialReconciler),
|
||||||
new AcDream.App.Streaming.LocalPlayerTeleportSession(
|
new AcDream.App.Streaming.LocalPlayerTeleportSession(
|
||||||
_liveSessionController),
|
_liveSessionController),
|
||||||
localTeleportPresentation);
|
localTeleportPresentation);
|
||||||
|
|
@ -2767,25 +2776,41 @@ public sealed class GameWindow : IDisposable
|
||||||
// remains only as the staged-startup fallback used by shutdown when
|
// remains only as the staged-startup fallback used by shutdown when
|
||||||
// composition fails before the transfer.
|
// composition fails before the transfer.
|
||||||
_portalTunnel = null;
|
_portalTunnel = null;
|
||||||
_liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||||
liveObjectFrame,
|
liveObjectFrame,
|
||||||
_worldState,
|
_worldState,
|
||||||
_liveSessionController,
|
_liveSessionController,
|
||||||
localPlayerFrame,
|
localPlayerFrame,
|
||||||
_liveSpatialReconciler);
|
liveSpatialReconciler);
|
||||||
_cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
||||||
_cameraController!,
|
_cameraController!,
|
||||||
_inputCapture,
|
_inputCapture,
|
||||||
_cameraInput,
|
_cameraInput,
|
||||||
localPlayerFrameRuntime,
|
localPlayerFrameRuntime,
|
||||||
_chaseCameraInput,
|
_chaseCameraInput,
|
||||||
localPlayerFrame,
|
localPlayerFrame,
|
||||||
_liveSpatialReconciler,
|
liveSpatialReconciler,
|
||||||
new AcDream.App.Combat.CombatCameraTargetSource(
|
new AcDream.App.Combat.CombatCameraTargetSource(
|
||||||
_gameplaySettings,
|
_gameplaySettings,
|
||||||
Combat,
|
Combat,
|
||||||
_selection,
|
_selection,
|
||||||
_worldSelectionQuery!));
|
_worldSelectionQuery!));
|
||||||
|
_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
||||||
|
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
||||||
|
_liveEntities),
|
||||||
|
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
||||||
|
_updateFrameClock,
|
||||||
|
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
||||||
|
streamingFrame,
|
||||||
|
_gameplayInputFrame,
|
||||||
|
liveFrameCoordinator,
|
||||||
|
new AcDream.App.Update.LiveEntityLivenessFramePhase(
|
||||||
|
_liveEntityLiveness,
|
||||||
|
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
|
||||||
|
_localPlayerTeleport,
|
||||||
|
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
||||||
|
_playerModeAutoEntry),
|
||||||
|
cameraFrame);
|
||||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||||
_liveSessionController.Start(
|
_liveSessionController.Start(
|
||||||
_options,
|
_options,
|
||||||
|
|
@ -2958,7 +2983,7 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
events = new AcDream.App.Net.LiveSessionEventRouter(
|
events = new AcDream.App.Net.LiveSessionEventRouter(
|
||||||
session,
|
session,
|
||||||
CreateLiveEntitySessionSink(),
|
_liveEntitySessionEvents.CreateSink(),
|
||||||
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
||||||
OnEnvironChanged,
|
OnEnvironChanged,
|
||||||
ticks =>
|
ticks =>
|
||||||
|
|
@ -2987,44 +3012,6 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AcDream.App.Net.LiveEntitySessionSink CreateLiveEntitySessionSink() => new(
|
|
||||||
Spawned: spawn => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityHydration!,
|
|
||||||
spawn,
|
|
||||||
static (hydration, value) => hydration.OnCreate(value)),
|
|
||||||
Deleted: deletion => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityHydration!, deletion,
|
|
||||||
static (hydration, value) => hydration.OnDelete(value)),
|
|
||||||
PickedUp: pickup => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityHydration!, pickup,
|
|
||||||
static (hydration, value) => hydration.OnPickup(value)),
|
|
||||||
MotionUpdated: motion => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityNetworkUpdates!, motion,
|
|
||||||
static (updates, value) => updates.OnMotion(value)),
|
|
||||||
PositionUpdated: position => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityNetworkUpdates!, position,
|
|
||||||
static (updates, value) => updates.OnPosition(value)),
|
|
||||||
VectorUpdated: vector => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityNetworkUpdates!, vector,
|
|
||||||
static (updates, value) => updates.OnVector(value)),
|
|
||||||
StateUpdated: state => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityNetworkUpdates!, state,
|
|
||||||
static (updates, value) => updates.OnState(value)),
|
|
||||||
ParentUpdated: parent => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityHydration!, parent,
|
|
||||||
static (hydration, value) => hydration.OnParent(value)),
|
|
||||||
TeleportStarted: teleport => _inboundEntityEvents.Run(
|
|
||||||
_localPlayerTeleportSink,
|
|
||||||
teleport,
|
|
||||||
static (sink, value) => sink.OnTeleportStarted(value)),
|
|
||||||
AppearanceUpdated: appearance => _inboundEntityEvents.Run(
|
|
||||||
_liveEntityHydration!, appearance,
|
|
||||||
static (hydration, value) => hydration.OnAppearance(value)),
|
|
||||||
PlayPhysicsScript: script => _inboundEntityEvents.Run(
|
|
||||||
this, script, static (window, value) => window.OnPlayScriptReceived(value)),
|
|
||||||
PlayPhysicsScriptType: message => _inboundEntityEvents.Run(
|
|
||||||
this, message, static (window, value) => window._entityEffects?.HandleTyped(value)));
|
|
||||||
|
|
||||||
private AcDream.App.Net.LiveInventorySessionBindings
|
private AcDream.App.Net.LiveInventorySessionBindings
|
||||||
CreateLiveInventorySessionBindings() => new(
|
CreateLiveInventorySessionBindings() => new(
|
||||||
Objects,
|
Objects,
|
||||||
|
|
@ -3276,47 +3263,6 @@ public sealed class GameWindow : IDisposable
|
||||||
doll.MeshRefs = reposed;
|
doll.MeshRefs = reposed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishLocalPhysicsTimestamps(
|
|
||||||
uint guid,
|
|
||||||
AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
|
|
||||||
{
|
|
||||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
|
||||||
if (guid != _playerServerGuid || session is null) return;
|
|
||||||
session.PublishAcceptedLocalPhysicsTimestamps(
|
|
||||||
timestamps.Instance,
|
|
||||||
timestamps.ServerControlledMove,
|
|
||||||
timestamps.Teleport,
|
|
||||||
timestamps.ForcePosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
|
||||||
// body's cell-relative CellPosition. This is the ONE place the streaming center
|
|
||||||
// (_liveCenter) is allowed to touch the physics frame — at the placement seam,
|
|
||||||
// converting the render-frame world position into the wire's (cell, local). After
|
|
||||||
// seeding, physics carries (cell, local) forward without ever reading _liveCenter.
|
|
||||||
private System.Numerics.Vector3 CellLocalForSeed(System.Numerics.Vector3 worldPos, uint cellId)
|
|
||||||
{
|
|
||||||
int lbX = (int)((cellId >> 24) & 0xFFu);
|
|
||||||
int lbY = (int)((cellId >> 16) & 0xFFu);
|
|
||||||
var origin = new System.Numerics.Vector3(
|
|
||||||
(lbX - _liveCenterX) * 192f,
|
|
||||||
(lbY - _liveCenterY) * 192f,
|
|
||||||
0f);
|
|
||||||
return worldPos - origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool LoginWorldReady()
|
|
||||||
{
|
|
||||||
return TryGetLoginWorldCell(out uint cell)
|
|
||||||
&& EvaluateWorldRevealReadiness(cell).IsReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AcDream.App.Streaming.WorldRevealReadinessSnapshot EvaluateWorldRevealReadiness(
|
|
||||||
uint destinationCell)
|
|
||||||
{
|
|
||||||
return _worldReveal?.Evaluate(destinationCell) ?? default;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetLoginWorldCell(out uint cell)
|
private bool TryGetLoginWorldCell(out uint cell)
|
||||||
{
|
{
|
||||||
cell = 0;
|
cell = 0;
|
||||||
|
|
@ -3329,20 +3275,6 @@ public sealed class GameWindow : IDisposable
|
||||||
return cell != 0;
|
return cell != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Server-sent direct PhysicsScript (F754). EntityEffectController owns
|
|
||||||
/// GUID translation and pre-materialization queueing.
|
|
||||||
///
|
|
||||||
/// <para>
|
|
||||||
/// F754 remains a direct PhysicsScript DID. It is never resolved through a
|
|
||||||
/// typed PhysicsScriptTable.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
private void OnPlayScriptReceived(AcDream.Core.Net.Messages.PlayPhysicsScript message)
|
|
||||||
{
|
|
||||||
_entityEffects?.HandleDirect(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateSkyPes(
|
private void UpdateSkyPes(
|
||||||
float dayFraction,
|
float dayFraction,
|
||||||
AcDream.Core.World.DayGroupData? dayGroup,
|
AcDream.Core.World.DayGroupData? dayGroup,
|
||||||
|
|
@ -3512,76 +3444,10 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
private void OnUpdate(double dt)
|
private void OnUpdate(double dt)
|
||||||
{
|
{
|
||||||
using var _updStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Update);
|
using var _updStage = _frameProfiler.BeginStage(
|
||||||
|
AcDream.App.Diagnostics.FrameStage.Update);
|
||||||
// A resource callback can request deletion while the presentation
|
_updateFrameOrchestrator.Tick(
|
||||||
// owner is inside a synchronous resume/suspend transition. The first
|
|
||||||
// unregister leaves a canonical teardown tombstone; drain it only now,
|
|
||||||
// after that callback stack has unwound on the same update thread.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_liveEntityHydration?.RetryPendingTeardowns();
|
|
||||||
}
|
|
||||||
catch (AggregateException error)
|
|
||||||
{
|
|
||||||
// The tombstone retains every unfinished owner step. Report this
|
|
||||||
// attempt, but keep the frame advancing so the next update can
|
|
||||||
// retry after transient renderer/plugin teardown failures.
|
|
||||||
Console.Error.WriteLine($"[live-entity-teardown] {error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
AcDream.App.Update.UpdateFrameTiming frameTiming = _updateFrameClock.Advance(
|
|
||||||
new AcDream.App.Update.UpdateFrameInput(dt));
|
new AcDream.App.Update.UpdateFrameInput(dt));
|
||||||
float frameDelta = frameTiming.SimulationDeltaSecondsSingle;
|
|
||||||
|
|
||||||
// Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
|
|
||||||
// Timer::cur_time at the packet/default-script call site. Publish this
|
|
||||||
// update frame's clock before streaming and network dispatch, then
|
|
||||||
// reuse the same timestamp for animation hooks and the later drain.
|
|
||||||
_scriptRunner?.PublishTime(frameTiming.ScriptTime);
|
|
||||||
|
|
||||||
// Streaming publishes before inbound CreateObject dispatch so newly
|
|
||||||
// accepted live projections can enter a resident bucket this frame.
|
|
||||||
_streamingFrame.Tick();
|
|
||||||
|
|
||||||
// Input callbacks feed the current object tick. Packets already read
|
|
||||||
// by Core.Net remain queued until the retail object/physics phase has
|
|
||||||
// consumed that input and published its final pose.
|
|
||||||
_gameplayInputFrame!.Tick(frameTiming);
|
|
||||||
|
|
||||||
// Drain pending live-session traffic AFTER streaming so any incoming
|
|
||||||
// CreateObject events find their landblock already loaded in
|
|
||||||
// GpuWorldState. Non-blocking — returns immediately if no datagrams
|
|
||||||
// are in the kernel buffer. Fires EntitySpawned events synchronously.
|
|
||||||
// Step 2: routed through the controller; functionally identical.
|
|
||||||
// Retail SmartBox::UseTime (0x00455410) advances CObjectMaint and
|
|
||||||
// CPhysics before it drains the inbound event queue. Keep the complete
|
|
||||||
// live-object phase on that side of the barrier too. In particular,
|
|
||||||
// the recall action retires before ACE's Hidden SetState freezes its
|
|
||||||
// PartArray at the teleport boundary.
|
|
||||||
_liveFrameCoordinator.Tick(frameDelta);
|
|
||||||
_liveEntityLiveness?.Tick(ClientTimerNow());
|
|
||||||
|
|
||||||
// Retail teleport transit runs after streaming and inbound state so a
|
|
||||||
// newly resident destination can materialize in this same frame. The
|
|
||||||
// focused owner also converges an F751 that arrived before the local
|
|
||||||
// player projection/controller became available.
|
|
||||||
_localPlayerTeleport!.Tick(frameDelta);
|
|
||||||
// Phase K.1a — tick the input dispatcher so Hold-type bindings
|
|
||||||
// re-fire while their chord is held. K.1b adds the subscribers
|
|
||||||
// that actually consume the events.
|
|
||||||
// Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse
|
|
||||||
// on selection, then revert). No-op when nothing is flashing.
|
|
||||||
// Retained panel-local timers advance through RetailUiRuntime.Tick on the draw seam.
|
|
||||||
|
|
||||||
// Phase K.2 — auto-enter player mode at login. The guard
|
|
||||||
// returns true on the firing tick (one-shot); subsequent ticks
|
|
||||||
// are no-ops. Skipped offline (no active session → IsLiveInWorld
|
|
||||||
// predicate stays false). Cancelled by manual fly-toggle in
|
|
||||||
// OnInputAction (Ctrl+Tab) or DebugPanel.
|
|
||||||
_playerModeAutoEntry?.TryEnter();
|
|
||||||
|
|
||||||
_cameraFrame.Tick(frameTiming);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCameraModeChanged(bool _modeBool)
|
private void OnCameraModeChanged(bool _modeBool)
|
||||||
|
|
|
||||||
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.Rendering;
|
||||||
|
using AcDream.App.Input;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
@ -146,9 +147,9 @@ internal sealed class LiveEntityHydrationController
|
||||||
private readonly ILiveEntityReadyPublisher _ready;
|
private readonly ILiveEntityReadyPublisher _ready;
|
||||||
private readonly ILiveEntityWorldOriginCoordinator _origin;
|
private readonly ILiveEntityWorldOriginCoordinator _origin;
|
||||||
private readonly ILiveEntityNetworkUpdateSink _networkUpdates;
|
private readonly ILiveEntityNetworkUpdateSink _networkUpdates;
|
||||||
private readonly ILiveEntityTeardownCoordinator _teardown;
|
private readonly IAcceptedLocalPhysicsTimestampPublisher _timestamps;
|
||||||
private readonly Action<uint, AcceptedPhysicsTimestamps> _publishTimestamps;
|
private readonly ILocalPlayerIdentitySource _identity;
|
||||||
private readonly Func<uint> _playerGuid;
|
private readonly LiveEntityDeletionController _deletion;
|
||||||
private readonly Action<string>? _diagnostic;
|
private readonly Action<string>? _diagnostic;
|
||||||
|
|
||||||
public LiveEntityHydrationController(
|
public LiveEntityHydrationController(
|
||||||
|
|
@ -160,9 +161,9 @@ internal sealed class LiveEntityHydrationController
|
||||||
ILiveEntityReadyPublisher ready,
|
ILiveEntityReadyPublisher ready,
|
||||||
ILiveEntityWorldOriginCoordinator origin,
|
ILiveEntityWorldOriginCoordinator origin,
|
||||||
ILiveEntityNetworkUpdateSink networkUpdates,
|
ILiveEntityNetworkUpdateSink networkUpdates,
|
||||||
ILiveEntityTeardownCoordinator teardown,
|
IAcceptedLocalPhysicsTimestampPublisher timestamps,
|
||||||
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
ILocalPlayerIdentitySource identity,
|
||||||
Func<uint> playerGuid,
|
LiveEntityDeletionController deletion,
|
||||||
Action<string>? diagnostic = null)
|
Action<string>? diagnostic = null)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
|
@ -173,10 +174,9 @@ internal sealed class LiveEntityHydrationController
|
||||||
_ready = ready ?? throw new ArgumentNullException(nameof(ready));
|
_ready = ready ?? throw new ArgumentNullException(nameof(ready));
|
||||||
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||||
_networkUpdates = networkUpdates ?? throw new ArgumentNullException(nameof(networkUpdates));
|
_networkUpdates = networkUpdates ?? throw new ArgumentNullException(nameof(networkUpdates));
|
||||||
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
|
_timestamps = timestamps ?? throw new ArgumentNullException(nameof(timestamps));
|
||||||
_publishTimestamps = publishTimestamps
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||||
?? throw new ArgumentNullException(nameof(publishTimestamps));
|
_deletion = deletion ?? throw new ArgumentNullException(nameof(deletion));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
|
||||||
_diagnostic = diagnostic;
|
_diagnostic = diagnostic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,7 +207,7 @@ internal sealed class LiveEntityHydrationController
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_publishTimestamps(spawn.Guid, result.Timestamps);
|
_timestamps.Publish(spawn.Guid, result.Timestamps);
|
||||||
if (_runtime.IsCurrentCreateIntegration(
|
if (_runtime.IsCurrentCreateIntegration(
|
||||||
record,
|
record,
|
||||||
createIntegrationVersion)
|
createIntegrationVersion)
|
||||||
|
|
@ -353,23 +353,7 @@ internal sealed class LiveEntityHydrationController
|
||||||
// SmartBox::HandleDeleteObject rejects the player before resolving an
|
// SmartBox::HandleDeleteObject rejects the player before resolving an
|
||||||
// object or touching any queued owner state. A pre-Create player
|
// object or touching any queued owner state. A pre-Create player
|
||||||
// Delete must therefore be side-effect-free too.
|
// Delete must therefore be side-effect-free too.
|
||||||
if (delete.Guid == _playerGuid())
|
return _deletion.Delete(delete);
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_runtime.TryGetRecord(delete.Guid, out _))
|
|
||||||
_teardown.ForgetUnknownOwner(delete.Guid);
|
|
||||||
|
|
||||||
bool removed = _runtime.UnregisterLiveEntity(
|
|
||||||
delete,
|
|
||||||
isLocalPlayer: false,
|
|
||||||
beforeTeardown: () =>
|
|
||||||
ObjectTableWiring.ApplyEntityDelete(_objects, delete));
|
|
||||||
if (removed)
|
|
||||||
{
|
|
||||||
_diagnostic?.Invoke(
|
|
||||||
$"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -377,9 +361,7 @@ internal sealed class LiveEntityHydrationController
|
||||||
/// generation gate and teardown transaction as an authoritative F747.
|
/// generation gate and teardown transaction as an authoritative F747.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OnPrune(LiveEntityPruneCandidate candidate) =>
|
public bool OnPrune(LiveEntityPruneCandidate candidate) =>
|
||||||
OnDelete(new DeleteObject.Parsed(
|
_deletion.Prune(candidate);
|
||||||
candidate.ServerGuid,
|
|
||||||
candidate.Generation));
|
|
||||||
|
|
||||||
public int RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
|
public int RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
|
||||||
|
|
||||||
|
|
@ -415,7 +397,7 @@ internal sealed class LiveEntityHydrationController
|
||||||
|
|
||||||
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
LiveEntityRecord[] records = _runtime.Records
|
LiveEntityRecord[] records = _runtime.Records
|
||||||
.Where(record => (record.ServerGuid != _playerGuid()
|
.Where(record => (record.ServerGuid != _identity.ServerGuid
|
||||||
|| !record.InitialHydrationCompleted
|
|| !record.InitialHydrationCompleted
|
||||||
|| record.CreateProjectionSynchronizationPending
|
|| record.CreateProjectionSynchronizationPending
|
||||||
|| record.AppearanceProjectionSynchronizationPending)
|
|| record.AppearanceProjectionSynchronizationPending)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
|
|
@ -113,7 +114,17 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
||||||
private readonly StreamingController _streaming;
|
private readonly StreamingController _streaming;
|
||||||
private readonly GpuWorldState _worldState;
|
private readonly GpuWorldState _worldState;
|
||||||
private readonly WorldRevealCoordinator _worldReveal;
|
private readonly WorldRevealCoordinator _worldReveal;
|
||||||
private readonly Func<uint> _playerGuid;
|
private sealed class DelegateIdentitySource : ILocalPlayerIdentitySource
|
||||||
|
{
|
||||||
|
private readonly Func<uint> _read;
|
||||||
|
|
||||||
|
public DelegateIdentitySource(Func<uint> read) =>
|
||||||
|
_read = read ?? throw new ArgumentNullException(nameof(read));
|
||||||
|
|
||||||
|
public uint ServerGuid => _read();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ILocalPlayerIdentitySource _identity;
|
||||||
private readonly ISealedDungeonCellClassifier _sealedDungeonCells;
|
private readonly ISealedDungeonCellClassifier _sealedDungeonCells;
|
||||||
private readonly Action<string>? _diagnostic;
|
private readonly Action<string>? _diagnostic;
|
||||||
|
|
||||||
|
|
@ -122,7 +133,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
||||||
StreamingController streaming,
|
StreamingController streaming,
|
||||||
GpuWorldState worldState,
|
GpuWorldState worldState,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
Func<uint> playerGuid,
|
ILocalPlayerIdentitySource identity,
|
||||||
ISealedDungeonCellClassifier sealedDungeonCells,
|
ISealedDungeonCellClassifier sealedDungeonCells,
|
||||||
Action<string>? diagnostic = null)
|
Action<string>? diagnostic = null)
|
||||||
{
|
{
|
||||||
|
|
@ -130,18 +141,37 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
||||||
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
||||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||||
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||||
_sealedDungeonCells = sealedDungeonCells
|
_sealedDungeonCells = sealedDungeonCells
|
||||||
?? throw new ArgumentNullException(nameof(sealedDungeonCells));
|
?? throw new ArgumentNullException(nameof(sealedDungeonCells));
|
||||||
_diagnostic = diagnostic;
|
_diagnostic = diagnostic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal LiveEntityWorldOriginCoordinator(
|
||||||
|
LiveWorldOriginState origin,
|
||||||
|
StreamingController streaming,
|
||||||
|
GpuWorldState worldState,
|
||||||
|
WorldRevealCoordinator worldReveal,
|
||||||
|
Func<uint> playerGuid,
|
||||||
|
ISealedDungeonCellClassifier sealedDungeonCells,
|
||||||
|
Action<string>? diagnostic = null)
|
||||||
|
: this(
|
||||||
|
origin,
|
||||||
|
streaming,
|
||||||
|
worldState,
|
||||||
|
worldReveal,
|
||||||
|
new DelegateIdentitySource(playerGuid),
|
||||||
|
sealedDungeonCells,
|
||||||
|
diagnostic)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsKnown => _origin.IsKnown;
|
public bool IsKnown => _origin.IsKnown;
|
||||||
|
|
||||||
public LiveEntityOriginInitialization TryInitialize(
|
public LiveEntityOriginInitialization TryInitialize(
|
||||||
WorldSession.EntitySpawn spawn)
|
WorldSession.EntitySpawn spawn)
|
||||||
{
|
{
|
||||||
if (spawn.Guid != _playerGuid()
|
if (spawn.Guid != _identity.ServerGuid
|
||||||
|| spawn.Position is not { } position)
|
|| spawn.Position is not { } position)
|
||||||
{
|
{
|
||||||
return new(_origin.IsKnown, Array.Empty<uint>());
|
return new(_origin.IsKnown, Array.Empty<uint>());
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.App.Input;
|
||||||
|
|
||||||
namespace AcDream.App.World;
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
|
@ -86,19 +87,19 @@ internal sealed class LiveEntityLivenessController
|
||||||
private const double MaintenanceIntervalSeconds = 1.0;
|
private const double MaintenanceIntervalSeconds = 1.0;
|
||||||
|
|
||||||
private readonly LiveEntityRuntime _runtime;
|
private readonly LiveEntityRuntime _runtime;
|
||||||
private readonly Func<uint> _playerGuid;
|
private readonly ILocalPlayerIdentitySource _identity;
|
||||||
private readonly Action<LiveEntityPruneCandidate> _prune;
|
private readonly ILiveEntityPruneSink _prune;
|
||||||
private readonly LiveEntityLivenessTracker _tracker = new();
|
private readonly LiveEntityLivenessTracker _tracker = new();
|
||||||
private readonly List<LiveEntityLivenessSample> _samples = new();
|
private readonly List<LiveEntityLivenessSample> _samples = new();
|
||||||
private double _nextMaintenanceAt;
|
private double _nextMaintenanceAt;
|
||||||
|
|
||||||
public LiveEntityLivenessController(
|
public LiveEntityLivenessController(
|
||||||
LiveEntityRuntime runtime,
|
LiveEntityRuntime runtime,
|
||||||
Func<uint> playerGuid,
|
ILocalPlayerIdentitySource identity,
|
||||||
Action<LiveEntityPruneCandidate> prune)
|
ILiveEntityPruneSink prune)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||||
_prune = prune ?? throw new ArgumentNullException(nameof(prune));
|
_prune = prune ?? throw new ArgumentNullException(nameof(prune));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +109,7 @@ internal sealed class LiveEntityLivenessController
|
||||||
return;
|
return;
|
||||||
_nextMaintenanceAt = now + MaintenanceIntervalSeconds;
|
_nextMaintenanceAt = now + MaintenanceIntervalSeconds;
|
||||||
|
|
||||||
uint playerGuid = _playerGuid();
|
uint playerGuid = _identity.ServerGuid;
|
||||||
if (playerGuid == 0
|
if (playerGuid == 0
|
||||||
|| !_runtime.TryGetRecord(playerGuid, out LiveEntityRecord player)
|
|| !_runtime.TryGetRecord(playerGuid, out LiveEntityRecord player)
|
||||||
|| player.Snapshot.Position is not { } playerPosition)
|
|| player.Snapshot.Position is not { } playerPosition)
|
||||||
|
|
@ -144,7 +145,7 @@ internal sealed class LiveEntityLivenessController
|
||||||
if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current)
|
if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current)
|
||||||
&& current.Generation == candidate.Generation)
|
&& current.Generation == candidate.Generation)
|
||||||
{
|
{
|
||||||
_prune(candidate);
|
_prune.Prune(candidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.App.Interaction;
|
using AcDream.App.Interaction;
|
||||||
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
|
@ -38,7 +39,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
||||||
private readonly ShadowObjectRegistry? _shadows;
|
private readonly ShadowObjectRegistry? _shadows;
|
||||||
private readonly LiveEntityLightController? _lights;
|
private readonly LiveEntityLightController? _lights;
|
||||||
private readonly EntityClassificationCache? _classification;
|
private readonly EntityClassificationCache? _classification;
|
||||||
private readonly Func<uint>? _playerGuid;
|
private readonly ILocalPlayerIdentitySource? _identity;
|
||||||
private readonly Func<LiveEntityRecord, LiveEntityTeardownPlan> _createPlan;
|
private readonly Func<LiveEntityRecord, LiveEntityTeardownPlan> _createPlan;
|
||||||
private readonly Action<uint> _forgetUnknownOwner;
|
private readonly Action<uint> _forgetUnknownOwner;
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
||||||
ShadowObjectRegistry shadows,
|
ShadowObjectRegistry shadows,
|
||||||
LiveEntityLightController lights,
|
LiveEntityLightController lights,
|
||||||
EntityClassificationCache classification,
|
EntityClassificationCache classification,
|
||||||
Func<uint> playerGuid)
|
ILocalPlayerIdentitySource identity)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
||||||
|
|
@ -77,7 +78,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
||||||
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||||
_classification = classification
|
_classification = classification
|
||||||
?? throw new ArgumentNullException(nameof(classification));
|
?? throw new ArgumentNullException(nameof(classification));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||||
_createPlan = CreatePlan;
|
_createPlan = CreatePlan;
|
||||||
_forgetUnknownOwner = effects.ForgetUnknownOwner;
|
_forgetUnknownOwner = effects.ForgetUnknownOwner;
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +159,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
||||||
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
|
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
|
||||||
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
|
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
|
||||||
record,
|
record,
|
||||||
_playerGuid!()));
|
_identity!.ServerGuid));
|
||||||
cleanups.Add(() => _children!.OnLogicalTeardown(record));
|
cleanups.Add(() => _children!.OnLogicalTeardown(record));
|
||||||
cleanups.Add(() => _shadows!.Deregister(existingEntity.Id));
|
cleanups.Add(() => _shadows!.Deregister(existingEntity.Id));
|
||||||
cleanups.Add(() => _lights!.Forget(existingEntity.Id));
|
cleanups.Add(() => _lights!.Forget(existingEntity.Id));
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.World;
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -40,6 +42,23 @@ internal sealed class LiveWorldOriginState
|
||||||
CenterY = centerY;
|
CenterY = centerY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (int X, int Y) GetCenter() => (CenterX, CenterY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the streamed render frame back into retail's landblock-local
|
||||||
|
/// physics frame at the one placement seed boundary.
|
||||||
|
/// </summary>
|
||||||
|
public Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
|
||||||
|
{
|
||||||
|
int landblockX = (int)((cellId >> 24) & 0xFFu);
|
||||||
|
int landblockY = (int)((cellId >> 16) & 0xFFu);
|
||||||
|
var origin = new Vector3(
|
||||||
|
(landblockX - CenterX) * 192f,
|
||||||
|
(landblockY - CenterY) * 192f,
|
||||||
|
0f);
|
||||||
|
return worldPosition - origin;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ends the accepted-origin lifetime while retaining the last coordinates
|
/// Ends the accepted-origin lifetime while retaining the last coordinates
|
||||||
/// as the next placeholder. Consumers must gate them on
|
/// as the next placeholder. Consumers must gate them on
|
||||||
|
|
|
||||||
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 System.Numerics;
|
||||||
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
@ -1594,9 +1595,14 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
public readonly RecordingOrigin Origin;
|
public readonly RecordingOrigin Origin;
|
||||||
public readonly RecordingNetworkSink Network = new();
|
public readonly RecordingNetworkSink Network = new();
|
||||||
public readonly RecordingTeardownCoordinator Teardown = new();
|
public readonly RecordingTeardownCoordinator Teardown = new();
|
||||||
|
public readonly RecordingTimestampPublisher Timestamps;
|
||||||
public readonly LiveEntityHydrationController Controller;
|
public readonly LiveEntityHydrationController Controller;
|
||||||
|
|
||||||
public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction { get; set; }
|
public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction
|
||||||
|
{
|
||||||
|
get => Timestamps.PublishAction;
|
||||||
|
set => Timestamps.PublishAction = value;
|
||||||
|
}
|
||||||
|
|
||||||
public Fixture(
|
public Fixture(
|
||||||
bool originKnown,
|
bool originKnown,
|
||||||
|
|
@ -1617,6 +1623,7 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Relationships = new RecordingRelationships(Operations);
|
Relationships = new RecordingRelationships(Operations);
|
||||||
Ready = new RecordingReadyPublisher(Operations);
|
Ready = new RecordingReadyPublisher(Operations);
|
||||||
Origin = new RecordingOrigin(originKnown);
|
Origin = new RecordingOrigin(originKnown);
|
||||||
|
Timestamps = new RecordingTimestampPublisher(Operations);
|
||||||
Network.ApplyAction = events =>
|
Network.ApplyAction = events =>
|
||||||
{
|
{
|
||||||
if (events.Position is not { } position)
|
if (events.Position is not { } position)
|
||||||
|
|
@ -1631,6 +1638,15 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
out _,
|
out _,
|
||||||
out _);
|
out _);
|
||||||
};
|
};
|
||||||
|
var identity = new LocalPlayerIdentityState
|
||||||
|
{
|
||||||
|
ServerGuid = playerGuid,
|
||||||
|
};
|
||||||
|
var deletion = new LiveEntityDeletionController(
|
||||||
|
Runtime,
|
||||||
|
Objects,
|
||||||
|
Teardown,
|
||||||
|
identity);
|
||||||
Controller = new LiveEntityHydrationController(
|
Controller = new LiveEntityHydrationController(
|
||||||
Runtime,
|
Runtime,
|
||||||
Objects,
|
Objects,
|
||||||
|
|
@ -1640,13 +1656,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Ready,
|
Ready,
|
||||||
Origin,
|
Origin,
|
||||||
Network,
|
Network,
|
||||||
Teardown,
|
Timestamps,
|
||||||
(guid, timestamps) =>
|
identity,
|
||||||
{
|
deletion);
|
||||||
Operations.Add($"timestamps:{timestamps.Instance}");
|
|
||||||
PublishTimestampsAction?.Invoke(guid, timestamps);
|
|
||||||
},
|
|
||||||
() => playerGuid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveEntityRecord Record
|
public LiveEntityRecord Record
|
||||||
|
|
@ -1671,6 +1683,23 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingTimestampPublisher
|
||||||
|
: IAcceptedLocalPhysicsTimestampPublisher
|
||||||
|
{
|
||||||
|
private readonly List<string> _operations;
|
||||||
|
|
||||||
|
public RecordingTimestampPublisher(List<string> operations) =>
|
||||||
|
_operations = operations;
|
||||||
|
|
||||||
|
public Action<uint, AcceptedPhysicsTimestamps>? PublishAction { get; set; }
|
||||||
|
|
||||||
|
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
|
||||||
|
{
|
||||||
|
_operations.Add($"timestamps:{timestamps.Instance}");
|
||||||
|
PublishAction?.Invoke(serverGuid, timestamps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class RecordingResources : ILiveEntityResourceLifecycle
|
private class RecordingResources : ILiveEntityResourceLifecycle
|
||||||
{
|
{
|
||||||
public int RegisterCount { get; protected set; }
|
public int RegisterCount { get; protected set; }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Numerics;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.World;
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
@ -59,4 +60,17 @@ public sealed class LiveWorldOriginStateTests
|
||||||
Assert.Equal(50, state.CenterX);
|
Assert.Equal(50, state.CenterX);
|
||||||
Assert.Equal(51, state.CenterY);
|
Assert.Equal(51, state.CenterY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CellLocalForSeed_UsesTheCurrentLandblockOrigin()
|
||||||
|
{
|
||||||
|
var state = new LiveWorldOriginState();
|
||||||
|
state.SetPlaceholder(0x30, 0x32);
|
||||||
|
|
||||||
|
Vector3 local = state.CellLocalForSeed(
|
||||||
|
new Vector3(200f, -300f, 5f),
|
||||||
|
0x3130_0001u);
|
||||||
|
|
||||||
|
Assert.Equal(new Vector3(8f, 84f, 5f), local);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,8 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ConditionalReconciles_RemainAtTheirOwningEdges()
|
public void ConditionalReconciles_RemainAtTheirOwningEdges()
|
||||||
{
|
{
|
||||||
// Checkpoint A freezes only the outer ownership contract. Checkpoints
|
// The outer ownership oracle is complemented by the production
|
||||||
// E and F must replace these probes with production teleport/camera
|
// teleport and camera tests that pin both conditional reconcile edges.
|
||||||
// owner tests before the conditional edges can be claimed guarded.
|
|
||||||
var calls = new List<string>();
|
var calls = new List<string>();
|
||||||
UpdateFrameOrchestrator frame = Create(
|
UpdateFrameOrchestrator frame = Create(
|
||||||
calls,
|
calls,
|
||||||
|
|
@ -228,19 +227,6 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
FieldInfo[] ownerFields = owner.GetFields(
|
FieldInfo[] ownerFields = owner.GetFields(
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow));
|
Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow));
|
||||||
if (owner == typeof(AcDream.App.Input.RetailLocalPlayerFrameController))
|
|
||||||
{
|
|
||||||
// Checkpoint D replaced input capture with its typed owner.
|
|
||||||
// Checkpoint F still owns the remaining player presentation
|
|
||||||
// callback composition. No other phase owner may add one.
|
|
||||||
Assert.Contains(
|
|
||||||
ownerFields,
|
|
||||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
|
||||||
Assert.DoesNotContain(
|
|
||||||
ownerFields,
|
|
||||||
field => field.FieldType == typeof(Func<AcDream.App.Input.MovementInput>));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
ownerFields,
|
ownerFields,
|
||||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
|
@ -277,14 +263,24 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce()
|
public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce()
|
||||||
{
|
{
|
||||||
|
string root = FindRepoRoot();
|
||||||
string source = File.ReadAllText(Path.Combine(
|
string source = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
root,
|
||||||
"src",
|
"src",
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
string adapters = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Update",
|
||||||
|
"UpdateFrameRuntimeAdapters.cs"));
|
||||||
|
|
||||||
Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1);
|
Assert.Equal(0, CountOccurrences(source, "PublishTime("));
|
||||||
|
Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime("));
|
||||||
|
Assert.Contains("new AcDream.App.Update.PhysicsScriptClockPublisher(", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -363,12 +359,10 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Streaming.StreamingFrameController(", source);
|
Assert.Contains("new AcDream.App.Streaming.StreamingFrameController(", source);
|
||||||
Assert.Equal(1, CountOccurrences(source, "_streamingFrame.Tick();"));
|
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
||||||
AssertAppearsInOrder(
|
Assert.Equal(1, CountOccurrences(
|
||||||
source,
|
source,
|
||||||
"_streamingFrame.Tick();",
|
"_updateFrameOrchestrator.Tick("));
|
||||||
"_gameplayInputFrame!.Tick(frameTiming);",
|
|
||||||
"_liveFrameCoordinator.Tick(frameDelta);");
|
|
||||||
Assert.DoesNotContain("_streamingController.Tick(observerCx", source,
|
Assert.DoesNotContain("_streamingController.Tick(observerCx", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("DungeonStreamingGate.Compute", source,
|
Assert.DoesNotContain("DungeonStreamingGate.Compute", source,
|
||||||
|
|
@ -387,7 +381,8 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Input.GameplayInputFrameController(", source);
|
Assert.Contains("new AcDream.App.Input.GameplayInputFrameController(", source);
|
||||||
Assert.Equal(1, CountOccurrences(source, "_gameplayInputFrame!.Tick(frameTiming);"));
|
Assert.DoesNotContain("_gameplayInputFrame!.Tick", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_inputDispatcher?.Tick()", source,
|
Assert.DoesNotContain("_inputDispatcher?.Tick()", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("TryTakeRawSample", source,
|
Assert.DoesNotContain("TryTakeRawSample", source,
|
||||||
|
|
@ -468,9 +463,8 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
||||||
source,
|
source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.DoesNotContain("_localPlayerTeleport!.Tick", source,
|
||||||
source,
|
StringComparison.Ordinal);
|
||||||
"_localPlayerTeleport!.Tick(frameDelta);"));
|
|
||||||
Assert.DoesNotContain("_teleportTransit", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_teleportTransit", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_teleportAnim", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_teleportAnim", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_teleportViewPlane", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_teleportViewPlane", source, StringComparison.Ordinal);
|
||||||
|
|
@ -486,9 +480,10 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"_liveEntityLiveness?.Tick(ClientTimerNow());",
|
"new AcDream.App.Update.LiveEntityLivenessFramePhase(",
|
||||||
"_localPlayerTeleport!.Tick(frameDelta);",
|
"_localPlayerTeleport,",
|
||||||
"_playerModeAutoEntry?.TryEnter();");
|
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
|
||||||
|
"cameraFrame);");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -649,7 +644,9 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Rendering",
|
||||||
"GameWindow.cs"));
|
"GameWindow.cs"));
|
||||||
Assert.Equal(1, CountOccurrences(source, "_cameraFrame.Tick(frameTiming);"));
|
Assert.Equal(1, CountOccurrences(
|
||||||
|
source,
|
||||||
|
"_updateFrameOrchestrator.Tick("));
|
||||||
Assert.DoesNotContain("CanAdvanceLocalPlayer", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("CanAdvanceLocalPlayer", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("GetCombatCameraTargetPoint()", source,
|
Assert.DoesNotContain("GetCombatCameraTargetPoint()", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
@ -657,11 +654,7 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_localPlayerFrame.TryGetPresentationAfterNetwork", source,
|
Assert.DoesNotContain("_localPlayerFrame.TryGetPresentationAfterNetwork", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
AssertAppearsInOrder(
|
Assert.DoesNotContain("_cameraFrame", source, StringComparison.Ordinal);
|
||||||
source,
|
|
||||||
"_localPlayerTeleport!.Tick(frameDelta);",
|
|
||||||
"_playerModeAutoEntry?.TryEnter();",
|
|
||||||
"_cameraFrame.Tick(frameTiming);");
|
|
||||||
|
|
||||||
string cameraSource = File.ReadAllText(Path.Combine(
|
string cameraSource = File.ReadAllText(Path.Combine(
|
||||||
root,
|
root,
|
||||||
|
|
@ -677,6 +670,123 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"retail?.Update(");
|
"retail?.Update(");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GameWindow_OnUpdateOwnsOnlyProfilingAndOneOrchestratorTick()
|
||||||
|
{
|
||||||
|
string source = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Rendering",
|
||||||
|
"GameWindow.cs"));
|
||||||
|
|
||||||
|
Assert.Equal(1, CountOccurrences(
|
||||||
|
source,
|
||||||
|
"_updateFrameOrchestrator.Tick("));
|
||||||
|
Assert.DoesNotContain("_updateFrameClock.Advance(", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("_liveFrameCoordinator", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("_liveEntityLiveness?.Tick", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("_playerModeAutoEntry?.TryEnter", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
source,
|
||||||
|
"private void OnUpdate(double dt)",
|
||||||
|
"_frameProfiler.BeginStage(",
|
||||||
|
"_updateFrameOrchestrator.Tick(",
|
||||||
|
"private void OnCameraModeChanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductionFrameAdaptersRetainTypedOwnersWithoutWindowCallbacks()
|
||||||
|
{
|
||||||
|
Type[] adapters =
|
||||||
|
[
|
||||||
|
typeof(LiveEntityTeardownFramePhase),
|
||||||
|
typeof(ConsoleUpdateFrameFailureSink),
|
||||||
|
typeof(PhysicsScriptClockPublisher),
|
||||||
|
typeof(LiveEntityLivenessFramePhase),
|
||||||
|
typeof(PlayerModeAutoEntryFramePhase),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (Type adapter in adapters)
|
||||||
|
{
|
||||||
|
FieldInfo[] fields = adapter.GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
fields,
|
||||||
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldInfo clock = Assert.Single(
|
||||||
|
typeof(LiveEntityLivenessFramePhase).GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.Name == "_clock");
|
||||||
|
Assert.Equal(typeof(IClientMonotonicTimeSource), clock.FieldType);
|
||||||
|
|
||||||
|
FieldInfo teardownRuntime = Assert.Single(
|
||||||
|
typeof(LiveEntityTeardownFramePhase).GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic));
|
||||||
|
Assert.Equal(typeof(LiveEntityRuntime), teardownRuntime.FieldType);
|
||||||
|
|
||||||
|
Type[] typedProductionOwners =
|
||||||
|
[
|
||||||
|
typeof(LiveEntityLivenessController),
|
||||||
|
typeof(AcDream.App.Input.LivePlayerModeAutoEntryContext),
|
||||||
|
typeof(LiveSessionLocalPhysicsTimestampPublisher),
|
||||||
|
typeof(AcDream.App.Physics.LiveEntityNetworkUpdateController),
|
||||||
|
typeof(AcDream.App.Rendering.LiveEntityPartArrayLifecycle),
|
||||||
|
typeof(AcDream.App.Physics.RemoteShadowPlacementSynchronizer),
|
||||||
|
typeof(AcDream.App.Physics.RemoteTeleportPlacementPresentation),
|
||||||
|
typeof(AcDream.App.Net.LiveEntitySessionController),
|
||||||
|
];
|
||||||
|
foreach (Type owner in typedProductionOwners)
|
||||||
|
{
|
||||||
|
FieldInfo[] fields = owner.GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
fields,
|
||||||
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldInfo originIdentity = Assert.Single(
|
||||||
|
typeof(LiveEntityWorldOriginCoordinator).GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.Name == "_identity");
|
||||||
|
Assert.Equal(
|
||||||
|
typeof(AcDream.App.Input.ILocalPlayerIdentitySource),
|
||||||
|
originIdentity.FieldType);
|
||||||
|
|
||||||
|
string source = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Rendering",
|
||||||
|
"GameWindow.cs"));
|
||||||
|
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("LoginWorldReady", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("candidate => _liveEntityHydration.OnPrune", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("CreateLiveEntitySessionSink", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("private System.Numerics.Vector3 CellLocalForSeed", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("OnPlayScriptReceived", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_liveWorldOrigin.GetCenter", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_liveWorldOrigin.CellLocalForSeed", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static UpdateFrameOrchestrator Create(
|
private static UpdateFrameOrchestrator Create(
|
||||||
List<string> calls,
|
List<string> calls,
|
||||||
RecordingTeardown? teardown = null,
|
RecordingTeardown? teardown = null,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue