refactor(app): key live projections by runtime identity
Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
This commit is contained in:
parent
e18df84437
commit
420e5eea70
73 changed files with 2939 additions and 1715 deletions
|
|
@ -22,11 +22,6 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
private readonly SelectionState _selection;
|
||||
private readonly LiveWorldOriginState _origin;
|
||||
|
||||
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
||||
_liveEntities.MaterializedWorldEntities;
|
||||
private IReadOnlyDictionary<uint, WorldEntity> _visibleEntitiesByServerGuid =>
|
||||
_liveEntities.WorldEntities;
|
||||
|
||||
public LiveEntityMotionRuntimeController(
|
||||
LiveEntityRuntime liveEntities,
|
||||
PhysicsDataCache physicsDataCache,
|
||||
|
|
@ -101,7 +96,7 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
// (own side) and StickyManager::adjust_offset's own-radius gap term.
|
||||
// Replaces the R4 `() => 0f` pins ("setup cylsphere radius lands with
|
||||
// R5-V3").
|
||||
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var selfEntity))
|
||||
if (!_liveEntities.TryGetWorldEntity(serverGuid, out var selfEntity))
|
||||
return rm.Sink;
|
||||
// R5-V2: forward-declared so the MoveToManager's target seams route
|
||||
// into the entity's TargetManager (retail CPhysicsObj::set_target →
|
||||
|
|
@ -237,7 +232,7 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
{
|
||||
Console.WriteLine(
|
||||
$"[autowalk-host-miss] object=0x{id:X8} "
|
||||
+ $"materialized={_entitiesByServerGuid.ContainsKey(id)} "
|
||||
+ $"materialized={_liveEntities.ContainsWorldEntity(id)} "
|
||||
+ $"registered={liveEntities.TryGetPhysicsHost(id, out _)} "
|
||||
+ $"hidden={liveEntities.IsHidden(id)}");
|
||||
}
|
||||
|
|
@ -454,7 +449,9 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
string target = turnPath.TargetGuid is { } targetGuid
|
||||
? $"0x{targetGuid:X8}" : "null";
|
||||
bool targetVisible = turnPath.TargetGuid is { } visibleGuid
|
||||
&& _visibleEntitiesByServerGuid.ContainsKey(visibleGuid);
|
||||
&& _liveEntities.TryGetInteractionEligibleEntity(
|
||||
visibleGuid,
|
||||
out _);
|
||||
bool targetHost = turnPath.TargetGuid is { } hostGuid
|
||||
&& _liveEntities?.TryGetPhysicsHost(hostGuid, out _) == true;
|
||||
var moveTo = movement.MoveTo;
|
||||
|
|
|
|||
|
|
@ -65,11 +65,6 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
|
||||
private uint _playerServerGuid => _playerIdentity.ServerGuid;
|
||||
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
|
||||
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
||||
_liveEntities.MaterializedWorldEntities;
|
||||
private IReadOnlyDictionary<uint, WorldEntity> _visibleEntitiesByServerGuid =>
|
||||
_liveEntities.WorldEntities;
|
||||
|
||||
internal uint? LastLivePlayerLandblockId =>
|
||||
_authorityGate.LastLivePlayerLandblockId;
|
||||
|
||||
|
|
@ -262,7 +257,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
ulong acceptedMovementVelocityAuthorityVersion =
|
||||
accepted.VelocityAuthorityVersion;
|
||||
|
||||
if (!_entitiesByServerGuid.TryGetValue(update.Guid, out var entity)) return;
|
||||
if (!_liveEntities.TryGetWorldEntity(update.Guid, out var entity)) return;
|
||||
if (!_animatedEntities.TryGetValue(entity.Id, out var ae))
|
||||
{
|
||||
DispatchRemoteInboundMotion(
|
||||
|
|
@ -566,8 +561,14 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// window from this transition. Without this, the entity
|
||||
// starts its run animation and is instantly interrupted.
|
||||
var refreshedTime = System.DateTime.UtcNow;
|
||||
if (_remoteMovementObservations.TryGetValue(update.Guid, out var prev))
|
||||
_remoteMovementObservations[update.Guid] = (prev.Pos, refreshedTime);
|
||||
if (acceptedMotionRecord.ProjectionKey is { } motionKey
|
||||
&& _remoteMovementObservations.TryGetValue(
|
||||
motionKey,
|
||||
out var prev))
|
||||
{
|
||||
_remoteMovementObservations[motionKey] =
|
||||
(prev.Pos, refreshedTime);
|
||||
}
|
||||
if (_remoteDeadReckon.TryGetValue(update.Guid, out var dr))
|
||||
dr.LastServerPosTime = (refreshedTime - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
}
|
||||
|
|
@ -817,7 +818,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
ulong acceptedVectorAuthorityVersion,
|
||||
ulong acceptedVectorVelocityAuthorityVersion)
|
||||
{
|
||||
if (!_entitiesByServerGuid.ContainsKey(update.Guid)) return;
|
||||
if (!_liveEntities.ContainsWorldEntity(update.Guid)) return;
|
||||
|
||||
if (update.Guid == _playerServerGuid) return; // local jump uses our own physics
|
||||
if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rm)) return;
|
||||
|
|
@ -863,7 +864,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// state-derived velocity write (adaptation note: retail's
|
||||
// equivalence comes from the per-tick transition-sweep order —
|
||||
// R6 scope).
|
||||
if (_entitiesByServerGuid.TryGetValue(update.Guid, out var ent)
|
||||
if (_liveEntities.TryGetWorldEntity(update.Guid, out var ent)
|
||||
&& _animatedEntities.TryGetValue(ent.Id, out var ae)
|
||||
&& ae.Sequencer is not null)
|
||||
{
|
||||
|
|
@ -940,12 +941,12 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
if (parsed.Guid == _playerServerGuid)
|
||||
_playerController?.ApplyPhysicsState(record.FinalPhysicsState);
|
||||
|
||||
if (!_entitiesByServerGuid.TryGetValue(parsed.Guid, out var entity)) return;
|
||||
if (!_liveEntities.TryGetWorldEntity(parsed.Guid, out var entity)) return;
|
||||
|
||||
// L.2g slice 1c (2026-05-13): the server addresses entities by
|
||||
// ServerGuid (parsed.Guid, e.g. 0x7A9B4015), but
|
||||
// ShadowObjectRegistry's cell index is keyed by local entity.Id
|
||||
// (e.g. 0x000F4245). Translate via _entitiesByServerGuid before
|
||||
// (e.g. 0x000F4245). Translate through the canonical Runtime directory before
|
||||
// mutating the registry — otherwise the lookup misses and the
|
||||
// state flip silently no-ops, leaving doors blocked even though
|
||||
// ACE flipped the ETHEREAL bit.
|
||||
|
|
@ -1035,7 +1036,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
_playerController)))
|
||||
return;
|
||||
|
||||
if (!_entitiesByServerGuid.ContainsKey(update.Guid))
|
||||
if (!_liveEntities.ContainsWorldEntity(update.Guid))
|
||||
{
|
||||
if (!IsCurrentPositionOwner())
|
||||
return;
|
||||
|
|
@ -1068,7 +1069,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
}
|
||||
}
|
||||
|
||||
if (!_entitiesByServerGuid.TryGetValue(update.Guid, out var entity)) return;
|
||||
if (!_liveEntities.TryGetWorldEntity(update.Guid, out var entity)) return;
|
||||
if (!IsCurrentPositionOwner(entity))
|
||||
return;
|
||||
_entityEffects?.MarkLiveOwnerPoseDirty(update.Guid);
|
||||
|
|
@ -1223,16 +1224,20 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
if (update.Guid != _playerServerGuid)
|
||||
{
|
||||
var now = System.DateTime.UtcNow;
|
||||
if (_remoteMovementObservations.TryGetValue(update.Guid, out var prev))
|
||||
RuntimeEntityKey positionKey = positionRecord.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Position owner 0x{update.Guid:X8}/" +
|
||||
$"{positionRecord.Generation} has no exact projection key.");
|
||||
if (_remoteMovementObservations.TryGetValue(positionKey, out var prev))
|
||||
{
|
||||
float moveDist = System.Numerics.Vector3.Distance(prev.Pos, worldPos);
|
||||
if (moveDist > 0.05f)
|
||||
_remoteMovementObservations[update.Guid] = (worldPos, now);
|
||||
_remoteMovementObservations[positionKey] = (worldPos, now);
|
||||
// else: leave old entry so "Time" = last real movement time
|
||||
}
|
||||
else
|
||||
{
|
||||
_remoteMovementObservations[update.Guid] = (worldPos, now);
|
||||
_remoteMovementObservations[positionKey] = (worldPos, now);
|
||||
}
|
||||
|
||||
// Retail-faithful hard-snap on UpdatePosition.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
|
|
@ -9,19 +10,20 @@ namespace AcDream.App.Physics;
|
|||
/// </summary>
|
||||
internal sealed class RemoteMovementObservationTracker
|
||||
{
|
||||
private readonly Dictionary<uint, (Vector3 Pos, DateTime Time)> _lastMove = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, (Vector3 Pos, DateTime Time)>
|
||||
_lastMove = [];
|
||||
|
||||
internal (Vector3 Pos, DateTime Time) this[uint serverGuid]
|
||||
internal (Vector3 Pos, DateTime Time) this[RuntimeEntityKey key]
|
||||
{
|
||||
set => _lastMove[serverGuid] = value;
|
||||
set => _lastMove[key] = value;
|
||||
}
|
||||
|
||||
internal bool TryGetValue(
|
||||
uint serverGuid,
|
||||
RuntimeEntityKey key,
|
||||
out (Vector3 Pos, DateTime Time) observation) =>
|
||||
_lastMove.TryGetValue(serverGuid, out observation);
|
||||
_lastMove.TryGetValue(key, out observation);
|
||||
|
||||
internal bool Remove(uint serverGuid) => _lastMove.Remove(serverGuid);
|
||||
internal bool Remove(RuntimeEntityKey key) => _lastMove.Remove(key);
|
||||
|
||||
internal void Clear() => _lastMove.Clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using AcDream.App.Rendering;
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
private readonly Action<uint, ushort, bool> _completeAuthoritativePlacement;
|
||||
private readonly Action<uint, ushort> _beginAuthoritativePlacement;
|
||||
private readonly PlacementResolver _resolvePlacement;
|
||||
private readonly Dictionary<uint, PendingPlacement> _pending = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, PendingPlacement> _pending = new();
|
||||
|
||||
internal RemoteTeleportController(
|
||||
PhysicsEngine physics,
|
||||
|
|
@ -178,9 +179,9 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
bool wasInContact = liveRecord.PhysicsBody.InContact;
|
||||
bool wasOnWalkable = liveRecord.PhysicsBody.OnWalkable;
|
||||
RollbackPlacement rollback = CaptureRollback(remote, liveRecord.PhysicsBody);
|
||||
if (_pending.TryGetValue(entity.ServerGuid, out PendingPlacement prior)
|
||||
&& ReferenceEquals(prior.Record, liveRecord)
|
||||
&& prior.Generation == generation)
|
||||
RuntimeEntityKey key = RequireProjectionKey(liveRecord);
|
||||
if (_pending.TryGetValue(key, out PendingPlacement prior)
|
||||
&& ReferenceEquals(prior.Record, liveRecord))
|
||||
{
|
||||
wasInContact = prior.WasInContact;
|
||||
wasOnWalkable = prior.WasOnWalkable;
|
||||
|
|
@ -219,7 +220,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
return Superseded(request);
|
||||
if (!placement.Ok)
|
||||
{
|
||||
_pending.Remove(entity.ServerGuid);
|
||||
_pending.Remove(key);
|
||||
if (!RollBackDeferredPlacement(request))
|
||||
return Superseded(request);
|
||||
return new Result(
|
||||
|
|
@ -230,24 +231,18 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
request.Body.Orientation);
|
||||
}
|
||||
|
||||
_pending.Remove(entity.ServerGuid);
|
||||
_pending.Remove(key);
|
||||
return CommitResolved(request, placement);
|
||||
}
|
||||
|
||||
internal void Forget(uint serverGuid)
|
||||
{
|
||||
_pending.Remove(serverGuid);
|
||||
}
|
||||
|
||||
internal void Forget(LiveEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (_pending.TryGetValue(record.ServerGuid, out PendingPlacement pending)
|
||||
&& pending.Generation == record.Generation
|
||||
&& (record.WorldEntity is null
|
||||
|| ReferenceEquals(pending.Entity, record.WorldEntity)))
|
||||
if (record.ProjectionKey is { } key
|
||||
&& _pending.TryGetValue(key, out PendingPlacement pending)
|
||||
&& ReferenceEquals(pending.Record, record))
|
||||
{
|
||||
_pending.Remove(record.ServerGuid);
|
||||
_pending.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +251,10 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
_pending.Clear();
|
||||
}
|
||||
|
||||
internal bool HasPending(uint serverGuid) => _pending.ContainsKey(serverGuid);
|
||||
internal bool HasPending(uint serverGuid) =>
|
||||
_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
&& record.ProjectionKey is { } key
|
||||
&& _pending.ContainsKey(key);
|
||||
internal int PendingPlacementCount => _pending.Count;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -404,7 +402,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
request.Entity.Rotation = request.RequestedOrientation;
|
||||
if (!IsCurrent(request))
|
||||
return false;
|
||||
_pending[request.Entity.ServerGuid] = request;
|
||||
_pending[RequireProjectionKey(request.Record)] = request;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -413,14 +411,15 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
if (!visible)
|
||||
return;
|
||||
|
||||
if (_pending.TryGetValue(record.ServerGuid, out PendingPlacement pending))
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
if (_pending.TryGetValue(key, out PendingPlacement pending))
|
||||
{
|
||||
if (record.WorldEntity is null
|
||||
|| !ReferenceEquals(record, pending.Record)
|
||||
|| !ReferenceEquals(record.WorldEntity, pending.Entity)
|
||||
|| record.Generation != pending.Generation)
|
||||
{
|
||||
_pending.Remove(record.ServerGuid);
|
||||
_pending.Remove(key);
|
||||
return;
|
||||
}
|
||||
if (record.RemoteMotionRuntime is not ILiveEntityRemotePlacementRuntime currentRemote
|
||||
|
|
@ -428,7 +427,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
|| !ReferenceEquals(record.PhysicsBody, pending.Body)
|
||||
|| !ReferenceEquals(currentRemote.Body, record.PhysicsBody))
|
||||
{
|
||||
_pending.Remove(record.ServerGuid);
|
||||
_pending.Remove(key);
|
||||
if (record.RemoteMotionRuntime is not null
|
||||
&& (record.PhysicsBody is null
|
||||
|| !ReferenceEquals(record.RemoteMotionRuntime.Body, record.PhysicsBody)))
|
||||
|
|
@ -441,7 +440,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
if (!ReferenceEquals(currentRemote, pending.Remote))
|
||||
{
|
||||
pending = pending with { Remote = currentRemote };
|
||||
_pending[record.ServerGuid] = pending;
|
||||
_pending[key] = pending;
|
||||
}
|
||||
if (!_liveEntities.IsCurrentPositionAuthority(
|
||||
pending.Record,
|
||||
|
|
@ -454,7 +453,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
_pending.Remove(record.ServerGuid);
|
||||
_pending.Remove(key);
|
||||
ResolveResult placement = Resolve(pending);
|
||||
if (!IsCurrent(pending))
|
||||
return;
|
||||
|
|
@ -588,4 +587,11 @@ internal sealed class RemoteTeleportController : IDisposable
|
|||
|
||||
private static bool IsPlayerGuid(uint guid) =>
|
||||
(guid & 0xFF000000u) == 0x50000000u;
|
||||
|
||||
private static RuntimeEntityKey RequireProjectionKey(
|
||||
LiveEntityRecord record) =>
|
||||
record.ProjectionKey
|
||||
?? throw new InvalidOperationException(
|
||||
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||
"has no exact projection key.");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue