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:
Erik 2026-07-25 21:50:58 +02:00
parent e18df84437
commit 420e5eea70
73 changed files with 2939 additions and 1715 deletions

View file

@ -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;

View file

@ -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.

View file

@ -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();
}

View file

@ -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.");
}