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
|
|
@ -73,8 +73,10 @@ internal sealed class CombatAttackTargetSource : ICombatAttackTargetSource
|
||||||
}
|
}
|
||||||
|
|
||||||
(uint Guid, float DistanceSquared)? best = null;
|
(uint Guid, float DistanceSquared)? best = null;
|
||||||
foreach ((uint guid, WorldEntity entity) in _liveEntities.WorldEntities)
|
foreach (LiveEntityRecord record in _liveEntities.VisibleRecords)
|
||||||
{
|
{
|
||||||
|
uint guid = record.ServerGuid;
|
||||||
|
WorldEntity entity = record.WorldEntity!;
|
||||||
if (!IsHostileMonster(guid))
|
if (!IsHostileMonster(guid))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -349,7 +349,19 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
new List<CompositeLiveEntityResourceLifecycle.Owner>
|
new List<CompositeLiveEntityResourceLifecycle.Owner>
|
||||||
{
|
{
|
||||||
new(
|
new(
|
||||||
entity => _ = entitySpawnAdapter.OnCreate(entity),
|
entity =>
|
||||||
|
{
|
||||||
|
if (liveEntities is null
|
||||||
|
|| !liveEntities.TryGetRecordByLocalEntityId(
|
||||||
|
entity.Id,
|
||||||
|
out LiveEntityRecord record)
|
||||||
|
|| record.ProjectionKey is not { } key)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Live presentation registration requires an exact Runtime projection key.");
|
||||||
|
}
|
||||||
|
_ = entitySpawnAdapter.OnCreate(key, entity);
|
||||||
|
},
|
||||||
entity => _ = entitySpawnAdapter.OnRemove(entity)),
|
entity => _ = entitySpawnAdapter.OnRemove(entity)),
|
||||||
new(
|
new(
|
||||||
entityScriptActivator.OnCreate,
|
entityScriptActivator.OnCreate,
|
||||||
|
|
@ -688,7 +700,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
});
|
});
|
||||||
var radarSnapshotProvider = new RadarSnapshotProvider(
|
var radarSnapshotProvider = new RadarSnapshotProvider(
|
||||||
d.Objects,
|
d.Objects,
|
||||||
() => liveEntities.WorldEntities,
|
liveEntities,
|
||||||
() => liveEntities.Snapshots,
|
() => liveEntities.Snapshots,
|
||||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||||
playerYawRadians: () => d.PlayerController.Controller?.Yaw ?? 0f,
|
playerYawRadians: () => d.PlayerController.Controller?.Yaw ?? 0f,
|
||||||
|
|
@ -696,7 +708,6 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
selectedGuid: () => d.Selection.SelectedObjectId,
|
selectedGuid: () => d.Selection.SelectedObjectId,
|
||||||
coordinatesOnRadar: () => d.Settings.Gameplay.CoordinatesOnRadar,
|
coordinatesOnRadar: () => d.Settings.Gameplay.CoordinatesOnRadar,
|
||||||
uiLocked: () => d.Settings.Gameplay.LockUI,
|
uiLocked: () => d.Settings.Gameplay.LockUI,
|
||||||
playerEntities: () => liveEntities.MaterializedWorldEntities,
|
|
||||||
spatialQuery: () => worldState);
|
spatialQuery: () => worldState);
|
||||||
bindings.Adopt(
|
bindings.Adopt(
|
||||||
"radar snapshot",
|
"radar snapshot",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
||||||
|
|
||||||
public void BindProjectionRemoved(
|
public void BindProjectionRemoved(
|
||||||
EquippedChildRenderController source,
|
EquippedChildRenderController source,
|
||||||
Action<uint> handler)
|
Action<LiveEntityRecord> handler)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(source);
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
ArgumentNullException.ThrowIfNull(handler);
|
ArgumentNullException.ThrowIfNull(handler);
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,11 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.WorldAvailability,
|
live.WorldAvailability,
|
||||||
d.Selection,
|
d.Selection,
|
||||||
live.WorldState,
|
live.WorldState,
|
||||||
|
guid => live.LiveEntities.TryGetLocalEntityId(
|
||||||
|
guid,
|
||||||
|
out uint localEntityId)
|
||||||
|
? localEntityId
|
||||||
|
: null,
|
||||||
content.Audio?.Engine);
|
content.Audio?.Engine);
|
||||||
var compositeWarmupSource =
|
var compositeWarmupSource =
|
||||||
new CompositeWarmupEntitySource(live.WorldState);
|
new CompositeWarmupEntitySource(live.WorldState);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ internal sealed class LocalPlayerAnimationController
|
||||||
output.Reset();
|
output.Reset();
|
||||||
|
|
||||||
uint playerGuid = _identity.ServerGuid;
|
uint playerGuid = _identity.ServerGuid;
|
||||||
if (!_liveEntities.MaterializedWorldEntities.TryGetValue(playerGuid, out var entity)
|
if (!_liveEntities.TryGetWorldEntity(playerGuid, out var entity)
|
||||||
|| !_animations.TryGetValue(entity.Id, out var animation)
|
|| !_animations.TryGetValue(entity.Id, out var animation)
|
||||||
|| animation.Sequencer is not { } sequencer
|
|| animation.Sequencer is not { } sequencer
|
||||||
|| !_liveEntities.ShouldAdvanceRootRuntime(playerGuid)
|
|| !_liveEntities.ShouldAdvanceRootRuntime(playerGuid)
|
||||||
|
|
@ -65,7 +65,7 @@ internal sealed class LocalPlayerAnimationController
|
||||||
public void CaptureHooks()
|
public void CaptureHooks()
|
||||||
{
|
{
|
||||||
uint playerGuid = _identity.ServerGuid;
|
uint playerGuid = _identity.ServerGuid;
|
||||||
if (_liveEntities.MaterializedWorldEntities.TryGetValue(playerGuid, out var entity)
|
if (_liveEntities.TryGetWorldEntity(playerGuid, out var entity)
|
||||||
&& _animations.TryGetValue(entity.Id, out var animation)
|
&& _animations.TryGetValue(entity.Id, out var animation)
|
||||||
&& animation.Sequencer is { } sequencer)
|
&& animation.Sequencer is { } sequencer)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ internal sealed class LivePlayerModeAutoEntryContext
|
||||||
public bool IsLiveInWorld => _session.IsInWorld;
|
public bool IsLiveInWorld => _session.IsInWorld;
|
||||||
|
|
||||||
public bool IsPlayerEntityPresent =>
|
public bool IsPlayerEntityPresent =>
|
||||||
_liveEntities.MaterializedWorldEntities.ContainsKey(_identity.ServerGuid);
|
_liveEntities.ContainsWorldEntity(_identity.ServerGuid);
|
||||||
|
|
||||||
public bool IsPlayerControllerReady => true;
|
public bool IsPlayerControllerReady => true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ internal sealed class PlayerModeController :
|
||||||
private bool TryEnter(string loggingTag)
|
private bool TryEnter(string loggingTag)
|
||||||
{
|
{
|
||||||
uint playerGuid = _identity.ServerGuid;
|
uint playerGuid = _identity.ServerGuid;
|
||||||
if (!_liveEntities.MaterializedWorldEntities.TryGetValue(
|
if (!_liveEntities.TryGetWorldEntity(
|
||||||
playerGuid,
|
playerGuid,
|
||||||
out WorldEntity? playerEntity))
|
out WorldEntity? playerEntity))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,10 @@ internal sealed class WorldSelectionQuery
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ClosestCombatTarget? best = null;
|
ClosestCombatTarget? best = null;
|
||||||
foreach ((uint guid, WorldEntity entity) in _liveEntities.WorldEntities)
|
foreach (LiveEntityRecord record in _liveEntities.VisibleRecords)
|
||||||
{
|
{
|
||||||
|
uint guid = record.ServerGuid;
|
||||||
|
WorldEntity entity = record.WorldEntity!;
|
||||||
if (!IsHostileMonster(guid))
|
if (!IsHostileMonster(guid))
|
||||||
continue;
|
continue;
|
||||||
float distanceSquared = Vector3.DistanceSquared(entity.Position, player.Position);
|
float distanceSquared = Vector3.DistanceSquared(entity.Position, player.Position);
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,6 @@ internal sealed class LiveEntityMotionRuntimeController
|
||||||
private readonly SelectionState _selection;
|
private readonly SelectionState _selection;
|
||||||
private readonly LiveWorldOriginState _origin;
|
private readonly LiveWorldOriginState _origin;
|
||||||
|
|
||||||
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
|
||||||
_liveEntities.MaterializedWorldEntities;
|
|
||||||
private IReadOnlyDictionary<uint, WorldEntity> _visibleEntitiesByServerGuid =>
|
|
||||||
_liveEntities.WorldEntities;
|
|
||||||
|
|
||||||
public LiveEntityMotionRuntimeController(
|
public LiveEntityMotionRuntimeController(
|
||||||
LiveEntityRuntime liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
PhysicsDataCache physicsDataCache,
|
PhysicsDataCache physicsDataCache,
|
||||||
|
|
@ -101,7 +96,7 @@ internal sealed class LiveEntityMotionRuntimeController
|
||||||
// (own side) and StickyManager::adjust_offset's own-radius gap term.
|
// (own side) and StickyManager::adjust_offset's own-radius gap term.
|
||||||
// Replaces the R4 `() => 0f` pins ("setup cylsphere radius lands with
|
// Replaces the R4 `() => 0f` pins ("setup cylsphere radius lands with
|
||||||
// R5-V3").
|
// R5-V3").
|
||||||
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var selfEntity))
|
if (!_liveEntities.TryGetWorldEntity(serverGuid, out var selfEntity))
|
||||||
return rm.Sink;
|
return rm.Sink;
|
||||||
// R5-V2: forward-declared so the MoveToManager's target seams route
|
// R5-V2: forward-declared so the MoveToManager's target seams route
|
||||||
// into the entity's TargetManager (retail CPhysicsObj::set_target →
|
// into the entity's TargetManager (retail CPhysicsObj::set_target →
|
||||||
|
|
@ -237,7 +232,7 @@ internal sealed class LiveEntityMotionRuntimeController
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[autowalk-host-miss] object=0x{id:X8} "
|
$"[autowalk-host-miss] object=0x{id:X8} "
|
||||||
+ $"materialized={_entitiesByServerGuid.ContainsKey(id)} "
|
+ $"materialized={_liveEntities.ContainsWorldEntity(id)} "
|
||||||
+ $"registered={liveEntities.TryGetPhysicsHost(id, out _)} "
|
+ $"registered={liveEntities.TryGetPhysicsHost(id, out _)} "
|
||||||
+ $"hidden={liveEntities.IsHidden(id)}");
|
+ $"hidden={liveEntities.IsHidden(id)}");
|
||||||
}
|
}
|
||||||
|
|
@ -454,7 +449,9 @@ internal sealed class LiveEntityMotionRuntimeController
|
||||||
string target = turnPath.TargetGuid is { } targetGuid
|
string target = turnPath.TargetGuid is { } targetGuid
|
||||||
? $"0x{targetGuid:X8}" : "null";
|
? $"0x{targetGuid:X8}" : "null";
|
||||||
bool targetVisible = turnPath.TargetGuid is { } visibleGuid
|
bool targetVisible = turnPath.TargetGuid is { } visibleGuid
|
||||||
&& _visibleEntitiesByServerGuid.ContainsKey(visibleGuid);
|
&& _liveEntities.TryGetInteractionEligibleEntity(
|
||||||
|
visibleGuid,
|
||||||
|
out _);
|
||||||
bool targetHost = turnPath.TargetGuid is { } hostGuid
|
bool targetHost = turnPath.TargetGuid is { } hostGuid
|
||||||
&& _liveEntities?.TryGetPhysicsHost(hostGuid, out _) == true;
|
&& _liveEntities?.TryGetPhysicsHost(hostGuid, out _) == true;
|
||||||
var moveTo = movement.MoveTo;
|
var moveTo = movement.MoveTo;
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,6 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
|
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
|
||||||
private uint _playerServerGuid => _playerIdentity.ServerGuid;
|
private uint _playerServerGuid => _playerIdentity.ServerGuid;
|
||||||
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
|
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
|
||||||
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
|
||||||
_liveEntities.MaterializedWorldEntities;
|
|
||||||
private IReadOnlyDictionary<uint, WorldEntity> _visibleEntitiesByServerGuid =>
|
|
||||||
_liveEntities.WorldEntities;
|
|
||||||
|
|
||||||
internal uint? LastLivePlayerLandblockId =>
|
internal uint? LastLivePlayerLandblockId =>
|
||||||
_authorityGate.LastLivePlayerLandblockId;
|
_authorityGate.LastLivePlayerLandblockId;
|
||||||
|
|
||||||
|
|
@ -262,7 +257,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
ulong acceptedMovementVelocityAuthorityVersion =
|
ulong acceptedMovementVelocityAuthorityVersion =
|
||||||
accepted.VelocityAuthorityVersion;
|
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))
|
if (!_animatedEntities.TryGetValue(entity.Id, out var ae))
|
||||||
{
|
{
|
||||||
DispatchRemoteInboundMotion(
|
DispatchRemoteInboundMotion(
|
||||||
|
|
@ -566,8 +561,14 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
// window from this transition. Without this, the entity
|
// window from this transition. Without this, the entity
|
||||||
// starts its run animation and is instantly interrupted.
|
// starts its run animation and is instantly interrupted.
|
||||||
var refreshedTime = System.DateTime.UtcNow;
|
var refreshedTime = System.DateTime.UtcNow;
|
||||||
if (_remoteMovementObservations.TryGetValue(update.Guid, out var prev))
|
if (acceptedMotionRecord.ProjectionKey is { } motionKey
|
||||||
_remoteMovementObservations[update.Guid] = (prev.Pos, refreshedTime);
|
&& _remoteMovementObservations.TryGetValue(
|
||||||
|
motionKey,
|
||||||
|
out var prev))
|
||||||
|
{
|
||||||
|
_remoteMovementObservations[motionKey] =
|
||||||
|
(prev.Pos, refreshedTime);
|
||||||
|
}
|
||||||
if (_remoteDeadReckon.TryGetValue(update.Guid, out var dr))
|
if (_remoteDeadReckon.TryGetValue(update.Guid, out var dr))
|
||||||
dr.LastServerPosTime = (refreshedTime - System.DateTime.UnixEpoch).TotalSeconds;
|
dr.LastServerPosTime = (refreshedTime - System.DateTime.UnixEpoch).TotalSeconds;
|
||||||
}
|
}
|
||||||
|
|
@ -817,7 +818,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
ulong acceptedVectorAuthorityVersion,
|
ulong acceptedVectorAuthorityVersion,
|
||||||
ulong acceptedVectorVelocityAuthorityVersion)
|
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 (update.Guid == _playerServerGuid) return; // local jump uses our own physics
|
||||||
if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rm)) return;
|
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
|
// state-derived velocity write (adaptation note: retail's
|
||||||
// equivalence comes from the per-tick transition-sweep order —
|
// equivalence comes from the per-tick transition-sweep order —
|
||||||
// R6 scope).
|
// 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)
|
&& _animatedEntities.TryGetValue(ent.Id, out var ae)
|
||||||
&& ae.Sequencer is not null)
|
&& ae.Sequencer is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -940,12 +941,12 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
if (parsed.Guid == _playerServerGuid)
|
if (parsed.Guid == _playerServerGuid)
|
||||||
_playerController?.ApplyPhysicsState(record.FinalPhysicsState);
|
_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
|
// L.2g slice 1c (2026-05-13): the server addresses entities by
|
||||||
// ServerGuid (parsed.Guid, e.g. 0x7A9B4015), but
|
// ServerGuid (parsed.Guid, e.g. 0x7A9B4015), but
|
||||||
// ShadowObjectRegistry's cell index is keyed by local entity.Id
|
// 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
|
// mutating the registry — otherwise the lookup misses and the
|
||||||
// state flip silently no-ops, leaving doors blocked even though
|
// state flip silently no-ops, leaving doors blocked even though
|
||||||
// ACE flipped the ETHEREAL bit.
|
// ACE flipped the ETHEREAL bit.
|
||||||
|
|
@ -1035,7 +1036,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
_playerController)))
|
_playerController)))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_entitiesByServerGuid.ContainsKey(update.Guid))
|
if (!_liveEntities.ContainsWorldEntity(update.Guid))
|
||||||
{
|
{
|
||||||
if (!IsCurrentPositionOwner())
|
if (!IsCurrentPositionOwner())
|
||||||
return;
|
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))
|
if (!IsCurrentPositionOwner(entity))
|
||||||
return;
|
return;
|
||||||
_entityEffects?.MarkLiveOwnerPoseDirty(update.Guid);
|
_entityEffects?.MarkLiveOwnerPoseDirty(update.Guid);
|
||||||
|
|
@ -1223,16 +1224,20 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
if (update.Guid != _playerServerGuid)
|
if (update.Guid != _playerServerGuid)
|
||||||
{
|
{
|
||||||
var now = System.DateTime.UtcNow;
|
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);
|
float moveDist = System.Numerics.Vector3.Distance(prev.Pos, worldPos);
|
||||||
if (moveDist > 0.05f)
|
if (moveDist > 0.05f)
|
||||||
_remoteMovementObservations[update.Guid] = (worldPos, now);
|
_remoteMovementObservations[positionKey] = (worldPos, now);
|
||||||
// else: leave old entry so "Time" = last real movement time
|
// else: leave old entry so "Time" = last real movement time
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_remoteMovementObservations[update.Guid] = (worldPos, now);
|
_remoteMovementObservations[positionKey] = (worldPos, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retail-faithful hard-snap on UpdatePosition.
|
// Retail-faithful hard-snap on UpdatePosition.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Physics;
|
namespace AcDream.App.Physics;
|
||||||
|
|
||||||
|
|
@ -9,19 +10,20 @@ namespace AcDream.App.Physics;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RemoteMovementObservationTracker
|
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(
|
internal bool TryGetValue(
|
||||||
uint serverGuid,
|
RuntimeEntityKey key,
|
||||||
out (Vector3 Pos, DateTime Time) observation) =>
|
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();
|
internal void Clear() => _lastMove.Clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AcDream.App.Rendering;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Physics;
|
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, bool> _completeAuthoritativePlacement;
|
||||||
private readonly Action<uint, ushort> _beginAuthoritativePlacement;
|
private readonly Action<uint, ushort> _beginAuthoritativePlacement;
|
||||||
private readonly PlacementResolver _resolvePlacement;
|
private readonly PlacementResolver _resolvePlacement;
|
||||||
private readonly Dictionary<uint, PendingPlacement> _pending = new();
|
private readonly Dictionary<RuntimeEntityKey, PendingPlacement> _pending = new();
|
||||||
|
|
||||||
internal RemoteTeleportController(
|
internal RemoteTeleportController(
|
||||||
PhysicsEngine physics,
|
PhysicsEngine physics,
|
||||||
|
|
@ -178,9 +179,9 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
bool wasInContact = liveRecord.PhysicsBody.InContact;
|
bool wasInContact = liveRecord.PhysicsBody.InContact;
|
||||||
bool wasOnWalkable = liveRecord.PhysicsBody.OnWalkable;
|
bool wasOnWalkable = liveRecord.PhysicsBody.OnWalkable;
|
||||||
RollbackPlacement rollback = CaptureRollback(remote, liveRecord.PhysicsBody);
|
RollbackPlacement rollback = CaptureRollback(remote, liveRecord.PhysicsBody);
|
||||||
if (_pending.TryGetValue(entity.ServerGuid, out PendingPlacement prior)
|
RuntimeEntityKey key = RequireProjectionKey(liveRecord);
|
||||||
&& ReferenceEquals(prior.Record, liveRecord)
|
if (_pending.TryGetValue(key, out PendingPlacement prior)
|
||||||
&& prior.Generation == generation)
|
&& ReferenceEquals(prior.Record, liveRecord))
|
||||||
{
|
{
|
||||||
wasInContact = prior.WasInContact;
|
wasInContact = prior.WasInContact;
|
||||||
wasOnWalkable = prior.WasOnWalkable;
|
wasOnWalkable = prior.WasOnWalkable;
|
||||||
|
|
@ -219,7 +220,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
return Superseded(request);
|
return Superseded(request);
|
||||||
if (!placement.Ok)
|
if (!placement.Ok)
|
||||||
{
|
{
|
||||||
_pending.Remove(entity.ServerGuid);
|
_pending.Remove(key);
|
||||||
if (!RollBackDeferredPlacement(request))
|
if (!RollBackDeferredPlacement(request))
|
||||||
return Superseded(request);
|
return Superseded(request);
|
||||||
return new Result(
|
return new Result(
|
||||||
|
|
@ -230,24 +231,18 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
request.Body.Orientation);
|
request.Body.Orientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
_pending.Remove(entity.ServerGuid);
|
_pending.Remove(key);
|
||||||
return CommitResolved(request, placement);
|
return CommitResolved(request, placement);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Forget(uint serverGuid)
|
|
||||||
{
|
|
||||||
_pending.Remove(serverGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Forget(LiveEntityRecord record)
|
internal void Forget(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
if (_pending.TryGetValue(record.ServerGuid, out PendingPlacement pending)
|
if (record.ProjectionKey is { } key
|
||||||
&& pending.Generation == record.Generation
|
&& _pending.TryGetValue(key, out PendingPlacement pending)
|
||||||
&& (record.WorldEntity is null
|
&& ReferenceEquals(pending.Record, record))
|
||||||
|| ReferenceEquals(pending.Entity, record.WorldEntity)))
|
|
||||||
{
|
{
|
||||||
_pending.Remove(record.ServerGuid);
|
_pending.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +251,10 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
_pending.Clear();
|
_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;
|
internal int PendingPlacementCount => _pending.Count;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -404,7 +402,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
request.Entity.Rotation = request.RequestedOrientation;
|
request.Entity.Rotation = request.RequestedOrientation;
|
||||||
if (!IsCurrent(request))
|
if (!IsCurrent(request))
|
||||||
return false;
|
return false;
|
||||||
_pending[request.Entity.ServerGuid] = request;
|
_pending[RequireProjectionKey(request.Record)] = request;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,14 +411,15 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
if (!visible)
|
if (!visible)
|
||||||
return;
|
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
|
if (record.WorldEntity is null
|
||||||
|| !ReferenceEquals(record, pending.Record)
|
|| !ReferenceEquals(record, pending.Record)
|
||||||
|| !ReferenceEquals(record.WorldEntity, pending.Entity)
|
|| !ReferenceEquals(record.WorldEntity, pending.Entity)
|
||||||
|| record.Generation != pending.Generation)
|
|| record.Generation != pending.Generation)
|
||||||
{
|
{
|
||||||
_pending.Remove(record.ServerGuid);
|
_pending.Remove(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (record.RemoteMotionRuntime is not ILiveEntityRemotePlacementRuntime currentRemote
|
if (record.RemoteMotionRuntime is not ILiveEntityRemotePlacementRuntime currentRemote
|
||||||
|
|
@ -428,7 +427,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
|| !ReferenceEquals(record.PhysicsBody, pending.Body)
|
|| !ReferenceEquals(record.PhysicsBody, pending.Body)
|
||||||
|| !ReferenceEquals(currentRemote.Body, record.PhysicsBody))
|
|| !ReferenceEquals(currentRemote.Body, record.PhysicsBody))
|
||||||
{
|
{
|
||||||
_pending.Remove(record.ServerGuid);
|
_pending.Remove(key);
|
||||||
if (record.RemoteMotionRuntime is not null
|
if (record.RemoteMotionRuntime is not null
|
||||||
&& (record.PhysicsBody is null
|
&& (record.PhysicsBody is null
|
||||||
|| !ReferenceEquals(record.RemoteMotionRuntime.Body, record.PhysicsBody)))
|
|| !ReferenceEquals(record.RemoteMotionRuntime.Body, record.PhysicsBody)))
|
||||||
|
|
@ -441,7 +440,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
if (!ReferenceEquals(currentRemote, pending.Remote))
|
if (!ReferenceEquals(currentRemote, pending.Remote))
|
||||||
{
|
{
|
||||||
pending = pending with { Remote = currentRemote };
|
pending = pending with { Remote = currentRemote };
|
||||||
_pending[record.ServerGuid] = pending;
|
_pending[key] = pending;
|
||||||
}
|
}
|
||||||
if (!_liveEntities.IsCurrentPositionAuthority(
|
if (!_liveEntities.IsCurrentPositionAuthority(
|
||||||
pending.Record,
|
pending.Record,
|
||||||
|
|
@ -454,7 +453,7 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pending.Remove(record.ServerGuid);
|
_pending.Remove(key);
|
||||||
ResolveResult placement = Resolve(pending);
|
ResolveResult placement = Resolve(pending);
|
||||||
if (!IsCurrent(pending))
|
if (!IsCurrent(pending))
|
||||||
return;
|
return;
|
||||||
|
|
@ -588,4 +587,11 @@ internal sealed class RemoteTeleportController : IDisposable
|
||||||
|
|
||||||
private static bool IsPlayerGuid(uint guid) =>
|
private static bool IsPlayerGuid(uint guid) =>
|
||||||
(guid & 0xFF000000u) == 0x50000000u;
|
(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.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,25 @@ namespace AcDream.App.Rendering;
|
||||||
/// those projections only after traversal, so dictionary enumeration remains
|
/// those projections only after traversal, so dictionary enumeration remains
|
||||||
/// stable.
|
/// stable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AttachmentUpdateOrder<TChild>
|
internal sealed class AttachmentUpdateOrder<TKey, TChild>
|
||||||
|
where TKey : struct
|
||||||
{
|
{
|
||||||
private readonly HashSet<uint> _visiting = new();
|
private readonly HashSet<TKey> _visiting = new();
|
||||||
private readonly HashSet<uint> _completed = new();
|
private readonly HashSet<TKey> _completed = new();
|
||||||
private readonly HashSet<uint> _failedSet = new();
|
private readonly HashSet<TKey> _failedSet = new();
|
||||||
private readonly List<uint> _failed = new();
|
private readonly List<TKey> _failed = new();
|
||||||
private readonly List<uint> _subtree = new();
|
private readonly List<TKey> _subtree = new();
|
||||||
private readonly Queue<uint> _recoveryQueue = new();
|
private readonly Queue<TKey> _recoveryQueue = new();
|
||||||
private readonly HashSet<uint> _recoveryVisited = new();
|
private readonly HashSet<TKey> _recoveryVisited = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the current attachment map without per-frame delegate or
|
/// Updates the current attachment map without per-frame delegate or
|
||||||
/// enumerator boxing. The returned list is borrowed until the next call.
|
/// enumerator boxing. The returned list is borrowed until the next call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<uint> ForEachParentFirst(
|
public IReadOnlyList<TKey> ForEachParentFirst(
|
||||||
Dictionary<uint, TChild> children,
|
Dictionary<TKey, TChild> children,
|
||||||
Func<TChild, uint?> parentOf,
|
Func<TChild, TKey?> parentOf,
|
||||||
Func<uint, bool> update)
|
Func<TKey, bool> update)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(children);
|
ArgumentNullException.ThrowIfNull(children);
|
||||||
ArgumentNullException.ThrowIfNull(parentOf);
|
ArgumentNullException.ThrowIfNull(parentOf);
|
||||||
|
|
@ -34,7 +35,7 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
||||||
_completed.Clear();
|
_completed.Clear();
|
||||||
_failedSet.Clear();
|
_failedSet.Clear();
|
||||||
_failed.Clear();
|
_failed.Clear();
|
||||||
foreach (uint childId in children.Keys)
|
foreach (TKey childId in children.Keys)
|
||||||
Visit(childId, children, parentOf, update);
|
Visit(childId, children, parentOf, update);
|
||||||
return _failed;
|
return _failed;
|
||||||
}
|
}
|
||||||
|
|
@ -44,10 +45,10 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
||||||
/// withdrawn without leaving a child rooted at a stale intermediate pose.
|
/// withdrawn without leaving a child rooted at a stale intermediate pose.
|
||||||
/// The returned list is borrowed until the next call.
|
/// The returned list is borrowed until the next call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<uint> CollectSubtreePostOrder(
|
public IReadOnlyList<TKey> CollectSubtreePostOrder(
|
||||||
Dictionary<uint, TChild> children,
|
Dictionary<TKey, TChild> children,
|
||||||
uint rootId,
|
TKey rootId,
|
||||||
Func<TChild, uint?> parentOf)
|
Func<TChild, TKey?> parentOf)
|
||||||
{
|
{
|
||||||
_subtree.Clear();
|
_subtree.Clear();
|
||||||
_visiting.Clear();
|
_visiting.Clear();
|
||||||
|
|
@ -61,9 +62,9 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
||||||
/// so A→B→C recovers in one parent-first drain.
|
/// so A→B→C recovers in one parent-first drain.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RealizeDescendants(
|
public void RealizeDescendants(
|
||||||
uint parentId,
|
TKey parentId,
|
||||||
Func<uint, IReadOnlyList<uint>> childrenWaitingForParent,
|
Func<TKey, IReadOnlyList<TKey>> childrenWaitingForParent,
|
||||||
Func<uint, bool> realize)
|
Func<TKey, bool> realize)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(childrenWaitingForParent);
|
ArgumentNullException.ThrowIfNull(childrenWaitingForParent);
|
||||||
ArgumentNullException.ThrowIfNull(realize);
|
ArgumentNullException.ThrowIfNull(realize);
|
||||||
|
|
@ -74,11 +75,11 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
||||||
_recoveryVisited.Add(parentId);
|
_recoveryVisited.Add(parentId);
|
||||||
while (_recoveryQueue.Count > 0)
|
while (_recoveryQueue.Count > 0)
|
||||||
{
|
{
|
||||||
uint currentParent = _recoveryQueue.Dequeue();
|
TKey currentParent = _recoveryQueue.Dequeue();
|
||||||
IReadOnlyList<uint> waiting = childrenWaitingForParent(currentParent);
|
IReadOnlyList<TKey> waiting = childrenWaitingForParent(currentParent);
|
||||||
for (int i = 0; i < waiting.Count; i++)
|
for (int i = 0; i < waiting.Count; i++)
|
||||||
{
|
{
|
||||||
uint childId = waiting[i];
|
TKey childId = waiting[i];
|
||||||
if (realize(childId) && _recoveryVisited.Add(childId))
|
if (realize(childId) && _recoveryVisited.Add(childId))
|
||||||
_recoveryQueue.Enqueue(childId);
|
_recoveryQueue.Enqueue(childId);
|
||||||
}
|
}
|
||||||
|
|
@ -86,25 +87,26 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Collect(
|
private void Collect(
|
||||||
uint rootId,
|
TKey rootId,
|
||||||
Dictionary<uint, TChild> children,
|
Dictionary<TKey, TChild> children,
|
||||||
Func<TChild, uint?> parentOf)
|
Func<TChild, TKey?> parentOf)
|
||||||
{
|
{
|
||||||
if (!_visiting.Add(rootId))
|
if (!_visiting.Add(rootId))
|
||||||
return;
|
return;
|
||||||
foreach ((uint candidateId, TChild child) in children)
|
foreach ((TKey candidateId, TChild child) in children)
|
||||||
{
|
{
|
||||||
if (parentOf(child) == rootId)
|
if (parentOf(child) is { } parent
|
||||||
|
&& EqualityComparer<TKey>.Default.Equals(parent, rootId))
|
||||||
Collect(candidateId, children, parentOf);
|
Collect(candidateId, children, parentOf);
|
||||||
}
|
}
|
||||||
_subtree.Add(rootId);
|
_subtree.Add(rootId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Visit(
|
private void Visit(
|
||||||
uint childId,
|
TKey childId,
|
||||||
Dictionary<uint, TChild> children,
|
Dictionary<TKey, TChild> children,
|
||||||
Func<TChild, uint?> parentOf,
|
Func<TChild, TKey?> parentOf,
|
||||||
Func<uint, bool> update)
|
Func<TKey, bool> update)
|
||||||
{
|
{
|
||||||
if (_completed.Contains(childId)
|
if (_completed.Contains(childId)
|
||||||
|| !children.TryGetValue(childId, out TChild? child))
|
|| !children.TryGetValue(childId, out TChild? child))
|
||||||
|
|
@ -114,7 +116,7 @@ internal sealed class AttachmentUpdateOrder<TChild>
|
||||||
if (!_visiting.Add(childId))
|
if (!_visiting.Add(childId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint? parentId = parentOf(child);
|
TKey? parentId = parentOf(child);
|
||||||
if (parentId is { } attachedParentId && children.ContainsKey(attachedParentId))
|
if (parentId is { } attachedParentId && children.ContainsKey(attachedParentId))
|
||||||
Visit(attachedParentId, children, parentOf, update);
|
Visit(attachedParentId, children, parentOf, update);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Plugins;
|
using AcDream.Core.Plugins;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -116,13 +117,13 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryMaterialize(
|
public bool TryMaterialize(
|
||||||
LiveEntityRecord expectedRecord,
|
RuntimeEntityRecord expectedCanonical,
|
||||||
WorldSession.EntitySpawn canonicalSpawn,
|
WorldSession.EntitySpawn canonicalSpawn,
|
||||||
LiveProjectionPurpose purpose,
|
LiveProjectionPurpose purpose,
|
||||||
ulong expectedCreateIntegrationVersion,
|
ulong expectedCreateIntegrationVersion,
|
||||||
LiveEntityAppearanceUpdateState? appearanceUpdate = null)
|
LiveEntityAppearanceUpdateState? appearanceUpdate = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
||||||
if (purpose is LiveProjectionPurpose.AppearanceMutation
|
if (purpose is LiveProjectionPurpose.AppearanceMutation
|
||||||
!= (appearanceUpdate is not null))
|
!= (appearanceUpdate is not null))
|
||||||
{
|
{
|
||||||
|
|
@ -131,13 +132,18 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
nameof(appearanceUpdate));
|
nameof(appearanceUpdate));
|
||||||
}
|
}
|
||||||
if (!_runtime.IsCurrentCreateIntegration(
|
if (!_runtime.IsCurrentCreateIntegration(
|
||||||
expectedRecord,
|
expectedCanonical,
|
||||||
expectedCreateIntegrationVersion)
|
expectedCreateIntegrationVersion)
|
||||||
|| canonicalSpawn.Guid != expectedRecord.ServerGuid
|
|| canonicalSpawn.Guid != expectedCanonical.ServerGuid
|
||||||
|| canonicalSpawn.InstanceSequence != expectedRecord.Generation)
|
|| canonicalSpawn.InstanceSequence != expectedCanonical.Incarnation)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
_runtime.TryGetProjection(
|
||||||
|
expectedCanonical,
|
||||||
|
out LiveEntityRecord? expectedRecord);
|
||||||
|
if (appearanceUpdate is not null && expectedRecord is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
_received++;
|
_received++;
|
||||||
bool dumpLiveSpawns = _options.DumpLiveSpawns;
|
bool dumpLiveSpawns = _options.DumpLiveSpawns;
|
||||||
|
|
@ -186,7 +192,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedRecord.HasPartArray = true;
|
_runtime.SetHasPartArray(expectedCanonical, true);
|
||||||
ushort? stanceOverride = canonicalSpawn.MotionState?.Stance;
|
ushort? stanceOverride = canonicalSpawn.MotionState?.Stance;
|
||||||
ushort? commandOverride = canonicalSpawn.MotionState?.ForwardCommand;
|
ushort? commandOverride = canonicalSpawn.MotionState?.ForwardCommand;
|
||||||
var idleCycle = MotionResolver.GetIdleCycle(
|
var idleCycle = MotionResolver.GetIdleCycle(
|
||||||
|
|
@ -327,7 +333,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
|
|
||||||
bool supersessionRecovery =
|
bool supersessionRecovery =
|
||||||
purpose is LiveProjectionPurpose.CreateSupersessionRecovery;
|
purpose is LiveProjectionPurpose.CreateSupersessionRecovery;
|
||||||
if (supersessionRecovery && expectedRecord.WorldEntity is not null)
|
if (supersessionRecovery
|
||||||
|
&& expectedRecord?.WorldEntity is not null)
|
||||||
{
|
{
|
||||||
return LiveEntityCreateSupersessionRecovery.TryApply(
|
return LiveEntityCreateSupersessionRecovery.TryApply(
|
||||||
_runtime,
|
_runtime,
|
||||||
|
|
@ -380,7 +387,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
if (appearanceUpdate is { } visualUpdate)
|
if (appearanceUpdate is { } visualUpdate)
|
||||||
{
|
{
|
||||||
return ApplyAppearance(
|
return ApplyAppearance(
|
||||||
expectedRecord,
|
expectedRecord!,
|
||||||
canonicalSpawn,
|
canonicalSpawn,
|
||||||
visualUpdate,
|
visualUpdate,
|
||||||
setup,
|
setup,
|
||||||
|
|
@ -398,6 +405,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
}
|
}
|
||||||
|
|
||||||
return MaterializeProjection(
|
return MaterializeProjection(
|
||||||
|
expectedCanonical,
|
||||||
expectedRecord,
|
expectedRecord,
|
||||||
canonicalSpawn,
|
canonicalSpawn,
|
||||||
setup,
|
setup,
|
||||||
|
|
@ -670,7 +678,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MaterializeProjection(
|
private bool MaterializeProjection(
|
||||||
LiveEntityRecord expectedRecord,
|
RuntimeEntityRecord expectedCanonical,
|
||||||
|
LiveEntityRecord? retainedRecord,
|
||||||
WorldSession.EntitySpawn spawn,
|
WorldSession.EntitySpawn spawn,
|
||||||
Setup setup,
|
Setup setup,
|
||||||
MotionResolver.IdleCycle? idleCycle,
|
MotionResolver.IdleCycle? idleCycle,
|
||||||
|
|
@ -690,17 +699,18 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
ulong expectedCreateIntegrationVersion,
|
ulong expectedCreateIntegrationVersion,
|
||||||
bool synchronizeAnimation)
|
bool synchronizeAnimation)
|
||||||
{
|
{
|
||||||
if (!_runtime.TryGetEffectProfile(spawn.Guid, out _))
|
EntityEffectProfile profile = spawn.Physics is { } physics
|
||||||
|
? EntityEffectProfile.CreateLive(setup, physics)
|
||||||
|
: EntityEffectProfile.CreateDatStatic(setup);
|
||||||
|
if (retainedRecord is not null
|
||||||
|
&& !_runtime.TryGetEffectProfile(spawn.Guid, out _))
|
||||||
{
|
{
|
||||||
EntityEffectProfile profile = spawn.Physics is { } physics
|
|
||||||
? EntityEffectProfile.CreateLive(setup, physics)
|
|
||||||
: EntityEffectProfile.CreateDatStatic(setup);
|
|
||||||
_runtime.SetEffectProfile(spawn.Guid, profile);
|
_runtime.SetEffectProfile(spawn.Guid, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool createdProjection = false;
|
bool createdProjection = false;
|
||||||
WorldEntity? entity = _runtime.MaterializeLiveEntity(
|
WorldEntity? entity = _runtime.MaterializeLiveEntity(
|
||||||
spawn.Guid,
|
expectedCanonical,
|
||||||
spawn.Position!.Value.LandblockId,
|
spawn.Position!.Value.LandblockId,
|
||||||
localId =>
|
localId =>
|
||||||
{
|
{
|
||||||
|
|
@ -721,8 +731,12 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
if (bounds.TryGet(out Vector3 minimum, out Vector3 maximum))
|
if (bounds.TryGet(out Vector3 minimum, out Vector3 maximum))
|
||||||
created.SetLocalBounds(minimum, maximum);
|
created.SetLocalBounds(minimum, maximum);
|
||||||
return created;
|
return created;
|
||||||
});
|
},
|
||||||
|
LiveEntityProjectionKind.World,
|
||||||
|
initializeProjection: record => record.EffectProfile = profile,
|
||||||
|
out LiveEntityRecord? expectedRecord);
|
||||||
if (entity is null
|
if (entity is null
|
||||||
|
|| expectedRecord is null
|
||||||
|| !_runtime.IsCurrentCreateIntegration(
|
|| !_runtime.IsCurrentCreateIntegration(
|
||||||
expectedRecord,
|
expectedRecord,
|
||||||
expectedCreateIntegrationVersion)
|
expectedCreateIntegrationVersion)
|
||||||
|
|
@ -828,7 +842,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
if (dumpLiveSpawns && _received % 20 == 0)
|
if (dumpLiveSpawns && _received % 20 == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"live: animated={_runtime.AnimationRuntimes.Count} "
|
$"live: animated={_runtime.AnimationRuntimeCount} "
|
||||||
+ $"animReject: noCycle={_noCycle} fr0={_zeroFramerate} "
|
+ $"animReject: noCycle={_noCycle} fr0={_zeroFramerate} "
|
||||||
+ $"1frame={_singleFrame} partFrames={_missingPartFrames}");
|
+ $"1frame={_singleFrame} partFrames={_missingPartFrames}");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
|
|
|
||||||
|
|
@ -41,25 +41,34 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
/// <summary>Raised after an attached projection has its composed pose.</summary>
|
/// <summary>Raised after an attached projection has its composed pose.</summary>
|
||||||
public event Action<uint>? ProjectionPoseReady;
|
public event Action<uint>? ProjectionPoseReady;
|
||||||
/// <summary>Raised when an attached projection leaves its cell presentation.</summary>
|
/// <summary>Raised when an attached projection leaves its cell presentation.</summary>
|
||||||
public event Action<uint>? ProjectionRemoved;
|
public event Action<LiveEntityRecord>? ProjectionRemoved;
|
||||||
private readonly Dictionary<uint, AttachedChild> _attachedByChild = new();
|
private readonly Dictionary<RuntimeEntityKey, AttachedChild> _attachedByChild = [];
|
||||||
private readonly Dictionary<uint, PendingUnparentTransition> _pendingUnparentByChild = new();
|
private readonly Dictionary<RuntimeEntityKey, PendingUnparentTransition>
|
||||||
private readonly Dictionary<uint, PendingOrdinaryRemoval> _pendingOrdinaryRemovalByRoot = new();
|
_pendingUnparentByChild = [];
|
||||||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingDetachedRemovalByChild = new();
|
private readonly Dictionary<RuntimeEntityKey, PendingOrdinaryRemoval>
|
||||||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingReparentRemovalByChild = new();
|
_pendingOrdinaryRemovalByRoot = [];
|
||||||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingPoseLossRemovalByChild = new();
|
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
|
||||||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingOrphanRemovalByChild = new();
|
_pendingDetachedRemovalByChild = [];
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
|
||||||
|
_pendingReparentRemovalByChild = [];
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
|
||||||
|
_pendingPoseLossRemovalByChild = [];
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, PendingProjectionSubtree>
|
||||||
|
_pendingOrphanRemovalByChild = [];
|
||||||
private readonly List<uint> _pendingProjectionChildrenScratch = new();
|
private readonly List<uint> _pendingProjectionChildrenScratch = new();
|
||||||
private readonly List<KeyValuePair<uint, PendingOrdinaryRemoval>>
|
private readonly List<KeyValuePair<RuntimeEntityKey, PendingOrdinaryRemoval>>
|
||||||
_pendingOrdinaryRemovalScratch = new();
|
_pendingOrdinaryRemovalScratch = new();
|
||||||
private readonly List<KeyValuePair<uint, PendingProjectionSubtree>>
|
private readonly List<KeyValuePair<RuntimeEntityKey, PendingProjectionSubtree>>
|
||||||
_pendingProjectionSubtreeScratch = new();
|
_pendingProjectionSubtreeScratch = new();
|
||||||
private readonly List<KeyValuePair<uint, PendingUnparentTransition>>
|
private readonly List<KeyValuePair<RuntimeEntityKey, PendingUnparentTransition>>
|
||||||
_pendingUnparentScratch = new();
|
_pendingUnparentScratch = new();
|
||||||
private readonly AttachmentUpdateOrder<AttachedChild> _updateOrder = new();
|
private readonly AttachmentUpdateOrder<RuntimeEntityKey, AttachedChild>
|
||||||
private readonly Func<AttachedChild, uint?> _parentOfAttached;
|
_updateOrder = new();
|
||||||
private readonly Func<uint, bool> _tickAttached;
|
private readonly AttachmentUpdateOrder<uint, uint> _relationRecoveryOrder =
|
||||||
private readonly Func<uint, bool> _reconcileAttached;
|
new();
|
||||||
|
private readonly Func<AttachedChild, RuntimeEntityKey?> _parentOfAttached;
|
||||||
|
private readonly Func<RuntimeEntityKey, bool> _tickAttached;
|
||||||
|
private readonly Func<RuntimeEntityKey, bool> _reconcileAttached;
|
||||||
private int _activePoseCompositionVisits;
|
private int _activePoseCompositionVisits;
|
||||||
|
|
||||||
internal int LastFullPoseCompositionVisits { get; private set; }
|
internal int LastFullPoseCompositionVisits { get; private set; }
|
||||||
|
|
@ -92,7 +101,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
_acceptParent = acceptParent ?? throw new ArgumentNullException(nameof(acceptParent));
|
_acceptParent = acceptParent ?? throw new ArgumentNullException(nameof(acceptParent));
|
||||||
_withdrawProjection = withdrawProjection
|
_withdrawProjection = withdrawProjection
|
||||||
?? throw new ArgumentNullException(nameof(withdrawProjection));
|
?? throw new ArgumentNullException(nameof(withdrawProjection));
|
||||||
_parentOfAttached = static child => child.ParentGuid;
|
_parentOfAttached = static child => child.ParentRecord.ProjectionKey;
|
||||||
_tickAttached = TickChild;
|
_tickAttached = TickChild;
|
||||||
_reconcileAttached = ReconcileChild;
|
_reconcileAttached = ReconcileChild;
|
||||||
|
|
||||||
|
|
@ -258,11 +267,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
|
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
|
||||||
{
|
{
|
||||||
_pendingUnparentByChild.Remove(childGuid);
|
|
||||||
Relations.EndChildProjection(childGuid);
|
Relations.EndChildProjection(childGuid);
|
||||||
return ChildUnparentDisposition.NotAttached;
|
return ChildUnparentDisposition.NotAttached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
var pending = new PendingUnparentTransition(
|
var pending = new PendingUnparentTransition(
|
||||||
record,
|
record,
|
||||||
record.PositionAuthorityVersion,
|
record.PositionAuthorityVersion,
|
||||||
|
|
@ -271,7 +280,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
restoreRootRelation: false,
|
restoreRootRelation: false,
|
||||||
restoreDescendantRelations: true),
|
restoreDescendantRelations: true),
|
||||||
continuation);
|
continuation);
|
||||||
return AdvanceUnparentTransition(childGuid, pending);
|
return AdvanceUnparentTransition(key, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Recompose every child after the parent's animation tick.</summary>
|
/// <summary>Recompose every child after the parent's animation tick.</summary>
|
||||||
|
|
@ -279,7 +288,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
RetryPendingProjectionTransitions();
|
RetryPendingProjectionTransitions();
|
||||||
_activePoseCompositionVisits = 0;
|
_activePoseCompositionVisits = 0;
|
||||||
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
IReadOnlyList<RuntimeEntityKey> failed = _updateOrder.ForEachParentFirst(
|
||||||
_attachedByChild,
|
_attachedByChild,
|
||||||
_parentOfAttached,
|
_parentOfAttached,
|
||||||
_tickAttached);
|
_tickAttached);
|
||||||
|
|
@ -297,7 +306,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
RetryPendingProjectionTransitions();
|
RetryPendingProjectionTransitions();
|
||||||
_activePoseCompositionVisits = 0;
|
_activePoseCompositionVisits = 0;
|
||||||
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
IReadOnlyList<RuntimeEntityKey> failed = _updateOrder.ForEachParentFirst(
|
||||||
_attachedByChild,
|
_attachedByChild,
|
||||||
_parentOfAttached,
|
_parentOfAttached,
|
||||||
_reconcileAttached);
|
_reconcileAttached);
|
||||||
|
|
@ -313,9 +322,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
_pendingOrdinaryRemovalScratch);
|
_pendingOrdinaryRemovalScratch);
|
||||||
for (int i = 0; i < _pendingOrdinaryRemovalScratch.Count; i++)
|
for (int i = 0; i < _pendingOrdinaryRemovalScratch.Count; i++)
|
||||||
{
|
{
|
||||||
(uint rootGuid, PendingOrdinaryRemoval pending) =
|
(RuntimeEntityKey rootKey, PendingOrdinaryRemoval pending) =
|
||||||
_pendingOrdinaryRemovalScratch[i];
|
_pendingOrdinaryRemovalScratch[i];
|
||||||
AdvanceOrdinaryRemoval(rootGuid, pending);
|
AdvanceOrdinaryRemoval(rootKey, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyEntries(
|
CopyEntries(
|
||||||
|
|
@ -323,9 +332,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
_pendingProjectionSubtreeScratch);
|
_pendingProjectionSubtreeScratch);
|
||||||
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
||||||
{
|
{
|
||||||
(uint childGuid, PendingProjectionSubtree pending) =
|
(RuntimeEntityKey childKey, PendingProjectionSubtree pending) =
|
||||||
_pendingProjectionSubtreeScratch[i];
|
_pendingProjectionSubtreeScratch[i];
|
||||||
AdvanceDetachedRemoval(childGuid, pending);
|
AdvanceDetachedRemoval(childKey, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
RetryProjectionSubtrees(_pendingReparentRemovalByChild);
|
RetryProjectionSubtrees(_pendingReparentRemovalByChild);
|
||||||
|
|
@ -335,16 +344,16 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
CopyEntries(_pendingUnparentByChild, _pendingUnparentScratch);
|
CopyEntries(_pendingUnparentByChild, _pendingUnparentScratch);
|
||||||
for (int i = 0; i < _pendingUnparentScratch.Count; i++)
|
for (int i = 0; i < _pendingUnparentScratch.Count; i++)
|
||||||
{
|
{
|
||||||
(uint childGuid, PendingUnparentTransition pending) =
|
(RuntimeEntityKey childKey, PendingUnparentTransition pending) =
|
||||||
_pendingUnparentScratch[i];
|
_pendingUnparentScratch[i];
|
||||||
if (!_liveEntities.IsCurrentRecord(pending.Record)
|
if (!_liveEntities.IsCurrentRecord(pending.Record)
|
||||||
|| pending.Record.PositionAuthorityVersion
|
|| pending.Record.PositionAuthorityVersion
|
||||||
!= pending.PositionAuthorityVersion)
|
!= pending.PositionAuthorityVersion)
|
||||||
{
|
{
|
||||||
_pendingUnparentByChild.Remove(childGuid);
|
_pendingUnparentByChild.Remove(childKey);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
AdvanceUnparentTransition(childGuid, pending);
|
AdvanceUnparentTransition(childKey, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
Relations.CopyPendingProjectionChildrenTo(_pendingProjectionChildrenScratch);
|
Relations.CopyPendingProjectionChildrenTo(_pendingProjectionChildrenScratch);
|
||||||
|
|
@ -353,17 +362,17 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CopyEntries<T>(
|
private static void CopyEntries<T>(
|
||||||
Dictionary<uint, T> source,
|
Dictionary<RuntimeEntityKey, T> source,
|
||||||
List<KeyValuePair<uint, T>> destination)
|
List<KeyValuePair<RuntimeEntityKey, T>> destination)
|
||||||
{
|
{
|
||||||
destination.Clear();
|
destination.Clear();
|
||||||
foreach (KeyValuePair<uint, T> entry in source)
|
foreach (KeyValuePair<RuntimeEntityKey, T> entry in source)
|
||||||
destination.Add(entry);
|
destination.Add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TickChild(uint childGuid)
|
private bool TickChild(RuntimeEntityKey childKey)
|
||||||
{
|
{
|
||||||
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
if (!_attachedByChild.TryGetValue(childKey, out AttachedChild? child))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_activePoseCompositionVisits++;
|
_activePoseCompositionVisits++;
|
||||||
|
|
@ -403,15 +412,15 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ReconcileChild(uint childGuid)
|
private bool ReconcileChild(RuntimeEntityKey childKey)
|
||||||
{
|
{
|
||||||
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child)
|
if (!_attachedByChild.TryGetValue(childKey, out AttachedChild? child)
|
||||||
|| !TryResolveExactAttachment(child, out WorldEntity parent))
|
|| !TryResolveExactAttachment(child, out WorldEntity parent))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParentPresentationMatches(child, parent) || TickChild(childGuid);
|
return ParentPresentationMatches(child, parent) || TickChild(childKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ParentPresentationMatches(
|
private bool ParentPresentationMatches(
|
||||||
|
|
@ -445,7 +454,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
if (!_liveEntities.TryGetRecord(pending.ParentGuid, out LiveEntityRecord parentRecord)
|
if (!_liveEntities.TryGetRecord(pending.ParentGuid, out LiveEntityRecord parentRecord)
|
||||||
|| parentRecord.WorldEntity is not { } parentEntity
|
|| parentRecord.WorldEntity is not { } parentEntity
|
||||||
|| !parentRecord.IsSpatiallyProjected
|
|| !parentRecord.IsSpatiallyProjected
|
||||||
|| !_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord childRecord)
|
|| !_liveEntities.TryGetCanonical(
|
||||||
|
childGuid,
|
||||||
|
out RuntimeEntityRecord childCanonical)
|
||||||
|| !_liveEntities.TryGetSnapshot(pending.ParentGuid, out WorldSession.EntitySpawn parentSpawn)
|
|| !_liveEntities.TryGetSnapshot(pending.ParentGuid, out WorldSession.EntitySpawn parentSpawn)
|
||||||
|| !_liveEntities.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn childSpawn))
|
|| !_liveEntities.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn childSpawn))
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -496,22 +507,25 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
// hydration path sees it. Install its logical effect profile before
|
// hydration path sees it. Install its logical effect profile before
|
||||||
// MaterializeLiveEntity registers runtime resources, exactly as for a
|
// MaterializeLiveEntity registers runtime resources, exactly as for a
|
||||||
// top-level projection; rebucketing/reattachment must never recreate it.
|
// top-level projection; rebucketing/reattachment must never recreate it.
|
||||||
if (!_liveEntities.TryGetEffectProfile(childGuid, out _))
|
var effectProfile = childSpawn.Physics is { } physics
|
||||||
|
? Vfx.EntityEffectProfile.CreateLive(childSetup, physics)
|
||||||
|
: Vfx.EntityEffectProfile.CreateDatStatic(childSetup);
|
||||||
|
if (_liveEntities.TryGetProjection(
|
||||||
|
childCanonical,
|
||||||
|
out LiveEntityRecord? retainedChild)
|
||||||
|
&& !_liveEntities.TryGetEffectProfile(childGuid, out _))
|
||||||
{
|
{
|
||||||
var effectProfile = childSpawn.Physics is { } physics
|
|
||||||
? Vfx.EntityEffectProfile.CreateLive(childSetup, physics)
|
|
||||||
: Vfx.EntityEffectProfile.CreateDatStatic(childSetup);
|
|
||||||
_liveEntities.SetEffectProfile(childGuid, effectProfile);
|
_liveEntities.SetEffectProfile(childGuid, effectProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_liveEntities.IsCurrentRecord(parentRecord)
|
if (!_liveEntities.IsCurrentRecord(parentRecord)
|
||||||
|| !_liveEntities.IsCurrentRecord(childRecord)
|
|| !_liveEntities.IsCurrentCanonical(childCanonical)
|
||||||
|| !Relations.IsPending(pending, candidateKind))
|
|| !Relations.IsPending(pending, candidateKind))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
WorldEntity? entity = _liveEntities.MaterializeLiveEntity(
|
WorldEntity? entity = _liveEntities.MaterializeLiveEntity(
|
||||||
childGuid,
|
childCanonical,
|
||||||
parentCellId,
|
parentCellId,
|
||||||
localId =>
|
localId =>
|
||||||
{
|
{
|
||||||
|
|
@ -529,8 +543,10 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
created.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
|
created.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
|
||||||
return created;
|
return created;
|
||||||
},
|
},
|
||||||
LiveEntityProjectionKind.Attached);
|
LiveEntityProjectionKind.Attached,
|
||||||
if (entity is null)
|
initializeProjection: record => record.EffectProfile = effectProfile,
|
||||||
|
out LiveEntityRecord? childRecord);
|
||||||
|
if (entity is null || childRecord is null)
|
||||||
return false;
|
return false;
|
||||||
if (!_liveEntities.IsCurrentRecord(parentRecord)
|
if (!_liveEntities.IsCurrentRecord(parentRecord)
|
||||||
|| !_liveEntities.IsCurrentRecord(childRecord)
|
|| !_liveEntities.IsCurrentRecord(childRecord)
|
||||||
|
|
@ -576,8 +592,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
scale,
|
scale,
|
||||||
entity);
|
entity);
|
||||||
CaptureParentPresentation(attached, parentEntity);
|
CaptureParentPresentation(attached, parentEntity);
|
||||||
_attachedByChild[childGuid] = attached;
|
RuntimeEntityKey childKey = RequireProjectionKey(childRecord);
|
||||||
_pendingUnparentByChild.Remove(childGuid);
|
_attachedByChild[childKey] = attached;
|
||||||
|
_pendingUnparentByChild.Remove(childKey);
|
||||||
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
|
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
|
||||||
{
|
{
|
||||||
// A child attached while its parent is already Hidden inherits
|
// A child attached while its parent is already Hidden inherits
|
||||||
|
|
@ -809,14 +826,15 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
ParentAttachmentRelation relation,
|
ParentAttachmentRelation relation,
|
||||||
ParentProjectionCandidateKind candidateKind)
|
ParentProjectionCandidateKind candidateKind)
|
||||||
{
|
{
|
||||||
if (!_liveEntities.TryGetRecord(
|
if (!_liveEntities.TryGetCanonical(
|
||||||
relation.ChildGuid,
|
relation.ChildGuid,
|
||||||
out LiveEntityRecord childRecord))
|
out RuntimeEntityRecord childCanonical))
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong positionAuthorityVersion = childRecord.PositionAuthorityVersion;
|
ulong positionAuthorityVersion =
|
||||||
|
childCanonical.PositionAuthorityVersion;
|
||||||
if (candidateKind is ParentProjectionCandidateKind.Staged)
|
if (candidateKind is ParentProjectionCandidateKind.Staged)
|
||||||
{
|
{
|
||||||
if (!_liveEntities.CommitStagedParent(relation, out _)
|
if (!_liveEntities.CommitStagedParent(relation, out _)
|
||||||
|
|
@ -833,9 +851,15 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
if (!Relations.IsPending(relation, candidateKind)
|
if (!Relations.IsPending(relation, candidateKind)
|
||||||
|| !_liveEntities.CommitAcceptedParentCellless(
|
|| !_liveEntities.CommitAcceptedParentCellless(
|
||||||
childRecord,
|
childCanonical,
|
||||||
positionAuthorityVersion)
|
positionAuthorityVersion))
|
||||||
|| !WithdrawPriorProjection(childRecord))
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
if (_liveEntities.TryGetProjection(
|
||||||
|
childCanonical,
|
||||||
|
out LiveEntityRecord? childRecord)
|
||||||
|
&& !WithdrawPriorProjection(childRecord))
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
@ -850,7 +874,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
if (relation.ParentGuid == relation.ChildGuid)
|
if (relation.ParentGuid == relation.ChildGuid)
|
||||||
return ParentProjectionValidationDisposition.Rejected;
|
return ParentProjectionValidationDisposition.Rejected;
|
||||||
if (!_liveEntities.TryGetRecord(relation.ParentGuid, out LiveEntityRecord parent)
|
if (!_liveEntities.TryGetRecord(relation.ParentGuid, out LiveEntityRecord parent)
|
||||||
|| !_liveEntities.TryGetRecord(relation.ChildGuid, out _)
|
|| !_liveEntities.TryGetCanonical(relation.ChildGuid, out _)
|
||||||
|| parent.WorldEntity is null
|
|| parent.WorldEntity is null
|
||||||
|| !parent.HasPartArray)
|
|| !parent.HasPartArray)
|
||||||
{
|
{
|
||||||
|
|
@ -877,7 +901,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
private void RetryWaitingDescendants(uint parentGuid)
|
private void RetryWaitingDescendants(uint parentGuid)
|
||||||
{
|
{
|
||||||
_updateOrder.RealizeDescendants(
|
_relationRecoveryOrder.RealizeDescendants(
|
||||||
parentGuid,
|
parentGuid,
|
||||||
Relations.ChildrenWaitingForParent,
|
Relations.ChildrenWaitingForParent,
|
||||||
ResolveAndTryRealize);
|
ResolveAndTryRealize);
|
||||||
|
|
@ -985,12 +1009,12 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AdvanceDetachedRemoval(
|
private void AdvanceDetachedRemoval(
|
||||||
uint childGuid,
|
RuntimeEntityKey childKey,
|
||||||
PendingProjectionSubtree pending)
|
PendingProjectionSubtree pending)
|
||||||
{
|
{
|
||||||
AdvanceProjectionSubtree(
|
AdvanceProjectionSubtree(
|
||||||
_pendingDetachedRemovalByChild,
|
_pendingDetachedRemovalByChild,
|
||||||
childGuid,
|
childKey,
|
||||||
pending);
|
pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -998,22 +1022,29 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
if (item.CurrentlyEquippedLocation == EquipMask.None)
|
if (item.CurrentlyEquippedLocation == EquipMask.None)
|
||||||
return;
|
return;
|
||||||
if (_attachedByChild.TryGetValue(item.ObjectId, out AttachedChild? attached)
|
if (!_liveEntities.TryGetRecord(
|
||||||
|
item.ObjectId,
|
||||||
|
out LiveEntityRecord record)
|
||||||
|
|| record.ProjectionKey is not { } key)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_attachedByChild.TryGetValue(key, out AttachedChild? attached)
|
||||||
&& _liveEntities.IsCurrentRecord(attached.ChildRecord)
|
&& _liveEntities.IsCurrentRecord(attached.ChildRecord)
|
||||||
&& attached.ChildRecord.IsSpatiallyProjected)
|
&& attached.ChildRecord.IsSpatiallyProjected)
|
||||||
{
|
{
|
||||||
// A component-stage failure left the exact equipped projection
|
// A component-stage failure left the exact equipped projection
|
||||||
// untouched. The authoritative rollback therefore has nothing to
|
// untouched. The authoritative rollback therefore has nothing to
|
||||||
// reconstruct and merely cancels its retained leave-world retry.
|
// reconstruct and merely cancels its retained leave-world retry.
|
||||||
_pendingDetachedRemovalByChild.Remove(item.ObjectId);
|
_pendingDetachedRemovalByChild.Remove(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_pendingDetachedRemovalByChild.TryGetValue(
|
if (_pendingDetachedRemovalByChild.TryGetValue(
|
||||||
item.ObjectId,
|
key,
|
||||||
out PendingProjectionSubtree pending))
|
out PendingProjectionSubtree pending))
|
||||||
{
|
{
|
||||||
AdvanceDetachedRemoval(item.ObjectId, pending);
|
AdvanceDetachedRemoval(key, pending);
|
||||||
if (_pendingDetachedRemovalByChild.ContainsKey(item.ObjectId))
|
if (_pendingDetachedRemovalByChild.ContainsKey(key))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1032,7 +1063,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
private bool Remove(uint childGuid)
|
private bool Remove(uint childGuid)
|
||||||
{
|
{
|
||||||
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
if (!_liveEntities.TryGetRecord(
|
||||||
|
childGuid,
|
||||||
|
out LiveEntityRecord record)
|
||||||
|
|| record.ProjectionKey is not { } key
|
||||||
|
|| !_attachedByChild.TryGetValue(key, out AttachedChild? child))
|
||||||
return false;
|
return false;
|
||||||
if (!_liveEntities.IsCurrentRecord(child.ChildRecord))
|
if (!_liveEntities.IsCurrentRecord(child.ChildRecord))
|
||||||
return CommitProjectionRemoval(child);
|
return CommitProjectionRemoval(child);
|
||||||
|
|
@ -1062,13 +1097,14 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
private bool WithdrawPriorProjection(LiveEntityRecord childRecord)
|
private bool WithdrawPriorProjection(LiveEntityRecord childRecord)
|
||||||
{
|
{
|
||||||
|
RuntimeEntityKey key = RequireProjectionKey(childRecord);
|
||||||
if (_pendingReparentRemovalByChild.TryGetValue(
|
if (_pendingReparentRemovalByChild.TryGetValue(
|
||||||
childRecord.ServerGuid,
|
key,
|
||||||
out PendingProjectionSubtree pending))
|
out PendingProjectionSubtree pending))
|
||||||
{
|
{
|
||||||
return AdvanceProjectionSubtree(
|
return AdvanceProjectionSubtree(
|
||||||
_pendingReparentRemovalByChild,
|
_pendingReparentRemovalByChild,
|
||||||
childRecord.ServerGuid,
|
key,
|
||||||
pending);
|
pending);
|
||||||
}
|
}
|
||||||
return BeginProjectionSubtreeWithdrawal(
|
return BeginProjectionSubtreeWithdrawal(
|
||||||
|
|
@ -1079,7 +1115,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChildUnparentDisposition AdvanceUnparentTransition(
|
private ChildUnparentDisposition AdvanceUnparentTransition(
|
||||||
uint childGuid,
|
RuntimeEntityKey childKey,
|
||||||
PendingUnparentTransition pending)
|
PendingUnparentTransition pending)
|
||||||
{
|
{
|
||||||
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
|
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
|
||||||
|
|
@ -1088,7 +1124,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
||||||
{
|
{
|
||||||
_pendingUnparentByChild[childGuid] = pending with { Subtree = next };
|
_pendingUnparentByChild[childKey] = pending with { Subtree = next };
|
||||||
if (outcome.Failure is not null)
|
if (outcome.Failure is not null)
|
||||||
throw outcome.Failure;
|
throw outcome.Failure;
|
||||||
return ChildUnparentDisposition.Pending;
|
return ChildUnparentDisposition.Pending;
|
||||||
|
|
@ -1096,22 +1132,22 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Superseded)
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Superseded)
|
||||||
{
|
{
|
||||||
_pendingUnparentByChild.Remove(childGuid);
|
_pendingUnparentByChild.Remove(childKey);
|
||||||
if (outcome.Failure is not null)
|
if (outcome.Failure is not null)
|
||||||
throw outcome.Failure;
|
throw outcome.Failure;
|
||||||
return ChildUnparentDisposition.Superseded;
|
return ChildUnparentDisposition.Superseded;
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingUnparentTransition awaitingContinuation = pending with { Subtree = next };
|
PendingUnparentTransition awaitingContinuation = pending with { Subtree = next };
|
||||||
_pendingUnparentByChild[childGuid] = awaitingContinuation;
|
_pendingUnparentByChild[childKey] = awaitingContinuation;
|
||||||
if (outcome.Failure is not null)
|
if (outcome.Failure is not null)
|
||||||
throw outcome.Failure;
|
throw outcome.Failure;
|
||||||
|
|
||||||
Relations.EndChildProjection(childGuid);
|
Relations.EndChildProjection(pending.Record.ServerGuid);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
awaitingContinuation.Continuation?.Invoke();
|
awaitingContinuation.Continuation?.Invoke();
|
||||||
_pendingUnparentByChild.Remove(childGuid);
|
_pendingUnparentByChild.Remove(childKey);
|
||||||
return ChildUnparentDisposition.Completed;
|
return ChildUnparentDisposition.Completed;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
@ -1124,9 +1160,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
&& awaitingContinuation.Record.ProjectionKind
|
&& awaitingContinuation.Record.ProjectionKind
|
||||||
is LiveEntityProjectionKind.World;
|
is LiveEntityProjectionKind.World;
|
||||||
if (continuationCommitted)
|
if (continuationCommitted)
|
||||||
_pendingUnparentByChild.Remove(childGuid);
|
_pendingUnparentByChild.Remove(childKey);
|
||||||
else
|
else
|
||||||
_pendingUnparentByChild[childGuid] = awaitingContinuation;
|
_pendingUnparentByChild[childKey] = awaitingContinuation;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1165,20 +1201,23 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
private bool CommitProjectionRemoval(AttachedChild child)
|
private bool CommitProjectionRemoval(AttachedChild child)
|
||||||
{
|
{
|
||||||
if (!_attachedByChild.TryGetValue(child.ChildGuid, out AttachedChild? current)
|
RuntimeEntityKey key = RequireProjectionKey(child.ChildRecord);
|
||||||
|
if (!_attachedByChild.TryGetValue(key, out AttachedChild? current)
|
||||||
|| !ReferenceEquals(current, child))
|
|| !ReferenceEquals(current, child))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_attachedByChild.Remove(child.ChildGuid);
|
_attachedByChild.Remove(key);
|
||||||
ProjectionRemoved?.Invoke(child.Entity.Id);
|
ProjectionRemoved?.Invoke(child.ChildRecord);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WithdrawForPoseLoss(uint childGuid)
|
private void WithdrawForPoseLoss(RuntimeEntityKey childKey)
|
||||||
{
|
{
|
||||||
if (!_liveEntities.TryGetRecord(childGuid, out LiveEntityRecord record))
|
if (!_liveEntities.TryGetRecord(
|
||||||
|
childKey,
|
||||||
|
out LiveEntityRecord record))
|
||||||
return;
|
return;
|
||||||
BeginProjectionSubtreeWithdrawal(
|
BeginProjectionSubtreeWithdrawal(
|
||||||
_pendingPoseLossRemovalByChild,
|
_pendingPoseLossRemovalByChild,
|
||||||
|
|
@ -1191,25 +1230,28 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
if (_liveEntities.TryGetRecord(guid, out LiveEntityRecord record))
|
if (_liveEntities.TryGetRecord(guid, out LiveEntityRecord record))
|
||||||
{
|
{
|
||||||
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
List<AttachedRemovalCapture> captures = CaptureRecordSubtreeParentFirst(record);
|
List<AttachedRemovalCapture> captures = CaptureRecordSubtreeParentFirst(record);
|
||||||
AdvanceOrdinaryRemoval(
|
AdvanceOrdinaryRemoval(
|
||||||
guid,
|
key,
|
||||||
new PendingOrdinaryRemoval(record, captures, NextIndex: 0));
|
new PendingOrdinaryRemoval(record, captures, NextIndex: 0));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_attachedByChild.TryGetValue(guid, out AttachedChild? stale))
|
AttachedChild? stale = _attachedByChild.Values.FirstOrDefault(
|
||||||
|
candidate => candidate.ChildGuid == guid);
|
||||||
|
if (stale is not null)
|
||||||
CommitProjectionRemoval(stale);
|
CommitProjectionRemoval(stale);
|
||||||
Relations.EndChildProjection(guid);
|
Relations.EndChildProjection(guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AdvanceOrdinaryRemoval(
|
private void AdvanceOrdinaryRemoval(
|
||||||
uint rootGuid,
|
RuntimeEntityKey rootKey,
|
||||||
PendingOrdinaryRemoval pending)
|
PendingOrdinaryRemoval pending)
|
||||||
{
|
{
|
||||||
if (!_liveEntities.IsCurrentRecord(pending.RootRecord))
|
if (!_liveEntities.IsCurrentRecord(pending.RootRecord))
|
||||||
{
|
{
|
||||||
_pendingOrdinaryRemovalByRoot.Remove(rootGuid);
|
_pendingOrdinaryRemovalByRoot.Remove(rootKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1219,7 +1261,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
ExactProjectionWithdrawalOutcome outcome = WithdrawCaptured(captured);
|
ExactProjectionWithdrawalOutcome outcome = WithdrawCaptured(captured);
|
||||||
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
if (outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending)
|
||||||
{
|
{
|
||||||
_pendingOrdinaryRemovalByRoot[rootGuid] = pending with { NextIndex = i };
|
_pendingOrdinaryRemovalByRoot[rootKey] = pending with { NextIndex = i };
|
||||||
if (outcome.Failure is not null)
|
if (outcome.Failure is not null)
|
||||||
throw outcome.Failure;
|
throw outcome.Failure;
|
||||||
return;
|
return;
|
||||||
|
|
@ -1233,7 +1275,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
|
|
||||||
if (outcome.Failure is not null)
|
if (outcome.Failure is not null)
|
||||||
{
|
{
|
||||||
_pendingOrdinaryRemovalByRoot[rootGuid] = pending with
|
_pendingOrdinaryRemovalByRoot[rootKey] = pending with
|
||||||
{
|
{
|
||||||
NextIndex = i + 1,
|
NextIndex = i + 1,
|
||||||
};
|
};
|
||||||
|
|
@ -1241,23 +1283,24 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingOrdinaryRemovalByRoot.Remove(rootGuid);
|
_pendingOrdinaryRemovalByRoot.Remove(rootKey);
|
||||||
Relations.EndChildProjection(rootGuid);
|
Relations.EndChildProjection(pending.RootRecord.ServerGuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TearDownRecordProjections(LiveEntityRecord record)
|
private void TearDownRecordProjections(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
if (_pendingUnparentByChild.TryGetValue(record.ServerGuid, out var pending)
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
|
if (_pendingUnparentByChild.TryGetValue(key, out var pending)
|
||||||
&& ReferenceEquals(pending.Record, record))
|
&& ReferenceEquals(pending.Record, record))
|
||||||
{
|
{
|
||||||
_pendingUnparentByChild.Remove(record.ServerGuid);
|
_pendingUnparentByChild.Remove(key);
|
||||||
}
|
}
|
||||||
if (_pendingOrdinaryRemovalByRoot.TryGetValue(
|
if (_pendingOrdinaryRemovalByRoot.TryGetValue(
|
||||||
record.ServerGuid,
|
key,
|
||||||
out PendingOrdinaryRemoval ordinary)
|
out PendingOrdinaryRemoval ordinary)
|
||||||
&& ReferenceEquals(ordinary.RootRecord, record))
|
&& ReferenceEquals(ordinary.RootRecord, record))
|
||||||
{
|
{
|
||||||
_pendingOrdinaryRemovalByRoot.Remove(record.ServerGuid);
|
_pendingOrdinaryRemovalByRoot.Remove(key);
|
||||||
}
|
}
|
||||||
RemovePendingProjectionSubtree(_pendingDetachedRemovalByChild, record);
|
RemovePendingProjectionSubtree(_pendingDetachedRemovalByChild, record);
|
||||||
RemovePendingProjectionSubtree(_pendingReparentRemovalByChild, record);
|
RemovePendingProjectionSubtree(_pendingReparentRemovalByChild, record);
|
||||||
|
|
@ -1293,7 +1336,8 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
var result = new List<AttachedRemovalCapture>();
|
var result = new List<AttachedRemovalCapture>();
|
||||||
var visited = new HashSet<LiveEntityRecord>(ReferenceEqualityComparer.Instance);
|
var visited = new HashSet<LiveEntityRecord>(ReferenceEqualityComparer.Instance);
|
||||||
if (_attachedByChild.TryGetValue(record.ServerGuid, out AttachedChild? root)
|
if (record.ProjectionKey is { } key
|
||||||
|
&& _attachedByChild.TryGetValue(key, out AttachedChild? root)
|
||||||
&& ReferenceEquals(root.ChildRecord, record))
|
&& ReferenceEquals(root.ChildRecord, record))
|
||||||
{
|
{
|
||||||
result.Add(Capture(root));
|
result.Add(Capture(root));
|
||||||
|
|
@ -1327,8 +1371,10 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
private ExactProjectionWithdrawalOutcome WithdrawCaptured(
|
private ExactProjectionWithdrawalOutcome WithdrawCaptured(
|
||||||
AttachedRemovalCapture captured)
|
AttachedRemovalCapture captured)
|
||||||
{
|
{
|
||||||
|
RuntimeEntityKey key = RequireProjectionKey(
|
||||||
|
captured.Attached.ChildRecord);
|
||||||
if (!_attachedByChild.TryGetValue(
|
if (!_attachedByChild.TryGetValue(
|
||||||
captured.Attached.ChildGuid,
|
key,
|
||||||
out AttachedChild? current)
|
out AttachedChild? current)
|
||||||
|| !ReferenceEquals(current, captured.Attached))
|
|| !ReferenceEquals(current, captured.Attached))
|
||||||
{
|
{
|
||||||
|
|
@ -1369,7 +1415,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
if (!visited.Add(record))
|
if (!visited.Add(record))
|
||||||
return;
|
return;
|
||||||
_attachedByChild.TryGetValue(record.ServerGuid, out AttachedChild? attached);
|
AttachedChild? attached = null;
|
||||||
|
if (record.ProjectionKey is { } key)
|
||||||
|
_attachedByChild.TryGetValue(key, out attached);
|
||||||
if (attached is not null && !ReferenceEquals(attached.ChildRecord, record))
|
if (attached is not null && !ReferenceEquals(attached.ChildRecord, record))
|
||||||
attached = null;
|
attached = null;
|
||||||
if (record.IsSpatiallyProjected || attached is not null)
|
if (record.IsSpatiallyProjected || attached is not null)
|
||||||
|
|
@ -1396,7 +1444,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool BeginProjectionSubtreeWithdrawal(
|
private bool BeginProjectionSubtreeWithdrawal(
|
||||||
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
|
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot,
|
||||||
LiveEntityRecord root,
|
LiveEntityRecord root,
|
||||||
bool restoreRootRelation,
|
bool restoreRootRelation,
|
||||||
bool restoreDescendantRelations)
|
bool restoreDescendantRelations)
|
||||||
|
|
@ -1405,12 +1453,15 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
root,
|
root,
|
||||||
restoreRootRelation,
|
restoreRootRelation,
|
||||||
restoreDescendantRelations);
|
restoreDescendantRelations);
|
||||||
return AdvanceProjectionSubtree(pendingByRoot, root.ServerGuid, pending);
|
return AdvanceProjectionSubtree(
|
||||||
|
pendingByRoot,
|
||||||
|
RequireProjectionKey(root),
|
||||||
|
pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool AdvanceProjectionSubtree(
|
private bool AdvanceProjectionSubtree(
|
||||||
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
|
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot,
|
||||||
uint rootGuid,
|
RuntimeEntityKey rootKey,
|
||||||
PendingProjectionSubtree pending)
|
PendingProjectionSubtree pending)
|
||||||
{
|
{
|
||||||
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
|
ExactProjectionWithdrawalOutcome outcome = AdvanceProjectionSubtree(
|
||||||
|
|
@ -1419,9 +1470,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
bool retry = outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending
|
bool retry = outcome.Disposition is ExactProjectionWithdrawalDisposition.Pending
|
||||||
|| (outcome.Failure is not null && next.NextIndex < next.Captures.Count);
|
|| (outcome.Failure is not null && next.NextIndex < next.Captures.Count);
|
||||||
if (retry)
|
if (retry)
|
||||||
pendingByRoot[rootGuid] = next;
|
pendingByRoot[rootKey] = next;
|
||||||
else
|
else
|
||||||
pendingByRoot.Remove(rootGuid);
|
pendingByRoot.Remove(rootKey);
|
||||||
if (outcome.Failure is not null)
|
if (outcome.Failure is not null)
|
||||||
throw outcome.Failure;
|
throw outcome.Failure;
|
||||||
return outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed
|
return outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed
|
||||||
|
|
@ -1508,27 +1559,29 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RetryProjectionSubtrees(
|
private void RetryProjectionSubtrees(
|
||||||
Dictionary<uint, PendingProjectionSubtree> pendingByRoot)
|
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot)
|
||||||
{
|
{
|
||||||
CopyEntries(pendingByRoot, _pendingProjectionSubtreeScratch);
|
CopyEntries(pendingByRoot, _pendingProjectionSubtreeScratch);
|
||||||
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
||||||
{
|
{
|
||||||
(uint rootGuid, PendingProjectionSubtree pending) =
|
(RuntimeEntityKey rootKey, PendingProjectionSubtree pending) =
|
||||||
_pendingProjectionSubtreeScratch[i];
|
_pendingProjectionSubtreeScratch[i];
|
||||||
AdvanceProjectionSubtree(pendingByRoot, rootGuid, pending);
|
AdvanceProjectionSubtree(pendingByRoot, rootKey, pending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemovePendingProjectionSubtree(
|
private static void RemovePendingProjectionSubtree(
|
||||||
Dictionary<uint, PendingProjectionSubtree> pendingByRoot,
|
Dictionary<RuntimeEntityKey, PendingProjectionSubtree> pendingByRoot,
|
||||||
LiveEntityRecord record)
|
LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
uint[] keys = pendingByRoot
|
if (record.ProjectionKey is { } key
|
||||||
.Where(pair => ReferenceEquals(pair.Value.RootRecord, record))
|
&& pendingByRoot.TryGetValue(
|
||||||
.Select(pair => pair.Key)
|
key,
|
||||||
.ToArray();
|
out PendingProjectionSubtree pending)
|
||||||
for (int i = 0; i < keys.Length; i++)
|
&& ReferenceEquals(pending.RootRecord, record))
|
||||||
pendingByRoot.Remove(keys[i]);
|
{
|
||||||
|
pendingByRoot.Remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
|
|
@ -1606,6 +1659,13 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
LiveEntityRecord RootRecord,
|
LiveEntityRecord RootRecord,
|
||||||
IReadOnlyList<AttachedRemovalCapture> Captures,
|
IReadOnlyList<AttachedRemovalCapture> Captures,
|
||||||
int NextIndex);
|
int NextIndex);
|
||||||
|
|
||||||
|
private static RuntimeEntityKey RequireProjectionKey(
|
||||||
|
LiveEntityRecord record) =>
|
||||||
|
record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||||
|
"has no exact projection key.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChildUnparentDisposition
|
public enum ChildUnparentDisposition
|
||||||
|
|
|
||||||
|
|
@ -524,15 +524,8 @@ public sealed class GameWindow :
|
||||||
/// projection, including live objects parked in pending landblocks;
|
/// projection, including live objects parked in pending landblocks;
|
||||||
/// visibility must never suppress authoritative mutation.
|
/// visibility must never suppress authoritative mutation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> EmptyLiveEntityMap =
|
|
||||||
new Dictionary<uint, AcDream.Core.World.WorldEntity>();
|
|
||||||
private static readonly IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> EmptyLiveSpawnMap =
|
private static readonly IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> EmptyLiveSpawnMap =
|
||||||
new Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn>();
|
new Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn>();
|
||||||
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _entitiesByServerGuid =>
|
|
||||||
_liveEntities?.MaterializedWorldEntities ?? EmptyLiveEntityMap;
|
|
||||||
/// <summary>Visible-only view for radar, picking, status, and targets.</summary>
|
|
||||||
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _visibleEntitiesByServerGuid =>
|
|
||||||
_liveEntities?.WorldEntities ?? EmptyLiveEntityMap;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Latest <see cref="AcDream.Core.Net.WorldSession.EntitySpawn"/> for each
|
/// Latest <see cref="AcDream.Core.Net.WorldSession.EntitySpawn"/> for each
|
||||||
/// guid. Captured before the renderability gate so no-position inventory /
|
/// guid. Captured before the renderability gate so no-position inventory /
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
|
@ -84,7 +85,7 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Present(
|
public void Present(
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules)
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(schedules);
|
ArgumentNullException.ThrowIfNull(schedules);
|
||||||
LiveEntityRuntime runtime = _liveEntities;
|
LiveEntityRuntime runtime = _liveEntities;
|
||||||
|
|
@ -107,7 +108,9 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
if (!TryGetCurrent(runtime, record, entity, animation))
|
if (!TryGetCurrent(runtime, record, entity, animation))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
schedules.TryGetValue(entity.Id, out LiveEntityAnimationSchedule schedule);
|
LiveEntityAnimationSchedule schedule = default;
|
||||||
|
if (record.ProjectionKey is { } key)
|
||||||
|
schedules.TryGetValue(key, out schedule);
|
||||||
bool hasOrdinarySchedule = IsCurrentSchedule(runtime, schedule, record, entity, animation);
|
bool hasOrdinarySchedule = IsCurrentSchedule(runtime, schedule, record, entity, animation);
|
||||||
IReadOnlyList<PartTransform>? sequenceFrames = hasOrdinarySchedule
|
IReadOnlyList<PartTransform>? sequenceFrames = hasOrdinarySchedule
|
||||||
? schedule.SequenceFrames
|
? schedule.SequenceFrames
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.World;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Physics.Motion;
|
using AcDream.Core.Physics.Motion;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
|
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
|
||||||
|
|
||||||
|
|
@ -27,7 +28,8 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
private readonly IEntityRootPosePublisher _rootPoses;
|
private readonly IEntityRootPosePublisher _rootPoses;
|
||||||
private readonly IAnimationHookCaptureSink _animationHooks;
|
private readonly IAnimationHookCaptureSink _animationHooks;
|
||||||
private readonly List<LiveEntityRecord> _rootSnapshot = new();
|
private readonly List<LiveEntityRecord> _rootSnapshot = new();
|
||||||
private readonly Dictionary<uint, LiveEntityAnimationSchedule> _schedules = new();
|
private readonly Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
|
_schedules = [];
|
||||||
private readonly Frame _rootFrameScratch = new();
|
private readonly Frame _rootFrameScratch = new();
|
||||||
private readonly MotionDeltaFrame _rootDeltaScratch = new();
|
private readonly MotionDeltaFrame _rootDeltaScratch = new();
|
||||||
|
|
||||||
|
|
@ -59,7 +61,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
/// callback. The returned dictionary is reused and valid until the next
|
/// callback. The returned dictionary is reused and valid until the next
|
||||||
/// call.
|
/// call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Tick(
|
public IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> Tick(
|
||||||
float elapsedSeconds,
|
float elapsedSeconds,
|
||||||
Vector3? playerPosition,
|
Vector3? playerPosition,
|
||||||
bool localHiddenPartPoseDirty,
|
bool localHiddenPartPoseDirty,
|
||||||
|
|
@ -139,7 +141,11 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
IReadOnlyList<PartTransform>? ownedFrames = schedule.SequenceFrames is { } produced
|
IReadOnlyList<PartTransform>? ownedFrames = schedule.SequenceFrames is { } produced
|
||||||
? animation.CaptureScheduleFrames(produced)
|
? animation.CaptureScheduleFrames(produced)
|
||||||
: null;
|
: null;
|
||||||
_schedules[entity.Id] = schedule with
|
RuntimeEntityKey key = record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Live entity 0x{record.ServerGuid:X8}/" +
|
||||||
|
$"{record.Generation} has no exact projection key.");
|
||||||
|
_schedules[key] = schedule with
|
||||||
{
|
{
|
||||||
SequenceFrames = ownedFrames,
|
SequenceFrames = ownedFrames,
|
||||||
Record = record,
|
Record = record,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Scene;
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
|
@ -9,7 +10,7 @@ internal interface ILiveRenderProjectionSink : IRenderProjectionSyncPhase
|
||||||
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
||||||
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
||||||
void OnProjectionPoseReady(uint serverGuid);
|
void OnProjectionPoseReady(uint serverGuid);
|
||||||
void OnProjectionRemoved(uint localEntityId);
|
void OnProjectionRemoved(LiveEntityRecord record);
|
||||||
void OnResourceUnregister(WorldEntity entity);
|
void OnResourceUnregister(WorldEntity entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
private readonly LiveEntityRuntime _runtime;
|
private readonly LiveEntityRuntime _runtime;
|
||||||
private readonly RenderProjectionJournal _journal;
|
private readonly RenderProjectionJournal _journal;
|
||||||
private readonly IRenderTraversalOrderSource _traversalOrder;
|
private readonly IRenderTraversalOrderSource _traversalOrder;
|
||||||
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
private readonly Dictionary<RuntimeEntityKey, TrackedProjection> _byKey = [];
|
||||||
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
||||||
private readonly List<TrackedProjection> _activeScratch = [];
|
private readonly List<TrackedProjection> _activeScratch = [];
|
||||||
|
|
||||||
|
|
@ -40,7 +41,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
?? throw new ArgumentNullException(nameof(traversalOrder));
|
?? throw new ArgumentNullException(nameof(traversalOrder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ProjectionCount => _byLocalId.Count;
|
public int ProjectionCount => _byKey.Count;
|
||||||
|
|
||||||
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
|
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
|
||||||
{
|
{
|
||||||
|
|
@ -62,7 +63,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
if (!_runtime.IsCurrentRecord(record)
|
if (!_runtime.IsCurrentRecord(record)
|
||||||
|| record.WorldEntity is not { } entity
|
|| record.WorldEntity is not { } entity
|
||||||
|| !_byLocalId.ContainsKey(entity.Id))
|
|| record.ProjectionKey is not { } key
|
||||||
|
|| !_byKey.ContainsKey(key))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -87,10 +89,12 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
Upsert(record, entity, record.IsSpatiallyVisible);
|
Upsert(record, entity, record.IsSpatiallyVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnProjectionRemoved(uint localEntityId)
|
public void OnProjectionRemoved(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
if (_byLocalId.TryGetValue(
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
localEntityId,
|
if (record.ProjectionKey is { } key
|
||||||
|
&& _byKey.TryGetValue(
|
||||||
|
key,
|
||||||
out TrackedProjection? tracked)
|
out TrackedProjection? tracked)
|
||||||
&& tracked.Projection.ProjectionClass is
|
&& tracked.Projection.ProjectionClass is
|
||||||
RenderProjectionClass.EquippedChild)
|
RenderProjectionClass.EquippedChild)
|
||||||
|
|
@ -116,11 +120,17 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
public void OnResourceUnregister(WorldEntity entity)
|
public void OnResourceUnregister(WorldEntity entity)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(entity);
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
if (_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked)
|
TrackedProjection? tracked = null;
|
||||||
&& ReferenceEquals(tracked.Entity, entity))
|
foreach (TrackedProjection candidate in _byKey.Values)
|
||||||
{
|
{
|
||||||
Remove(tracked);
|
if (ReferenceEquals(candidate.Entity, entity))
|
||||||
|
{
|
||||||
|
tracked = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (tracked is not null)
|
||||||
|
Remove(tracked);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -153,7 +163,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
// synchronization only refreshes children whose attachment projection
|
// synchronization only refreshes children whose attachment projection
|
||||||
// is still retained by this derived scene.
|
// is still retained by this derived scene.
|
||||||
_activeScratch.Clear();
|
_activeScratch.Clear();
|
||||||
foreach (TrackedProjection tracked in _byLocalId.Values)
|
foreach (TrackedProjection tracked in _byKey.Values)
|
||||||
{
|
{
|
||||||
if (tracked.Record.IsSpatiallyProjected
|
if (tracked.Record.IsSpatiallyProjected
|
||||||
&& tracked.Record.ProjectionKind is
|
&& tracked.Record.ProjectionKind is
|
||||||
|
|
@ -166,8 +176,9 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
TrackedProjection tracked = _activeScratch[i];
|
TrackedProjection tracked = _activeScratch[i];
|
||||||
if (!_runtime.IsCurrentRecord(tracked.Record)
|
if (!_runtime.IsCurrentRecord(tracked.Record)
|
||||||
|| !ReferenceEquals(tracked.Record.WorldEntity, tracked.Entity)
|
|| !ReferenceEquals(tracked.Record.WorldEntity, tracked.Entity)
|
||||||
|| !_byLocalId.TryGetValue(
|
|| tracked.Record.ProjectionKey is not { } key
|
||||||
tracked.Entity.Id,
|
|| !_byKey.TryGetValue(
|
||||||
|
key,
|
||||||
out TrackedProjection? current)
|
out TrackedProjection? current)
|
||||||
|| !ReferenceEquals(current, tracked))
|
|| !ReferenceEquals(current, tracked))
|
||||||
{
|
{
|
||||||
|
|
@ -183,7 +194,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
|
|
||||||
public void ResetTracking()
|
public void ResetTracking()
|
||||||
{
|
{
|
||||||
_byLocalId.Clear();
|
_byKey.Clear();
|
||||||
_activeRootScratch.Clear();
|
_activeRootScratch.Clear();
|
||||||
_activeScratch.Clear();
|
_activeScratch.Clear();
|
||||||
}
|
}
|
||||||
|
|
@ -192,9 +203,11 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
uint localEntityId,
|
uint localEntityId,
|
||||||
out RenderProjectionRecord record)
|
out RenderProjectionRecord record)
|
||||||
{
|
{
|
||||||
if (_byLocalId.TryGetValue(
|
if (_runtime.TryGetRecordByLocalEntityId(
|
||||||
localEntityId,
|
localEntityId,
|
||||||
out TrackedProjection? tracked))
|
out LiveEntityRecord owner)
|
||||||
|
&& owner.ProjectionKey is { } key
|
||||||
|
&& _byKey.TryGetValue(key, out TrackedProjection? tracked))
|
||||||
{
|
{
|
||||||
record = tracked.Projection;
|
record = tracked.Projection;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -217,11 +230,12 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
record,
|
record,
|
||||||
entity,
|
entity,
|
||||||
spatiallyVisible);
|
spatiallyVisible);
|
||||||
if (!_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked))
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
|
if (!_byKey.TryGetValue(key, out TrackedProjection? tracked))
|
||||||
{
|
{
|
||||||
_journal.Register(in projected);
|
_journal.Register(in projected);
|
||||||
_byLocalId.Add(
|
_byKey.Add(
|
||||||
entity.Id,
|
key,
|
||||||
new TrackedProjection(record, entity, projected));
|
new TrackedProjection(record, entity, projected));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +248,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
!= projected.ProjectionClass)
|
!= projected.ProjectionClass)
|
||||||
{
|
{
|
||||||
_journal.Register(in projected);
|
_journal.Register(in projected);
|
||||||
_byLocalId[entity.Id] =
|
_byKey[key] =
|
||||||
new TrackedProjection(record, entity, projected);
|
new TrackedProjection(record, entity, projected);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -283,8 +297,9 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
// Spatial withdrawal is not logical destruction. Retain the last
|
// Spatial withdrawal is not logical destruction. Retain the last
|
||||||
// resident order while the projection is inactive so a portal/rebucket
|
// resident order while the projection is inactive so a portal/rebucket
|
||||||
// edge does not manufacture an unrelated ordering mutation.
|
// edge does not manufacture an unrelated ordering mutation.
|
||||||
return _byLocalId.TryGetValue(
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
entity.Id,
|
return _byKey.TryGetValue(
|
||||||
|
key,
|
||||||
out TrackedProjection? retained)
|
out TrackedProjection? retained)
|
||||||
? projected with { SortKey = retained.Projection.SortKey }
|
? projected with { SortKey = retained.Projection.SortKey }
|
||||||
: projected;
|
: projected;
|
||||||
|
|
@ -292,7 +307,8 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
|
|
||||||
private void Remove(TrackedProjection tracked)
|
private void Remove(TrackedProjection tracked)
|
||||||
{
|
{
|
||||||
if (!_byLocalId.Remove(tracked.Entity.Id))
|
RuntimeEntityKey key = RequireProjectionKey(tracked.Record);
|
||||||
|
if (!_byKey.Remove(key))
|
||||||
return;
|
return;
|
||||||
_journal.Unregister(
|
_journal.Unregister(
|
||||||
tracked.Projection.Id,
|
tracked.Projection.Id,
|
||||||
|
|
@ -302,6 +318,13 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
private static uint Canonicalize(uint cellOrLandblockId) =>
|
private static uint Canonicalize(uint cellOrLandblockId) =>
|
||||||
(cellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
(cellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
|
|
||||||
|
private static RuntimeEntityKey RequireProjectionKey(
|
||||||
|
LiveEntityRecord record) =>
|
||||||
|
record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||||
|
"has no exact projection key.");
|
||||||
|
|
||||||
private sealed class TrackedProjection(
|
private sealed class TrackedProjection(
|
||||||
LiveEntityRecord record,
|
LiveEntityRecord record,
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ internal sealed class RuntimeFramePipelineDiagnosticFactsSource :
|
||||||
_streaming?.DeferredApplyBacklog ?? 0,
|
_streaming?.DeferredApplyBacklog ?? 0,
|
||||||
_streaming?.FullWindowRetirementCount ?? 0,
|
_streaming?.FullWindowRetirementCount ?? 0,
|
||||||
_streaming?.LastFullWindowRetirementLandblockCount ?? 0,
|
_streaming?.LastFullWindowRetirementLandblockCount ?? 0,
|
||||||
_liveEntities.MaterializedWorldEntities.Count,
|
_liveEntities.MaterializedCount,
|
||||||
_liveEntities.Snapshots.Count,
|
_liveEntities.Snapshots.Count,
|
||||||
_world.Entities.Count);
|
_world.Entities.Count);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Vfx;
|
namespace AcDream.App.Rendering.Vfx;
|
||||||
|
|
@ -33,16 +34,17 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
private readonly Func<uint, uint?> _parentOfAttachedChild;
|
private readonly Func<uint, uint?> _parentOfAttachedChild;
|
||||||
private readonly Action<uint> _ownerUnregistered;
|
private readonly Action<uint> _ownerUnregistered;
|
||||||
private readonly Action<uint, uint?> _ownerSoundTableChanged;
|
private readonly Action<uint, uint?> _ownerSoundTableChanged;
|
||||||
private readonly Dictionary<uint, EntityEffectProfile> _profilesByLocalId = new();
|
private readonly Dictionary<RuntimeEntityKey, EntityEffectProfile> _liveProfiles = [];
|
||||||
private readonly Dictionary<uint, ushort> _readyGenerationByServerGuid = new();
|
private readonly HashSet<RuntimeEntityKey> _readyLiveOwners = [];
|
||||||
private readonly Dictionary<uint, Queue<PendingEffect>> _pendingByServerGuid = new();
|
private readonly Dictionary<uint, Queue<PendingEffect>> _pendingByServerGuid = new();
|
||||||
private readonly Dictionary<uint, WorldEntity> _staticOwners = new();
|
private readonly Dictionary<uint, WorldEntity> _staticOwners = new();
|
||||||
|
private readonly Dictionary<uint, EntityEffectProfile> _staticProfiles = new();
|
||||||
private readonly HashSet<uint> _syntheticOwners = new();
|
private readonly HashSet<uint> _syntheticOwners = new();
|
||||||
private readonly HashSet<LiveEntityRecord> _dirtyLiveOwners =
|
private readonly HashSet<LiveEntityRecord> _dirtyLiveOwners =
|
||||||
new(ReferenceEqualityComparer.Instance);
|
new(ReferenceEqualityComparer.Instance);
|
||||||
private readonly List<LiveEntityRecord> _dirtyLiveOwnerOrder = [];
|
private readonly List<LiveEntityRecord> _dirtyLiveOwnerOrder = [];
|
||||||
private readonly List<LiveEntityRecord> _dirtyLiveOwnerSnapshot = [];
|
private readonly List<LiveEntityRecord> _dirtyLiveOwnerSnapshot = [];
|
||||||
private readonly List<uint> _readyGuidSnapshot = [];
|
private readonly List<RuntimeEntityKey> _readyKeySnapshot = [];
|
||||||
private uint _posePublishLocalId;
|
private uint _posePublishLocalId;
|
||||||
private Action<string>? _diagnosticSink = Console.WriteLine;
|
private Action<string>? _diagnosticSink = Console.WriteLine;
|
||||||
|
|
||||||
|
|
@ -76,7 +78,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
}
|
}
|
||||||
|
|
||||||
public int PendingPacketCount => _pendingByServerGuid.Values.Sum(queue => queue.Count);
|
public int PendingPacketCount => _pendingByServerGuid.Values.Sum(queue => queue.Count);
|
||||||
public int ReadyOwnerCount => _profilesByLocalId.Count;
|
public int ReadyOwnerCount => _liveProfiles.Count + _staticProfiles.Count;
|
||||||
internal int LastPoseRefreshOwnerVisitCount { get; private set; }
|
internal int LastPoseRefreshOwnerVisitCount { get; private set; }
|
||||||
|
|
||||||
public void HandleDirect(PlayPhysicsScript message)
|
public void HandleDirect(PlayPhysicsScript message)
|
||||||
|
|
@ -135,8 +137,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_readyGenerationByServerGuid[serverGuid] = record.Generation;
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
_profilesByLocalId[entity.Id] = profile;
|
_readyLiveOwners.Add(key);
|
||||||
|
_liveProfiles[key] = profile;
|
||||||
_runner.SetOwnerAnchor(entity.Id, entity.Position);
|
_runner.SetOwnerAnchor(entity.Id, entity.Position);
|
||||||
_ownerSoundTableChanged(entity.Id, profile.CurrentSoundTableDid);
|
_ownerSoundTableChanged(entity.Id, profile.CurrentSoundTableDid);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -166,7 +169,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_profilesByLocalId[localId] = profile;
|
_liveProfiles[RequireProjectionKey(record)] = profile;
|
||||||
_ownerSoundTableChanged(localId, profile.CurrentSoundTableDid);
|
_ownerSoundTableChanged(localId, profile.CurrentSoundTableDid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +184,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
if (ownerLocalId == 0)
|
if (ownerLocalId == 0)
|
||||||
return;
|
return;
|
||||||
_staticOwners[ownerLocalId] = entity;
|
_staticOwners[ownerLocalId] = entity;
|
||||||
_profilesByLocalId[ownerLocalId] = profile;
|
_staticProfiles[ownerLocalId] = profile;
|
||||||
_runner.SetOwnerAnchor(ownerLocalId, entity.Position);
|
_runner.SetOwnerAnchor(ownerLocalId, entity.Position);
|
||||||
_ownerSoundTableChanged(ownerLocalId, profile.CurrentSoundTableDid);
|
_ownerSoundTableChanged(ownerLocalId, profile.CurrentSoundTableDid);
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +193,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
{
|
{
|
||||||
if (!_staticOwners.Remove(localId))
|
if (!_staticOwners.Remove(localId))
|
||||||
return;
|
return;
|
||||||
_profilesByLocalId.Remove(localId);
|
_staticProfiles.Remove(localId);
|
||||||
_runner.StopAllForEntity(localId);
|
_runner.StopAllForEntity(localId);
|
||||||
_ownerUnregistered(localId);
|
_ownerUnregistered(localId);
|
||||||
}
|
}
|
||||||
|
|
@ -212,16 +215,13 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
{
|
{
|
||||||
_pendingByServerGuid.Remove(record.ServerGuid);
|
_pendingByServerGuid.Remove(record.ServerGuid);
|
||||||
}
|
}
|
||||||
if (_readyGenerationByServerGuid.TryGetValue(
|
if (record.ProjectionKey is { } key)
|
||||||
record.ServerGuid,
|
|
||||||
out ushort readyGeneration)
|
|
||||||
&& readyGeneration == record.Generation)
|
|
||||||
{
|
{
|
||||||
_readyGenerationByServerGuid.Remove(record.ServerGuid);
|
_readyLiveOwners.Remove(key);
|
||||||
|
_liveProfiles.Remove(key);
|
||||||
}
|
}
|
||||||
if (record.LocalEntityId is not { } localId)
|
if (record.LocalEntityId is not { } localId)
|
||||||
return;
|
return;
|
||||||
_profilesByLocalId.Remove(localId);
|
|
||||||
_runner.StopAllForEntity(localId);
|
_runner.StopAllForEntity(localId);
|
||||||
_ownerUnregistered(localId);
|
_ownerUnregistered(localId);
|
||||||
}
|
}
|
||||||
|
|
@ -232,18 +232,15 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
|
|
||||||
public void ClearNetworkState()
|
public void ClearNetworkState()
|
||||||
{
|
{
|
||||||
_readyGuidSnapshot.Clear();
|
_readyKeySnapshot.Clear();
|
||||||
_readyGuidSnapshot.AddRange(_readyGenerationByServerGuid.Keys);
|
_readyKeySnapshot.AddRange(_readyLiveOwners);
|
||||||
foreach (uint serverGuid in _readyGuidSnapshot)
|
foreach (RuntimeEntityKey key in _readyKeySnapshot)
|
||||||
{
|
{
|
||||||
if (_liveEntities.TryGetLocalEntityId(serverGuid, out uint localId))
|
_runner.StopAllForEntity(key.LocalEntityId);
|
||||||
{
|
_ownerUnregistered(key.LocalEntityId);
|
||||||
_profilesByLocalId.Remove(localId);
|
|
||||||
_runner.StopAllForEntity(localId);
|
|
||||||
_ownerUnregistered(localId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_readyGenerationByServerGuid.Clear();
|
_readyLiveOwners.Clear();
|
||||||
|
_liveProfiles.Clear();
|
||||||
_pendingByServerGuid.Clear();
|
_pendingByServerGuid.Clear();
|
||||||
_dirtyLiveOwners.Clear();
|
_dirtyLiveOwners.Clear();
|
||||||
_dirtyLiveOwnerOrder.Clear();
|
_dirtyLiveOwnerOrder.Clear();
|
||||||
|
|
@ -331,7 +328,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
uint rawScriptType,
|
uint rawScriptType,
|
||||||
float intensity)
|
float intensity)
|
||||||
{
|
{
|
||||||
if (!_profilesByLocalId.TryGetValue(ownerLocalId, out EntityEffectProfile? profile)
|
if (!TryGetProfile(ownerLocalId, out EntityEffectProfile? profile)
|
||||||
|| profile.CurrentPhysicsScriptTableDid is not { } tableDid)
|
|| profile.CurrentPhysicsScriptTableDid is not { } tableDid)
|
||||||
{
|
{
|
||||||
DiagnosticSink?.Invoke(
|
DiagnosticSink?.Invoke(
|
||||||
|
|
@ -360,7 +357,7 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
public bool PlayDefault(uint ownerLocalId)
|
public bool PlayDefault(uint ownerLocalId)
|
||||||
{
|
{
|
||||||
if (!CanStartOwner(ownerLocalId)
|
if (!CanStartOwner(ownerLocalId)
|
||||||
|| !_profilesByLocalId.TryGetValue(ownerLocalId, out EntityEffectProfile? profile))
|
|| !TryGetProfile(ownerLocalId, out EntityEffectProfile? profile))
|
||||||
return false;
|
return false;
|
||||||
return PlayTyped(
|
return PlayTyped(
|
||||||
ownerLocalId,
|
ownerLocalId,
|
||||||
|
|
@ -433,8 +430,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
|
|
||||||
private bool TryGetLiveRoot(uint ownerLocalId, out LiveEntityRecord record)
|
private bool TryGetLiveRoot(uint ownerLocalId, out LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
if (_liveEntities.TryGetServerGuid(ownerLocalId, out uint serverGuid)
|
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||||
&& _liveEntities.TryGetRecord(serverGuid, out record!))
|
ownerLocalId,
|
||||||
|
out record!))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -444,12 +442,12 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
|
|
||||||
private bool TryGetReadyLocalId(uint serverGuid, out uint localId)
|
private bool TryGetReadyLocalId(uint serverGuid, out uint localId)
|
||||||
{
|
{
|
||||||
if (_readyGenerationByServerGuid.TryGetValue(serverGuid, out ushort readyGeneration)
|
if (_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||||
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
&& record.ProjectionKey is { } key
|
||||||
&& record.Generation == readyGeneration
|
&& _readyLiveOwners.Contains(key)
|
||||||
&& _liveEntities.TryGetLocalEntityId(serverGuid, out localId)
|
&& _liveProfiles.ContainsKey(key))
|
||||||
&& _profilesByLocalId.ContainsKey(localId))
|
|
||||||
{
|
{
|
||||||
|
localId = key.LocalEntityId;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,8 +459,9 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
{
|
{
|
||||||
if (_posePublishLocalId == localId)
|
if (_posePublishLocalId == localId)
|
||||||
return;
|
return;
|
||||||
if (_liveEntities.TryGetServerGuid(localId, out uint serverGuid)
|
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||||
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record))
|
localId,
|
||||||
|
out LiveEntityRecord record))
|
||||||
{
|
{
|
||||||
MarkLiveOwnerPoseDirty(record);
|
MarkLiveOwnerPoseDirty(record);
|
||||||
}
|
}
|
||||||
|
|
@ -476,10 +475,8 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
|
|
||||||
private void MarkLiveOwnerPoseDirty(LiveEntityRecord record)
|
private void MarkLiveOwnerPoseDirty(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
if (!_readyGenerationByServerGuid.TryGetValue(
|
if (record.ProjectionKey is not { } key
|
||||||
record.ServerGuid,
|
|| !_readyLiveOwners.Contains(key)
|
||||||
out ushort readyGeneration)
|
|
||||||
|| readyGeneration != record.Generation
|
|
||||||
|| !_dirtyLiveOwners.Add(record))
|
|| !_dirtyLiveOwners.Add(record))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -550,6 +547,29 @@ public sealed class EntityEffectController : IAnimationHookSink,
|
||||||
PlayTyped(localId, effect.RawScriptType, effect.Intensity);
|
PlayTyped(localId, effect.RawScriptType, effect.Intensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryGetProfile(
|
||||||
|
uint ownerLocalId,
|
||||||
|
out EntityEffectProfile profile)
|
||||||
|
{
|
||||||
|
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||||
|
ownerLocalId,
|
||||||
|
out LiveEntityRecord record)
|
||||||
|
&& record.ProjectionKey is { } key
|
||||||
|
&& _liveProfiles.TryGetValue(key, out profile!))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _staticProfiles.TryGetValue(ownerLocalId, out profile!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeEntityKey RequireProjectionKey(
|
||||||
|
LiveEntityRecord record) =>
|
||||||
|
record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||||
|
"has no exact projection key.");
|
||||||
|
|
||||||
private enum PendingEffectKind
|
private enum PendingEffectKind
|
||||||
{
|
{
|
||||||
Direct,
|
Direct,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Lighting;
|
using AcDream.Core.Lighting;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Vfx;
|
namespace AcDream.App.Rendering.Vfx;
|
||||||
|
|
@ -22,8 +23,8 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
private readonly EntityEffectPoseRegistry _poses;
|
private readonly EntityEffectPoseRegistry _poses;
|
||||||
private readonly LightingHookSink _lighting;
|
private readonly LightingHookSink _lighting;
|
||||||
private readonly Func<uint, Setup?> _loadSetup;
|
private readonly Func<uint, Setup?> _loadSetup;
|
||||||
private readonly Dictionary<uint, uint> _serverGuidByOwner = new();
|
private readonly HashSet<RuntimeEntityKey> _trackedOwners = [];
|
||||||
private readonly HashSet<uint> _presentOwners = new();
|
private readonly HashSet<RuntimeEntityKey> _presentOwners = [];
|
||||||
|
|
||||||
public LiveEntityLightController(
|
public LiveEntityLightController(
|
||||||
LiveEntityRuntime liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
|
|
@ -49,8 +50,9 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_serverGuidByOwner[entity.Id] = serverGuid;
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
_presentOwners.Add(entity.Id);
|
_trackedOwners.Add(key);
|
||||||
|
_presentOwners.Add(key);
|
||||||
_lighting.UnregisterOwner(entity.Id, forgetState: false);
|
_lighting.UnregisterOwner(entity.Id, forgetState: false);
|
||||||
_lighting.InitializeOwnerLighting(
|
_lighting.InitializeOwnerLighting(
|
||||||
entity.Id,
|
entity.Id,
|
||||||
|
|
@ -87,7 +89,13 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
/// <summary>Withdraw cell-scoped presentation but retain logical light state.</summary>
|
/// <summary>Withdraw cell-scoped presentation but retain logical light state.</summary>
|
||||||
public void Unregister(uint ownerLocalId)
|
public void Unregister(uint ownerLocalId)
|
||||||
{
|
{
|
||||||
_presentOwners.Remove(ownerLocalId);
|
if (_liveEntities.TryGetRecordByLocalEntityId(
|
||||||
|
ownerLocalId,
|
||||||
|
out LiveEntityRecord record)
|
||||||
|
&& record.ProjectionKey is { } key)
|
||||||
|
{
|
||||||
|
_presentOwners.Remove(key);
|
||||||
|
}
|
||||||
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
|
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,16 +113,20 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>End logical ownership after leave-world presentation teardown.</summary>
|
/// <summary>End logical ownership after leave-world presentation teardown.</summary>
|
||||||
public void Forget(uint ownerLocalId)
|
public void Forget(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
_presentOwners.Remove(ownerLocalId);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
_serverGuidByOwner.Remove(ownerLocalId);
|
if (record.ProjectionKey is not { } key)
|
||||||
_lighting.UnregisterOwner(ownerLocalId);
|
return;
|
||||||
|
|
||||||
|
_presentOwners.Remove(key);
|
||||||
|
_trackedOwners.Remove(key);
|
||||||
|
_lighting.UnregisterOwner(key.LocalEntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Refresh() => _lighting.RefreshAttachedLights();
|
public void Refresh() => _lighting.RefreshAttachedLights();
|
||||||
|
|
||||||
internal int TrackedOwnerCount => _serverGuidByOwner.Count;
|
internal int TrackedOwnerCount => _trackedOwners.Count;
|
||||||
internal int PresentedOwnerCount => _presentOwners.Count;
|
internal int PresentedOwnerCount => _presentOwners.Count;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -127,7 +139,8 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||||
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached
|
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached
|
||||||
|| record.WorldEntity is not { } entity
|
|| record.WorldEntity is not { } entity
|
||||||
|| _presentOwners.Contains(entity.Id))
|
|| record.ProjectionKey is not { } key
|
||||||
|
|| _presentOwners.Contains(key))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -136,8 +149,11 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
|
|
||||||
private void OnOwnerLightingChanged(uint ownerLocalId, bool enabled)
|
private void OnOwnerLightingChanged(uint ownerLocalId, bool enabled)
|
||||||
{
|
{
|
||||||
if (!_presentOwners.Contains(ownerLocalId)
|
if (!_liveEntities.TryGetRecordByLocalEntityId(
|
||||||
|| !_serverGuidByOwner.TryGetValue(ownerLocalId, out uint serverGuid))
|
ownerLocalId,
|
||||||
|
out LiveEntityRecord record)
|
||||||
|
|| record.ProjectionKey is not { } key
|
||||||
|
|| !_presentOwners.Contains(key))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -148,15 +164,17 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Register(serverGuid);
|
Register(record.ServerGuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_lighting.OwnerLightingChanged -= OnOwnerLightingChanged;
|
_lighting.OwnerLightingChanged -= OnOwnerLightingChanged;
|
||||||
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
|
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
|
||||||
foreach (uint ownerId in _serverGuidByOwner.Keys.ToArray())
|
foreach (RuntimeEntityKey key in _trackedOwners)
|
||||||
Forget(ownerId);
|
_lighting.UnregisterOwner(key.LocalEntityId);
|
||||||
|
_trackedOwners.Clear();
|
||||||
|
_presentOwners.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
||||||
|
|
@ -176,4 +194,11 @@ public sealed class LiveEntityLightController : IDisposable
|
||||||
else
|
else
|
||||||
Unregister(entity.Id);
|
Unregister(entity.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RuntimeEntityKey RequireProjectionKey(
|
||||||
|
LiveEntityRecord record) =>
|
||||||
|
record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||||
|
"has no exact projection key.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
|
@ -48,18 +49,20 @@ public sealed class EntitySpawnAdapter
|
||||||
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
|
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
|
||||||
private readonly IWbMeshAdapter? _meshAdapter;
|
private readonly IWbMeshAdapter? _meshAdapter;
|
||||||
|
|
||||||
// One logical owner per server GUID. Animated state survives projection
|
// One logical owner per exact Runtime identity. Animated state survives projection
|
||||||
// suspension, while the resident bit controls the shorter GPU-presentation
|
// suspension, while the resident bit controls the shorter GPU-presentation
|
||||||
// lifetime. The exact WorldEntity reference makes delayed visibility edges
|
// lifetime. The exact WorldEntity reference makes delayed visibility edges
|
||||||
// from a displaced GUID generation harmless.
|
// from a displaced GUID generation harmless.
|
||||||
// Single-threaded: called only from the render thread (same as GpuWorldState).
|
// Single-threaded: called only from the render thread (same as GpuWorldState).
|
||||||
private readonly Dictionary<uint, Owner> _ownersByGuid = new();
|
private readonly Dictionary<RuntimeEntityKey, Owner> _ownersByKey = [];
|
||||||
|
|
||||||
private sealed class Owner(
|
private sealed class Owner(
|
||||||
|
RuntimeEntityKey key,
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
AnimatedEntityState state,
|
AnimatedEntityState state,
|
||||||
HashSet<ulong> meshIds)
|
HashSet<ulong> meshIds)
|
||||||
{
|
{
|
||||||
|
public RuntimeEntityKey Key { get; } = key;
|
||||||
public WorldEntity Entity { get; } = entity;
|
public WorldEntity Entity { get; } = entity;
|
||||||
public AnimatedEntityState State { get; } = state;
|
public AnimatedEntityState State { get; } = state;
|
||||||
public HashSet<ulong> MeshIds { get; set; } = meshIds;
|
public HashSet<ulong> MeshIds { get; set; } = meshIds;
|
||||||
|
|
@ -137,12 +140,25 @@ public sealed class EntitySpawnAdapter
|
||||||
/// <paramref name="entity"/> is atlas-tier (<c>ServerGuid == 0</c>).
|
/// <paramref name="entity"/> is atlas-tier (<c>ServerGuid == 0</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AnimatedEntityState? OnCreate(WorldEntity entity)
|
public AnimatedEntityState? OnCreate(WorldEntity entity)
|
||||||
|
=> OnCreate(new RuntimeEntityKey(entity.Id, 0), entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates one exact Runtime-owned presentation resource set.
|
||||||
|
/// </summary>
|
||||||
|
public AnimatedEntityState? OnCreate(
|
||||||
|
RuntimeEntityKey key,
|
||||||
|
WorldEntity entity)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(entity);
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
|
||||||
// Atlas-tier entities (procedural / dat-hydrated, ServerGuid == 0)
|
// Atlas-tier entities (procedural / dat-hydrated, ServerGuid == 0)
|
||||||
// are handled by LandblockSpawnAdapter, not here.
|
// are handled by LandblockSpawnAdapter, not here.
|
||||||
if (entity.ServerGuid == 0) return null;
|
if (entity.ServerGuid == 0) return null;
|
||||||
|
if (key.LocalEntityId == 0 || key.LocalEntityId != entity.Id)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The exact Runtime projection key must match the WorldEntity local ID.");
|
||||||
|
}
|
||||||
|
|
||||||
// A.5 T18: populate cached AABB so WalkEntities reads from the cache
|
// A.5 T18: populate cached AABB so WalkEntities reads from the cache
|
||||||
// rather than recomputing Position±5 per frame. Called here because
|
// rather than recomputing Position±5 per frame. Called here because
|
||||||
|
|
@ -177,8 +193,8 @@ public sealed class EntitySpawnAdapter
|
||||||
// valid. Retirement is also completed before replacement publication:
|
// valid. Retirement is also completed before replacement publication:
|
||||||
// a failed texture or mesh release therefore leaves the prior owner in
|
// a failed texture or mesh release therefore leaves the prior owner in
|
||||||
// the dictionary, with its per-resource progress available to retry.
|
// the dictionary, with its per-resource progress available to retry.
|
||||||
var replacementOwner = new Owner(entity, state, meshIds);
|
var replacementOwner = new Owner(key, entity, state, meshIds);
|
||||||
if (_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? displacedOwner))
|
if (_ownersByKey.TryGetValue(key, out Owner? displacedOwner))
|
||||||
{
|
{
|
||||||
if (displacedOwner.RemovalPending)
|
if (displacedOwner.RemovalPending)
|
||||||
{
|
{
|
||||||
|
|
@ -200,11 +216,11 @@ public sealed class EntitySpawnAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ownersByGuid[entity.ServerGuid] = replacementOwner;
|
_ownersByKey[key] = replacementOwner;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_ownersByGuid.Add(entity.ServerGuid, replacementOwner);
|
_ownersByKey.Add(key, replacementOwner);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
|
@ -223,8 +239,7 @@ public sealed class EntitySpawnAdapter
|
||||||
public bool SetPresentationResident(WorldEntity entity, bool resident)
|
public bool SetPresentationResident(WorldEntity entity, bool resident)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(entity);
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
if (!_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? owner)
|
if (!TryFindOwner(entity, out _, out Owner owner)
|
||||||
|| !ReferenceEquals(owner.Entity, entity)
|
|
||||||
|| owner.RemovalPending
|
|| owner.RemovalPending
|
||||||
|| (resident ? owner.IsFullyResident : owner.IsFullySuspended))
|
|| (resident ? owner.IsFullyResident : owner.IsFullySuspended))
|
||||||
{
|
{
|
||||||
|
|
@ -270,8 +285,7 @@ public sealed class EntitySpawnAdapter
|
||||||
ArgumentNullException.ThrowIfNull(partOverrides);
|
ArgumentNullException.ThrowIfNull(partOverrides);
|
||||||
ArgumentNullException.ThrowIfNull(publishAppearance);
|
ArgumentNullException.ThrowIfNull(publishAppearance);
|
||||||
|
|
||||||
if (!_ownersByGuid.TryGetValue(entity.ServerGuid, out Owner? owner)
|
if (!TryFindOwner(entity, out _, out Owner owner)
|
||||||
|| !ReferenceEquals(owner.Entity, entity)
|
|
||||||
|| owner.RemovalPending
|
|| owner.RemovalPending
|
||||||
|| owner.Transition != PresentationTransition.None)
|
|| owner.Transition != PresentationTransition.None)
|
||||||
{
|
{
|
||||||
|
|
@ -340,19 +354,31 @@ public sealed class EntitySpawnAdapter
|
||||||
/// and propagates the exception; a later call resumes its unfinished releases.
|
/// and propagates the exception; a later call resumes its unfinished releases.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void OnRemove(uint serverGuid)
|
public void OnRemove(uint serverGuid)
|
||||||
=> _ = TryRemove(serverGuid, expectedEntity: null);
|
|
||||||
|
|
||||||
private bool TryRemove(uint serverGuid, WorldEntity? expectedEntity)
|
|
||||||
{
|
{
|
||||||
if (!_ownersByGuid.TryGetValue(serverGuid, out Owner? owner)
|
foreach ((RuntimeEntityKey key, Owner owner) in _ownersByKey)
|
||||||
|| (expectedEntity is not null && !ReferenceEquals(owner.Entity, expectedEntity)))
|
{
|
||||||
|
if (owner.Entity.ServerGuid == serverGuid)
|
||||||
|
{
|
||||||
|
_ = TryRemove(key, owner.Entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRemove(
|
||||||
|
RuntimeEntityKey key,
|
||||||
|
WorldEntity expectedEntity)
|
||||||
|
{
|
||||||
|
if (!_ownersByKey.TryGetValue(key, out Owner? owner)
|
||||||
|
|| !ReferenceEquals(owner.Entity, expectedEntity))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
owner.RemovalPending = true;
|
owner.RemovalPending = true;
|
||||||
if (owner.Transition != PresentationTransition.None)
|
if (owner.Transition != PresentationTransition.None)
|
||||||
throw new EntityPresentationRemovalDeferredException(serverGuid);
|
throw new EntityPresentationRemovalDeferredException(
|
||||||
|
owner.Entity.ServerGuid);
|
||||||
|
|
||||||
// A resident owner still holds both mesh and potentially-lazy texture
|
// A resident owner still holds both mesh and potentially-lazy texture
|
||||||
// resources. A suspended owner released them at the visibility edge,
|
// resources. A suspended owner released them at the visibility edge,
|
||||||
|
|
@ -363,10 +389,10 @@ public sealed class EntitySpawnAdapter
|
||||||
if (owner.HasPresentationResources && !SuspendPresentation(owner))
|
if (owner.HasPresentationResources && !SuspendPresentation(owner))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (_ownersByGuid.TryGetValue(serverGuid, out Owner? current)
|
if (_ownersByKey.TryGetValue(key, out Owner? current)
|
||||||
&& ReferenceEquals(current, owner))
|
&& ReferenceEquals(current, owner))
|
||||||
{
|
{
|
||||||
_ownersByGuid.Remove(serverGuid);
|
_ownersByKey.Remove(key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -384,7 +410,8 @@ public sealed class EntitySpawnAdapter
|
||||||
public bool OnRemove(WorldEntity entity)
|
public bool OnRemove(WorldEntity entity)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(entity);
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
return TryRemove(entity.ServerGuid, entity);
|
return TryFindOwner(entity, out RuntimeEntityKey key, out _)
|
||||||
|
&& TryRemove(key, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -393,9 +420,35 @@ public sealed class EntitySpawnAdapter
|
||||||
/// been removed.
|
/// been removed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AnimatedEntityState? GetState(uint serverGuid)
|
public AnimatedEntityState? GetState(uint serverGuid)
|
||||||
=> _ownersByGuid.TryGetValue(serverGuid, out Owner? owner)
|
{
|
||||||
? owner.State
|
foreach (Owner owner in _ownersByKey.Values)
|
||||||
: null;
|
{
|
||||||
|
if (owner.Entity.ServerGuid == serverGuid)
|
||||||
|
return owner.State;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFindOwner(
|
||||||
|
WorldEntity entity,
|
||||||
|
out RuntimeEntityKey key,
|
||||||
|
out Owner owner)
|
||||||
|
{
|
||||||
|
foreach ((RuntimeEntityKey candidateKey, Owner candidate) in _ownersByKey)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(candidate.Entity, entity))
|
||||||
|
{
|
||||||
|
key = candidateKey;
|
||||||
|
owner = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key = default;
|
||||||
|
owner = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private bool ResumePresentation(Owner owner)
|
private bool ResumePresentation(Owner owner)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -150,15 +150,14 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
// This index mirrors only loaded live projections and changes at the same
|
// This index mirrors only loaded live projections and changes at the same
|
||||||
// transaction boundary as _projectionLocations.
|
// transaction boundary as _projectionLocations.
|
||||||
private readonly Dictionary<uint, HashSet<WorldEntity>> _loadedLiveByLandblock = new();
|
private readonly Dictionary<uint, HashSet<WorldEntity>> _loadedLiveByLandblock = new();
|
||||||
// A server GUID normally has exactly one spatial projection. Keep that
|
// Runtime-issued local IDs stay unique for the complete materialized
|
||||||
// allocation-free common case directly in the primary map; the secondary
|
// lifetime, including retryable teardown. Spatial storage therefore never
|
||||||
// list exists only for the rare overlap during GUID reuse/re-entrant
|
// groups graphical owners by reusable server GUID.
|
||||||
// lifetime transitions.
|
private readonly Dictionary<uint, WorldEntity> _liveProjectionByLocalId = new();
|
||||||
private readonly Dictionary<uint, WorldEntity> _primaryProjectionByGuid = new();
|
|
||||||
private readonly Dictionary<uint, List<WorldEntity>> _additionalProjectionsByGuid = new();
|
|
||||||
private readonly Dictionary<uint, int> _visibleLiveProjectionCounts = new();
|
private readonly Dictionary<uint, int> _visibleLiveProjectionCounts = new();
|
||||||
private readonly Dictionary<uint, bool> _visibilityBeforeMutation = new();
|
private readonly Dictionary<uint, bool> _visibilityBeforeMutation = new();
|
||||||
private readonly Queue<(uint ServerGuid, bool Visible)> _visibilityTransitions = new();
|
private readonly Queue<(uint LocalEntityId, bool Visible)> _visibilityTransitions = new();
|
||||||
|
private readonly List<WorldEntity> _guidRemovalScratch = new();
|
||||||
private bool _dispatchingVisibilityTransitions;
|
private bool _dispatchingVisibilityTransitions;
|
||||||
private int _mutationDepth;
|
private int _mutationDepth;
|
||||||
private long _visibilityCommitCount;
|
private long _visibilityCommitCount;
|
||||||
|
|
@ -180,18 +179,18 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_loaded.ContainsKey(landblockId)
|
_loaded.ContainsKey(landblockId)
|
||||||
&& (_wbSpawnAdapter?.IsLandblockRenderReady(landblockId) ?? true);
|
&& (_wbSpawnAdapter?.IsLandblockRenderReady(landblockId) ?? true);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when at least one projection for the server object belongs to a
|
/// True when the exact Runtime-issued local projection belongs to a loaded
|
||||||
/// loaded spatial bucket. This deliberately ignores the world-generation
|
/// spatial bucket. This deliberately ignores the world-generation
|
||||||
/// presentation gate: destination objects must acquire their render
|
/// presentation gate: destination objects must acquire their render
|
||||||
/// resources while portal/login reveal keeps normal world consumers closed.
|
/// resources while portal/login reveal keeps normal world consumers closed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsLiveEntityProjectionResident(uint serverGuid) =>
|
public bool IsLiveEntityProjectionResident(uint localEntityId) =>
|
||||||
serverGuid != 0
|
localEntityId != 0
|
||||||
&& _visibleLiveProjectionCounts.ContainsKey(serverGuid);
|
&& _visibleLiveProjectionCounts.ContainsKey(localEntityId);
|
||||||
|
|
||||||
public bool IsLiveEntityVisible(uint serverGuid) =>
|
public bool IsLiveEntityVisible(uint localEntityId) =>
|
||||||
_availability.IsWorldAvailable
|
_availability.IsWorldAvailable
|
||||||
&& IsLiveEntityProjectionResident(serverGuid);
|
&& IsLiveEntityProjectionResident(localEntityId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to grab the loaded record for a landblock — useful for callers
|
/// Try to grab the loaded record for a landblock — useful for callers
|
||||||
|
|
@ -583,11 +582,11 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
if (--_mutationDepth != 0)
|
if (--_mutationDepth != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach ((uint guid, bool wasVisible) in _visibilityBeforeMutation)
|
foreach ((uint localEntityId, bool wasVisible) in _visibilityBeforeMutation)
|
||||||
{
|
{
|
||||||
bool isVisible = _visibleLiveProjectionCounts.ContainsKey(guid);
|
bool isVisible = _visibleLiveProjectionCounts.ContainsKey(localEntityId);
|
||||||
if (wasVisible != isVisible)
|
if (wasVisible != isVisible)
|
||||||
_visibilityTransitions.Enqueue((guid, isVisible));
|
_visibilityTransitions.Enqueue((localEntityId, isVisible));
|
||||||
}
|
}
|
||||||
_visibilityBeforeMutation.Clear();
|
_visibilityBeforeMutation.Clear();
|
||||||
_visibilityCommitCount++;
|
_visibilityCommitCount++;
|
||||||
|
|
@ -604,26 +603,27 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
|
|
||||||
private void AdjustVisibleLiveProjection(WorldEntity entity, int delta)
|
private void AdjustVisibleLiveProjection(WorldEntity entity, int delta)
|
||||||
{
|
{
|
||||||
uint guid = entity.ServerGuid;
|
uint localEntityId = entity.Id;
|
||||||
if (guid == 0 || delta == 0)
|
if (entity.ServerGuid == 0 || localEntityId == 0 || delta == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_visibleLiveProjectionCounts.TryGetValue(guid, out int priorCount);
|
_visibleLiveProjectionCounts.TryGetValue(localEntityId, out int priorCount);
|
||||||
if (!_visibilityBeforeMutation.ContainsKey(guid))
|
if (!_visibilityBeforeMutation.ContainsKey(localEntityId))
|
||||||
_visibilityBeforeMutation.Add(guid, priorCount > 0);
|
_visibilityBeforeMutation.Add(localEntityId, priorCount > 0);
|
||||||
|
|
||||||
int nextCount = checked(priorCount + delta);
|
int nextCount = checked(priorCount + delta);
|
||||||
if (nextCount < 0)
|
if (nextCount < 0)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Live projection visibility count underflow for 0x{guid:X8}.");
|
$"Live projection visibility count underflow for local " +
|
||||||
|
$"0x{localEntityId:X8} / server 0x{entity.ServerGuid:X8}.");
|
||||||
|
|
||||||
if (nextCount == 0)
|
if (nextCount == 0)
|
||||||
{
|
{
|
||||||
_visibleLiveProjectionCounts.Remove(guid);
|
_visibleLiveProjectionCounts.Remove(localEntityId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_visibleLiveProjectionCounts[guid] = nextCount;
|
_visibleLiveProjectionCounts[localEntityId] = nextCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -695,16 +695,11 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
if (!isNew)
|
if (!isNew)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint guid = entity.ServerGuid;
|
if (!_liveProjectionByLocalId.TryAdd(entity.Id, entity))
|
||||||
if (_primaryProjectionByGuid.TryAdd(guid, entity))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_additionalProjectionsByGuid.TryGetValue(guid, out List<WorldEntity>? projections))
|
|
||||||
{
|
{
|
||||||
projections = new List<WorldEntity>(1);
|
throw new InvalidOperationException(
|
||||||
_additionalProjectionsByGuid.Add(guid, projections);
|
$"Runtime local projection id 0x{entity.Id:X8} is already spatially owned.");
|
||||||
}
|
}
|
||||||
projections.Add(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveProjectionLocation(WorldEntity entity)
|
private void RemoveProjectionLocation(WorldEntity entity)
|
||||||
|
|
@ -715,51 +710,18 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
if (location.IsLoaded)
|
if (location.IsLoaded)
|
||||||
RemoveLoadedLiveIndex(location.LandblockId, entity);
|
RemoveLoadedLiveIndex(location.LandblockId, entity);
|
||||||
|
|
||||||
uint guid = entity.ServerGuid;
|
if (!_liveProjectionByLocalId.Remove(
|
||||||
if (!_primaryProjectionByGuid.TryGetValue(guid, out WorldEntity? primary))
|
entity.Id,
|
||||||
throw new InvalidOperationException(
|
out WorldEntity? indexed))
|
||||||
$"Live projection 0x{entity.Id:X8} had no server-guid index entry.");
|
|
||||||
|
|
||||||
if (ReferenceEquals(primary, entity))
|
|
||||||
{
|
{
|
||||||
if (_additionalProjectionsByGuid.TryGetValue(guid, out List<WorldEntity>? projections)
|
|
||||||
&& projections.Count > 0)
|
|
||||||
{
|
|
||||||
int lastIndex = projections.Count - 1;
|
|
||||||
_primaryProjectionByGuid[guid] = projections[lastIndex];
|
|
||||||
projections.RemoveAt(lastIndex);
|
|
||||||
if (projections.Count == 0)
|
|
||||||
_additionalProjectionsByGuid.Remove(guid);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_primaryProjectionByGuid.Remove(guid);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_additionalProjectionsByGuid.TryGetValue(guid, out List<WorldEntity>? additional))
|
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Live projection 0x{entity.Id:X8} was absent from its server-guid index.");
|
$"Live projection 0x{entity.Id:X8} had no exact local-id index entry.");
|
||||||
|
}
|
||||||
int index = -1;
|
if (!ReferenceEquals(indexed, entity))
|
||||||
for (int i = 0; i < additional.Count; i++)
|
|
||||||
{
|
{
|
||||||
if (!ReferenceEquals(additional[i], entity))
|
|
||||||
continue;
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (index < 0)
|
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Live projection 0x{entity.Id:X8} was absent from its server-guid index.");
|
$"Runtime local projection id 0x{entity.Id:X8} resolved a different owner.");
|
||||||
|
}
|
||||||
int lastAdditionalIndex = additional.Count - 1;
|
|
||||||
if (index != lastAdditionalIndex)
|
|
||||||
additional[index] = additional[lastAdditionalIndex];
|
|
||||||
additional.RemoveAt(lastAdditionalIndex);
|
|
||||||
if (additional.Count == 0)
|
|
||||||
_additionalProjectionsByGuid.Remove(guid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddLoadedLiveIndex(uint landblockId, WorldEntity entity)
|
private void AddLoadedLiveIndex(uint landblockId, WorldEntity entity)
|
||||||
|
|
@ -1397,10 +1359,13 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
MutationBatch mutation = BeginMutationBatch();
|
MutationBatch mutation = BeginMutationBatch();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach ((uint guid, int count) in _visibleLiveProjectionCounts)
|
foreach ((uint localEntityId, int count) in _visibleLiveProjectionCounts)
|
||||||
{
|
{
|
||||||
if (count > 0 && !_visibilityBeforeMutation.ContainsKey(guid))
|
if (count > 0
|
||||||
_visibilityBeforeMutation.Add(guid, true);
|
&& !_visibilityBeforeMutation.ContainsKey(localEntityId))
|
||||||
|
{
|
||||||
|
_visibilityBeforeMutation.Add(localEntityId, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_visibleLiveProjectionCounts.Clear();
|
_visibleLiveProjectionCounts.Clear();
|
||||||
|
|
||||||
|
|
@ -1410,8 +1375,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_flatEntityIndices.Clear();
|
_flatEntityIndices.Clear();
|
||||||
_projectionLocations.Clear();
|
_projectionLocations.Clear();
|
||||||
_loadedLiveByLandblock.Clear();
|
_loadedLiveByLandblock.Clear();
|
||||||
_primaryProjectionByGuid.Clear();
|
_liveProjectionByLocalId.Clear();
|
||||||
_additionalProjectionsByGuid.Clear();
|
|
||||||
|
|
||||||
_loaded.Clear();
|
_loaded.Clear();
|
||||||
_renderTraversalLandblockSlots.Clear();
|
_renderTraversalLandblockSlots.Clear();
|
||||||
|
|
@ -1525,12 +1489,18 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
|
|
||||||
using MutationBatch mutation = BeginMutationBatch();
|
using MutationBatch mutation = BeginMutationBatch();
|
||||||
|
|
||||||
// Canonical live projections are indexed by server GUID and entity
|
// GUID-only removal is reserved for session cleanup paths that do not
|
||||||
// identity, so delete/withdraw never scans every resident landblock.
|
// have an exact projection owner. Ordinary lifecycle operations use
|
||||||
while (_primaryProjectionByGuid.TryGetValue(serverGuid, out WorldEntity? projection))
|
// the exact-entity overload below.
|
||||||
|
_guidRemovalScratch.Clear();
|
||||||
|
foreach (WorldEntity projection in _projectionLocations.Keys)
|
||||||
{
|
{
|
||||||
RemoveEntityFromAllBuckets(projection);
|
if (projection.ServerGuid == serverGuid)
|
||||||
|
_guidRemovalScratch.Add(projection);
|
||||||
}
|
}
|
||||||
|
for (int i = 0; i < _guidRemovalScratch.Count; i++)
|
||||||
|
RemoveEntityFromAllBuckets(_guidRemovalScratch[i]);
|
||||||
|
_guidRemovalScratch.Clear();
|
||||||
|
|
||||||
// A persistent projection may have been rescued by RemoveLandblock
|
// A persistent projection may have been rescued by RemoveLandblock
|
||||||
// and not yet drained by the next GameWindow frame. Rebucketing or
|
// and not yet drained by the next GameWindow frame. Rebucketing or
|
||||||
|
|
@ -1582,8 +1552,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_visibilityBeforeMutation.Clear();
|
_visibilityBeforeMutation.Clear();
|
||||||
_projectionLocations.Clear();
|
_projectionLocations.Clear();
|
||||||
_loadedLiveByLandblock.Clear();
|
_loadedLiveByLandblock.Clear();
|
||||||
_primaryProjectionByGuid.Clear();
|
_liveProjectionByLocalId.Clear();
|
||||||
_additionalProjectionsByGuid.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1905,7 +1874,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
((Action<uint, bool>)subscribers[i])(
|
((Action<uint, bool>)subscribers[i])(
|
||||||
transition.ServerGuid,
|
transition.LocalEntityId,
|
||||||
transition.Visible);
|
transition.Visible);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
||||||
// SnapToCell owns the retail Position frame and may normalize an
|
// SnapToCell owns the retail Position frame and may normalize an
|
||||||
// outdoor land-cell index from the cell-local origin. Publish that
|
// outdoor land-cell index from the cell-local origin. Publish that
|
||||||
// canonical result, not the pre-snap resolver hint, to rendering.
|
// canonical result, not the pre-snap resolver hint, to rendering.
|
||||||
if (_liveEntities.MaterializedWorldEntities.TryGetValue(
|
if (_liveEntities.TryGetWorldEntity(
|
||||||
playerGuid,
|
playerGuid,
|
||||||
out WorldEntity? entity))
|
out WorldEntity? entity))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -62,18 +62,22 @@ internal sealed class WorldGenerationQuiescence
|
||||||
private readonly WorldGenerationAvailabilityState _availability;
|
private readonly WorldGenerationAvailabilityState _availability;
|
||||||
private readonly SelectionState _selection;
|
private readonly SelectionState _selection;
|
||||||
private readonly GpuWorldState _world;
|
private readonly GpuWorldState _world;
|
||||||
|
private readonly Func<uint, uint?> _resolveLocalEntityId;
|
||||||
private readonly IWorldAudioQuiescence? _audio;
|
private readonly IWorldAudioQuiescence? _audio;
|
||||||
|
|
||||||
public WorldGenerationQuiescence(
|
public WorldGenerationQuiescence(
|
||||||
WorldGenerationAvailabilityState availability,
|
WorldGenerationAvailabilityState availability,
|
||||||
SelectionState selection,
|
SelectionState selection,
|
||||||
GpuWorldState world,
|
GpuWorldState world,
|
||||||
|
Func<uint, uint?> resolveLocalEntityId,
|
||||||
IWorldAudioQuiescence? audio)
|
IWorldAudioQuiescence? audio)
|
||||||
{
|
{
|
||||||
_availability = availability
|
_availability = availability
|
||||||
?? throw new ArgumentNullException(nameof(availability));
|
?? throw new ArgumentNullException(nameof(availability));
|
||||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||||
|
_resolveLocalEntityId = resolveLocalEntityId
|
||||||
|
?? throw new ArgumentNullException(nameof(resolveLocalEntityId));
|
||||||
_audio = audio;
|
_audio = audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +89,8 @@ internal sealed class WorldGenerationQuiescence
|
||||||
uint? selected = firstEdge ? _selection.SelectedObjectId : null;
|
uint? selected = firstEdge ? _selection.SelectedObjectId : null;
|
||||||
bool clearWorldSelection =
|
bool clearWorldSelection =
|
||||||
selected is uint selectedGuid
|
selected is uint selectedGuid
|
||||||
&& _world.IsLiveEntityVisible(selectedGuid);
|
&& _resolveLocalEntityId(selectedGuid) is uint localEntityId
|
||||||
|
&& _world.IsLiveEntityVisible(localEntityId);
|
||||||
|
|
||||||
_availability.Begin(generation);
|
_availability.Begin(generation);
|
||||||
if (!firstEdge)
|
if (!firstEdge)
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ public sealed class RadarSnapshotProvider
|
||||||
private static readonly Vector2 ProductionCenter = new(60f, 60f);
|
private static readonly Vector2 ProductionCenter = new(60f, 60f);
|
||||||
|
|
||||||
private readonly ClientObjectTable _objects;
|
private readonly ClientObjectTable _objects;
|
||||||
private readonly Func<IReadOnlyDictionary<uint, WorldEntity>> _worldEntities;
|
private readonly ILiveEntityRadarSource _liveEntities;
|
||||||
private readonly Func<IReadOnlyDictionary<uint, WorldEntity>> _playerEntities;
|
|
||||||
private readonly Func<IReadOnlyDictionary<uint, WorldSession.EntitySpawn>> _spawns;
|
private readonly Func<IReadOnlyDictionary<uint, WorldSession.EntitySpawn>> _spawns;
|
||||||
private readonly Func<uint> _playerGuid;
|
private readonly Func<uint> _playerGuid;
|
||||||
private readonly Func<float> _playerYawRadians;
|
private readonly Func<float> _playerYawRadians;
|
||||||
|
|
@ -35,7 +34,7 @@ public sealed class RadarSnapshotProvider
|
||||||
|
|
||||||
public RadarSnapshotProvider(
|
public RadarSnapshotProvider(
|
||||||
ClientObjectTable objects,
|
ClientObjectTable objects,
|
||||||
Func<IReadOnlyDictionary<uint, WorldEntity>> worldEntities,
|
ILiveEntityRadarSource liveEntities,
|
||||||
Func<IReadOnlyDictionary<uint, WorldSession.EntitySpawn>> spawns,
|
Func<IReadOnlyDictionary<uint, WorldSession.EntitySpawn>> spawns,
|
||||||
Func<uint> playerGuid,
|
Func<uint> playerGuid,
|
||||||
Func<float> playerYawRadians,
|
Func<float> playerYawRadians,
|
||||||
|
|
@ -44,12 +43,10 @@ public sealed class RadarSnapshotProvider
|
||||||
Func<bool> coordinatesOnRadar,
|
Func<bool> coordinatesOnRadar,
|
||||||
Func<bool> uiLocked,
|
Func<bool> uiLocked,
|
||||||
Func<uint, RadarRelationshipTraits>? relationshipFor = null,
|
Func<uint, RadarRelationshipTraits>? relationshipFor = null,
|
||||||
Func<IReadOnlyDictionary<uint, WorldEntity>>? playerEntities = null,
|
|
||||||
Func<ILiveEntitySpatialQuery?>? spatialQuery = null)
|
Func<ILiveEntitySpatialQuery?>? spatialQuery = null)
|
||||||
{
|
{
|
||||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||||
_worldEntities = worldEntities ?? throw new ArgumentNullException(nameof(worldEntities));
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||||
_playerEntities = playerEntities ?? worldEntities;
|
|
||||||
_spawns = spawns ?? throw new ArgumentNullException(nameof(spawns));
|
_spawns = spawns ?? throw new ArgumentNullException(nameof(spawns));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||||
_playerYawRadians = playerYawRadians ?? throw new ArgumentNullException(nameof(playerYawRadians));
|
_playerYawRadians = playerYawRadians ?? throw new ArgumentNullException(nameof(playerYawRadians));
|
||||||
|
|
@ -67,13 +64,14 @@ public sealed class RadarSnapshotProvider
|
||||||
// startup. Resolve these canonical projections per snapshot so the
|
// startup. Resolve these canonical projections per snapshot so the
|
||||||
// provider observes that runtime once it is installed, rather than
|
// provider observes that runtime once it is installed, rather than
|
||||||
// retaining the bootstrap empty-map sentinels forever.
|
// retaining the bootstrap empty-map sentinels forever.
|
||||||
IReadOnlyDictionary<uint, WorldEntity> worldEntities = _worldEntities();
|
|
||||||
IReadOnlyDictionary<uint, WorldEntity> playerEntities = _playerEntities();
|
|
||||||
IReadOnlyDictionary<uint, WorldSession.EntitySpawn> spawns = _spawns();
|
IReadOnlyDictionary<uint, WorldSession.EntitySpawn> spawns = _spawns();
|
||||||
|
|
||||||
bool uiLocked = _uiLocked();
|
bool uiLocked = _uiLocked();
|
||||||
uint playerGuid = _playerGuid();
|
uint playerGuid = _playerGuid();
|
||||||
if (playerGuid == 0u || !playerEntities.TryGetValue(playerGuid, out var playerEntity))
|
if (playerGuid == 0u
|
||||||
|
|| !_liveEntities.TryGetMaterialized(
|
||||||
|
playerGuid,
|
||||||
|
out WorldEntity playerEntity))
|
||||||
return UiRadarSnapshot.Empty with { UiLocked = uiLocked };
|
return UiRadarSnapshot.Empty with { UiLocked = uiLocked };
|
||||||
|
|
||||||
float heading = MoveToMath.HeadingFromYaw(_playerYawRadians());
|
float heading = MoveToMath.HeadingFromYaw(_playerYawRadians());
|
||||||
|
|
@ -104,8 +102,7 @@ public sealed class RadarSnapshotProvider
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var pair in worldEntities)
|
_liveEntities.CopyVisibleTo(_candidateScratch);
|
||||||
_candidateScratch.Add(pair);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var blips = new List<UiRadarBlip>(Math.Min(_candidateScratch.Count, 64));
|
var blips = new List<UiRadarBlip>(Math.Min(_candidateScratch.Count, 64));
|
||||||
|
|
@ -116,7 +113,7 @@ public sealed class RadarSnapshotProvider
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var entity = pair.Value;
|
var entity = pair.Value;
|
||||||
if (!worldEntities.TryGetValue(guid, out WorldEntity? visibleEntity)
|
if (!_liveEntities.TryGetVisible(guid, out WorldEntity? visibleEntity)
|
||||||
|| !ReferenceEquals(entity, visibleEntity))
|
|| !ReferenceEquals(entity, visibleEntity))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.UI.Abstractions.Panels.Settings;
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
|
|
||||||
namespace AcDream.App.Update;
|
namespace AcDream.App.Update;
|
||||||
|
|
@ -196,12 +197,12 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
_selectionInteractions?.DrainOutbound();
|
_selectionInteractions?.DrainOutbound();
|
||||||
|
|
||||||
Vector3? playerPosition =
|
Vector3? playerPosition =
|
||||||
_liveEntities.MaterializedWorldEntities.TryGetValue(
|
_liveEntities.TryGetWorldEntity(
|
||||||
_localPlayer.ServerGuid,
|
_localPlayer.ServerGuid,
|
||||||
out WorldEntity? player)
|
out WorldEntity? player)
|
||||||
? player.Position
|
? player.Position
|
||||||
: null;
|
: null;
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules =
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules =
|
||||||
_animations.Tick(
|
_animations.Tick(
|
||||||
deltaSeconds,
|
deltaSeconds,
|
||||||
playerPosition,
|
playerPosition,
|
||||||
|
|
|
||||||
15
src/AcDream.App/World/ILiveEntityRadarSource.cs
Normal file
15
src/AcDream.App/World/ILiveEntityRadarSource.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocation-free live-projection query used by the radar. Implementations
|
||||||
|
/// expose current presentation state without publishing a second GUID identity
|
||||||
|
/// map.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILiveEntityRadarSource
|
||||||
|
{
|
||||||
|
bool TryGetMaterialized(uint serverGuid, out WorldEntity entity);
|
||||||
|
bool TryGetVisible(uint serverGuid, out WorldEntity entity);
|
||||||
|
void CopyVisibleTo(List<KeyValuePair<uint, WorldEntity>> destination);
|
||||||
|
}
|
||||||
|
|
@ -78,9 +78,9 @@ internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink
|
||||||
public bool Prune(LiveEntityPruneCandidate candidate)
|
public bool Prune(LiveEntityPruneCandidate candidate)
|
||||||
{
|
{
|
||||||
if (!_runtime.TryGetRecord(
|
if (!_runtime.TryGetRecord(
|
||||||
candidate.ServerGuid,
|
candidate.Key,
|
||||||
out LiveEntityRecord record)
|
out LiveEntityRecord record)
|
||||||
|| record.Generation != candidate.Generation)
|
|| record.ServerGuid != candidate.ServerGuid)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ internal enum LiveProjectionPurpose
|
||||||
internal interface ILiveEntityProjectionMaterializer
|
internal interface ILiveEntityProjectionMaterializer
|
||||||
{
|
{
|
||||||
bool TryMaterialize(
|
bool TryMaterialize(
|
||||||
LiveEntityRecord expectedRecord,
|
RuntimeEntityRecord expectedCanonical,
|
||||||
WorldSession.EntitySpawn canonicalSpawn,
|
WorldSession.EntitySpawn canonicalSpawn,
|
||||||
LiveProjectionPurpose purpose,
|
LiveProjectionPurpose purpose,
|
||||||
ulong expectedCreateIntegrationVersion,
|
ulong expectedCreateIntegrationVersion,
|
||||||
|
|
@ -181,6 +181,15 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
private readonly LiveEntityDeletionController _deletion;
|
private readonly LiveEntityDeletionController _deletion;
|
||||||
private readonly DormantLiveEntityStore _dormant;
|
private readonly DormantLiveEntityStore _dormant;
|
||||||
private readonly Action<string>? _diagnostic;
|
private readonly Action<string>? _diagnostic;
|
||||||
|
private readonly Dictionary<RuntimeEntityRecord, CanonicalProjectionOperation>
|
||||||
|
_projectionOperations =
|
||||||
|
new(ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
|
private sealed class CanonicalProjectionOperation
|
||||||
|
{
|
||||||
|
public ulong CreateIntegrationVersion { get; set; }
|
||||||
|
public bool RetryRequested { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public LiveEntityHydrationController(
|
public LiveEntityHydrationController(
|
||||||
LiveEntityRuntime runtime,
|
LiveEntityRuntime runtime,
|
||||||
|
|
@ -264,17 +273,17 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
// dormant snapshot until an active record actually accepts
|
// dormant snapshot until an active record actually accepts
|
||||||
// ownership; otherwise a transient teardown failure would discard
|
// ownership; otherwise a transient teardown failure would discard
|
||||||
// the only ACE revisit source.
|
// the only ACE revisit source.
|
||||||
if (registration.Record is not { } record)
|
if (registration.Canonical is not { } canonical)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_dormant.RemoveThroughAcceptedCreate(spawn);
|
_dormant.RemoveThroughAcceptedCreate(spawn);
|
||||||
ulong createIntegrationVersion = record.CreateIntegrationVersion;
|
ulong createIntegrationVersion = canonical.CreateIntegrationVersion;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_timestamps.Publish(spawn.Guid, result.Timestamps);
|
_timestamps.Publish(spawn.Guid, result.Timestamps);
|
||||||
if (_runtime.IsCurrentCreateIntegration(
|
if (_runtime.IsCurrentCreateIntegration(
|
||||||
record,
|
canonical,
|
||||||
createIntegrationVersion)
|
createIntegrationVersion)
|
||||||
&& ObjectTableWiring.ApplyEntitySpawn(
|
&& ObjectTableWiring.ApplyEntitySpawn(
|
||||||
_objects,
|
_objects,
|
||||||
|
|
@ -283,10 +292,10 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
AcDream.Core.Physics.CreateObjectTimestampDisposition.NewGeneration
|
AcDream.Core.Physics.CreateObjectTimestampDisposition.NewGeneration
|
||||||
|| dormantDisposition is DormantCreateDisposition.NewGeneration,
|
|| dormantDisposition is DormantCreateDisposition.NewGeneration,
|
||||||
accepting: () => _runtime.IsCurrentCreateIntegration(
|
accepting: () => _runtime.IsCurrentCreateIntegration(
|
||||||
record,
|
canonical,
|
||||||
createIntegrationVersion))
|
createIntegrationVersion))
|
||||||
&& _runtime.IsCurrentCreateIntegration(
|
&& _runtime.IsCurrentCreateIntegration(
|
||||||
record,
|
canonical,
|
||||||
createIntegrationVersion))
|
createIntegrationVersion))
|
||||||
{
|
{
|
||||||
if (result.Disposition is
|
if (result.Disposition is
|
||||||
|
|
@ -300,7 +309,7 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
// that snapshot rather than replaying a delta tail
|
// that snapshot rather than replaying a delta tail
|
||||||
// against owners that did not exist.
|
// against owners that did not exist.
|
||||||
ProjectExact(
|
ProjectExact(
|
||||||
record,
|
canonical,
|
||||||
result.Snapshot,
|
result.Snapshot,
|
||||||
LiveProjectionPurpose.LogicalRegistration,
|
LiveProjectionPurpose.LogicalRegistration,
|
||||||
expectedCreateIntegrationVersion:
|
expectedCreateIntegrationVersion:
|
||||||
|
|
@ -312,15 +321,23 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
_networkUpdates.ApplySameGeneration(refresh);
|
_networkUpdates.ApplySameGeneration(refresh);
|
||||||
|
|
||||||
if (_runtime.IsCurrentCreateIntegration(
|
if (_runtime.IsCurrentCreateIntegration(
|
||||||
record,
|
canonical,
|
||||||
createIntegrationVersion)
|
createIntegrationVersion))
|
||||||
&& (record.CreateProjectionSynchronizationPending
|
|
||||||
|| !record.InitialHydrationCompleted))
|
|
||||||
{
|
{
|
||||||
|
_runtime.TryGetProjection(
|
||||||
|
canonical,
|
||||||
|
out LiveEntityRecord? projection);
|
||||||
|
if (projection is not null
|
||||||
|
&& !projection.CreateProjectionSynchronizationPending
|
||||||
|
&& projection.InitialHydrationCompleted)
|
||||||
|
{
|
||||||
|
goto AppearanceSynchronization;
|
||||||
|
}
|
||||||
ProjectExact(
|
ProjectExact(
|
||||||
record,
|
canonical,
|
||||||
record.Snapshot,
|
canonical.Snapshot,
|
||||||
record.CreateProjectionSynchronizationPending
|
projection?.CreateProjectionSynchronizationPending
|
||||||
|
is true
|
||||||
? LiveProjectionPurpose.CreateSupersessionRecovery
|
? LiveProjectionPurpose.CreateSupersessionRecovery
|
||||||
: LiveProjectionPurpose.SpatialRecovery,
|
: LiveProjectionPurpose.SpatialRecovery,
|
||||||
expectedCreateIntegrationVersion:
|
expectedCreateIntegrationVersion:
|
||||||
|
|
@ -331,19 +348,22 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ProjectExact(
|
ProjectExact(
|
||||||
record,
|
canonical,
|
||||||
result.Snapshot,
|
result.Snapshot,
|
||||||
LiveProjectionPurpose.LogicalRegistration,
|
LiveProjectionPurpose.LogicalRegistration,
|
||||||
expectedCreateIntegrationVersion:
|
expectedCreateIntegrationVersion:
|
||||||
createIntegrationVersion);
|
createIntegrationVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_runtime.IsCurrentRecord(record)
|
AppearanceSynchronization:
|
||||||
&& record.AppearanceProjectionSynchronizationPending)
|
if (_runtime.TryGetProjection(
|
||||||
|
canonical,
|
||||||
|
out LiveEntityRecord? currentProjection)
|
||||||
|
&& currentProjection.AppearanceProjectionSynchronizationPending)
|
||||||
{
|
{
|
||||||
SynchronizeAppearance(
|
SynchronizeAppearance(
|
||||||
record,
|
currentProjection,
|
||||||
record.ObjDescAuthorityVersion);
|
currentProjection.ObjDescAuthorityVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -375,12 +395,25 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
{
|
{
|
||||||
lock (_datLock)
|
lock (_datLock)
|
||||||
{
|
{
|
||||||
if (!_runtime.TryApplyObjDesc(update, out _)
|
if (!_runtime.TryApplyObjDesc(update, out _))
|
||||||
|| !_runtime.TryGetRecord(update.Guid, out LiveEntityRecord record))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_runtime.TryGetRecord(
|
||||||
|
update.Guid,
|
||||||
|
out LiveEntityRecord record))
|
||||||
|
{
|
||||||
|
return _runtime.TryGetCanonical(
|
||||||
|
update.Guid,
|
||||||
|
out RuntimeEntityRecord canonical)
|
||||||
|
&& ProjectExact(
|
||||||
|
canonical,
|
||||||
|
canonical.Snapshot,
|
||||||
|
LiveProjectionPurpose.SpatialRecovery,
|
||||||
|
canonical.CreateIntegrationVersion);
|
||||||
|
}
|
||||||
|
|
||||||
return SynchronizeAppearance(
|
return SynchronizeAppearance(
|
||||||
record,
|
record,
|
||||||
record.ObjDescAuthorityVersion);
|
record.ObjDescAuthorityVersion);
|
||||||
|
|
@ -466,42 +499,64 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
if (_runtime.Count == 0)
|
if (_runtime.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
uint canonicalLandblock =
|
||||||
LiveEntityRecord[] records = _runtime.Records
|
(loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
.Where(record => (record.ServerGuid != _identity.ServerGuid
|
var candidates = new List<RuntimeEntityRecord>();
|
||||||
|| !record.InitialHydrationCompleted
|
foreach (RuntimeEntityRecord candidate in _runtime.CanonicalRecords)
|
||||||
|| record.CreateProjectionSynchronizationPending
|
{
|
||||||
|| record.AppearanceProjectionSynchronizationPending)
|
_runtime.TryGetProjection(
|
||||||
&& record.ProjectionCellId != 0
|
candidate,
|
||||||
&& ((record.ProjectionCellId & 0xFFFF0000u) | 0xFFFFu) == canonical
|
out LiveEntityRecord? projection);
|
||||||
&& record.Snapshot.SetupTableId is not null)
|
if (candidate.ServerGuid == _identity.ServerGuid
|
||||||
.ToArray();
|
&& projection is not null
|
||||||
if (records.Length == 0)
|
&& projection.InitialHydrationCompleted
|
||||||
|
&& !projection.CreateProjectionSynchronizationPending
|
||||||
|
&& !projection.AppearanceProjectionSynchronizationPending)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint projectionCellId = projection?.ProjectionCellId
|
||||||
|
?? candidate.Snapshot.Position?.LandblockId
|
||||||
|
?? candidate.FullCellId;
|
||||||
|
if (projectionCellId != 0
|
||||||
|
&& ((projectionCellId & 0xFFFF0000u) | 0xFFFFu)
|
||||||
|
== canonicalLandblock
|
||||||
|
&& candidate.Snapshot.SetupTableId is not null)
|
||||||
|
{
|
||||||
|
candidates.Add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int projected = 0;
|
int projected = 0;
|
||||||
lock (_datLock)
|
lock (_datLock)
|
||||||
{
|
{
|
||||||
foreach (LiveEntityRecord record in records)
|
foreach (RuntimeEntityRecord candidate in candidates)
|
||||||
{
|
{
|
||||||
if (!_runtime.IsCurrentRecord(record))
|
if (!_runtime.IsCurrentCanonical(candidate))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ulong projectedObjDescVersion = record.ObjDescAuthorityVersion;
|
_runtime.TryGetProjection(
|
||||||
|
candidate,
|
||||||
|
out LiveEntityRecord? record);
|
||||||
|
ulong projectedObjDescVersion =
|
||||||
|
candidate.ObjDescAuthorityVersion;
|
||||||
bool projectedCanonicalAppearance = false;
|
bool projectedCanonicalAppearance = false;
|
||||||
|
|
||||||
if (record.CreateProjectionSynchronizationPending)
|
if (record?.CreateProjectionSynchronizationPending is true)
|
||||||
{
|
{
|
||||||
if (ProjectExact(
|
if (ProjectExact(
|
||||||
record,
|
candidate,
|
||||||
record.Snapshot,
|
candidate.Snapshot,
|
||||||
LiveProjectionPurpose.CreateSupersessionRecovery))
|
LiveProjectionPurpose.CreateSupersessionRecovery))
|
||||||
{
|
{
|
||||||
projected++;
|
projected++;
|
||||||
projectedCanonicalAppearance = true;
|
projectedCanonicalAppearance = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (record.WorldEntity is not null
|
else if (record?.WorldEntity is not null
|
||||||
&& record.InitialHydrationCompleted)
|
&& record.InitialHydrationCompleted)
|
||||||
{
|
{
|
||||||
if (_runtime.RebucketLiveEntity(
|
if (_runtime.RebucketLiveEntity(
|
||||||
|
|
@ -512,15 +567,17 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (ProjectExact(
|
else if (ProjectExact(
|
||||||
record,
|
candidate,
|
||||||
record.Snapshot,
|
candidate.Snapshot,
|
||||||
LiveProjectionPurpose.SpatialRecovery))
|
LiveProjectionPurpose.SpatialRecovery))
|
||||||
{
|
{
|
||||||
projected++;
|
projected++;
|
||||||
projectedCanonicalAppearance = true;
|
projectedCanonicalAppearance = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectedCanonicalAppearance
|
_runtime.TryGetProjection(candidate, out record);
|
||||||
|
if (record is not null
|
||||||
|
&& projectedCanonicalAppearance
|
||||||
&& record.AppearanceProjectionSynchronizationPending)
|
&& record.AppearanceProjectionSynchronizationPending)
|
||||||
{
|
{
|
||||||
CompleteAppearanceProjectionSynchronization(
|
CompleteAppearanceProjectionSynchronization(
|
||||||
|
|
@ -528,7 +585,8 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
projectedObjDescVersion);
|
projectedObjDescVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_runtime.IsCurrentRecord(record)
|
if (record is not null
|
||||||
|
&& _runtime.IsCurrentRecord(record)
|
||||||
&& record.AppearanceProjectionSynchronizationPending
|
&& record.AppearanceProjectionSynchronizationPending
|
||||||
&& SynchronizeAppearance(
|
&& SynchronizeAppearance(
|
||||||
record,
|
record,
|
||||||
|
|
@ -684,7 +742,7 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
published = _materializer.TryMaterialize(
|
published = _materializer.TryMaterialize(
|
||||||
expectedRecord,
|
expectedRecord.Canonical,
|
||||||
expectedRecord.Snapshot,
|
expectedRecord.Snapshot,
|
||||||
LiveProjectionPurpose.AppearanceMutation,
|
LiveProjectionPurpose.AppearanceMutation,
|
||||||
expectedRecord.CreateIntegrationVersion,
|
expectedRecord.CreateIntegrationVersion,
|
||||||
|
|
@ -715,78 +773,138 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
LiveEntityRecord expectedRecord,
|
LiveEntityRecord expectedRecord,
|
||||||
WorldSession.EntitySpawn acceptedSpawn,
|
WorldSession.EntitySpawn acceptedSpawn,
|
||||||
LiveProjectionPurpose purpose,
|
LiveProjectionPurpose purpose,
|
||||||
|
ulong? expectedCreateIntegrationVersion = null) =>
|
||||||
|
ProjectExact(
|
||||||
|
expectedRecord.Canonical,
|
||||||
|
acceptedSpawn,
|
||||||
|
purpose,
|
||||||
|
expectedCreateIntegrationVersion);
|
||||||
|
|
||||||
|
private bool ProjectExact(
|
||||||
|
RuntimeEntityRecord expectedCanonical,
|
||||||
|
WorldSession.EntitySpawn acceptedSpawn,
|
||||||
|
LiveProjectionPurpose purpose,
|
||||||
ulong? expectedCreateIntegrationVersion = null)
|
ulong? expectedCreateIntegrationVersion = null)
|
||||||
{
|
{
|
||||||
while (true)
|
if (_projectionOperations.TryGetValue(
|
||||||
|
expectedCanonical,
|
||||||
|
out CanonicalProjectionOperation? active))
|
||||||
{
|
{
|
||||||
ulong createIntegrationVersion = expectedCreateIntegrationVersion
|
if (expectedCanonical.CreateIntegrationVersion
|
||||||
?? expectedRecord.CreateIntegrationVersion;
|
!= active.CreateIntegrationVersion)
|
||||||
if (purpose is LiveProjectionPurpose.CreateSupersessionRecovery
|
|
||||||
&& !_runtime.TryMarkCreateProjectionSynchronizationPending(
|
|
||||||
expectedRecord,
|
|
||||||
createIntegrationVersion))
|
|
||||||
{
|
{
|
||||||
return false;
|
active.RetryRequested = true;
|
||||||
}
|
|
||||||
if (!_runtime.TryBeginProjectionHydration(
|
|
||||||
expectedRecord,
|
|
||||||
createIntegrationVersion))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool retry;
|
var operation = new CanonicalProjectionOperation();
|
||||||
bool result;
|
_projectionOperations.Add(expectedCanonical, operation);
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
result = ProjectExactOnce(
|
ulong createIntegrationVersion = expectedCreateIntegrationVersion
|
||||||
expectedRecord,
|
?? expectedCanonical.CreateIntegrationVersion;
|
||||||
|
if (!_runtime.IsCurrentCreateIntegration(
|
||||||
|
expectedCanonical,
|
||||||
|
createIntegrationVersion))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (purpose is LiveProjectionPurpose.CreateSupersessionRecovery
|
||||||
|
&& _runtime.TryGetProjection(
|
||||||
|
expectedCanonical,
|
||||||
|
out LiveEntityRecord? synchronizedProjection)
|
||||||
|
&& !_runtime.TryMarkCreateProjectionSynchronizationPending(
|
||||||
|
synchronizedProjection,
|
||||||
|
createIntegrationVersion))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.RetryRequested = false;
|
||||||
|
operation.CreateIntegrationVersion =
|
||||||
|
createIntegrationVersion;
|
||||||
|
bool result = ProjectExactOnce(
|
||||||
|
expectedCanonical,
|
||||||
acceptedSpawn,
|
acceptedSpawn,
|
||||||
purpose,
|
purpose,
|
||||||
createIntegrationVersion);
|
createIntegrationVersion);
|
||||||
|
|
||||||
|
bool retry = operation.RetryRequested
|
||||||
|
|| (_runtime.IsCurrentCanonical(expectedCanonical)
|
||||||
|
&& expectedCanonical.CreateIntegrationVersion
|
||||||
|
!= createIntegrationVersion);
|
||||||
|
if (!retry || !_runtime.IsCurrentCanonical(expectedCanonical))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// A fresher same-incarnation CreateObject or a synchronous
|
||||||
|
// loaded-landblock callback re-entered this exact canonical
|
||||||
|
// construction. Resume immediately from Runtime's newest
|
||||||
|
// state; no projection work is deferred to another frame.
|
||||||
|
if (_runtime.TryGetProjection(
|
||||||
|
expectedCanonical,
|
||||||
|
out LiveEntityRecord? retryProjection))
|
||||||
|
{
|
||||||
|
retryProjection.CreateProjectionSynchronizationPending = true;
|
||||||
|
}
|
||||||
|
acceptedSpawn = expectedCanonical.Snapshot;
|
||||||
|
purpose = LiveProjectionPurpose.CreateSupersessionRecovery;
|
||||||
|
expectedCreateIntegrationVersion =
|
||||||
|
expectedCanonical.CreateIntegrationVersion;
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (_runtime.IsCurrentCanonical(expectedCanonical)
|
||||||
|
&& _runtime.TryGetProjection(
|
||||||
|
expectedCanonical,
|
||||||
|
out LiveEntityRecord? interruptedProjection))
|
||||||
{
|
{
|
||||||
retry = _runtime.EndProjectionHydration(expectedRecord);
|
interruptedProjection.CreateProjectionSynchronizationPending = true;
|
||||||
|
}
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!_projectionOperations.Remove(expectedCanonical, out var removed)
|
||||||
|
|| !ReferenceEquals(removed, operation))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Canonical projection construction ownership was corrupted.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!retry || !_runtime.IsCurrentRecord(expectedRecord))
|
|
||||||
return result;
|
|
||||||
|
|
||||||
// A fresher same-incarnation CreateObject arrived while this
|
|
||||||
// transaction owned the hydration edge. Resume in-place from the
|
|
||||||
// newest canonical snapshot; logical resources remain registered.
|
|
||||||
acceptedSpawn = expectedRecord.Snapshot;
|
|
||||||
purpose = LiveProjectionPurpose.CreateSupersessionRecovery;
|
|
||||||
expectedCreateIntegrationVersion =
|
|
||||||
expectedRecord.CreateIntegrationVersion;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ProjectExactOnce(
|
private bool ProjectExactOnce(
|
||||||
LiveEntityRecord expectedRecord,
|
RuntimeEntityRecord expectedCanonical,
|
||||||
WorldSession.EntitySpawn acceptedSpawn,
|
WorldSession.EntitySpawn acceptedSpawn,
|
||||||
LiveProjectionPurpose purpose,
|
LiveProjectionPurpose purpose,
|
||||||
ulong createIntegrationVersion)
|
ulong createIntegrationVersion)
|
||||||
{
|
{
|
||||||
_relationships.OnSpawn(acceptedSpawn);
|
_relationships.OnSpawn(acceptedSpawn);
|
||||||
if (!_runtime.IsCurrentCreateIntegration(
|
if (!_runtime.IsCurrentCreateIntegration(
|
||||||
expectedRecord,
|
expectedCanonical,
|
||||||
createIntegrationVersion))
|
createIntegrationVersion))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// A queued Parent event can synchronously mutate the canonical
|
// A queued Parent event can synchronously mutate the canonical
|
||||||
// Position/Parent snapshot. Never continue from the stale argument.
|
// Position/Parent snapshot. Never continue from the stale argument.
|
||||||
WorldSession.EntitySpawn canonicalSpawn = expectedRecord.Snapshot;
|
WorldSession.EntitySpawn canonicalSpawn = expectedCanonical.Snapshot;
|
||||||
InitializeOriginAndRecoverLoaded(canonicalSpawn);
|
InitializeOriginAndRecoverLoaded(canonicalSpawn);
|
||||||
if (!_runtime.IsCurrentCreateIntegration(
|
if (!_runtime.IsCurrentCreateIntegration(
|
||||||
expectedRecord,
|
expectedCanonical,
|
||||||
createIntegrationVersion)
|
createIntegrationVersion)
|
||||||
|| !_origin.IsKnown)
|
|| !_origin.IsKnown)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_runtime.TryGetProjection(
|
||||||
|
expectedCanonical,
|
||||||
|
out LiveEntityRecord? expectedRecord);
|
||||||
|
|
||||||
// Retail's same-instance CreateObject branch completes a POSITION_TS
|
// Retail's same-instance CreateObject branch completes a POSITION_TS
|
||||||
// with no world Position through DoParentEvent or DoPickupEvent, not
|
// with no world Position through DoParentEvent or DoPickupEvent, not
|
||||||
// through CPhysicsObj::enter_world. The relationship/update owners
|
// through CPhysicsObj::enter_world. The relationship/update owners
|
||||||
|
|
@ -797,6 +915,9 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
{
|
{
|
||||||
if (canonicalSpawn.ParentGuid is not null and not 0)
|
if (canonicalSpawn.ParentGuid is not null and not 0)
|
||||||
{
|
{
|
||||||
|
if (expectedRecord is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (expectedRecord.InitialHydrationCompleted
|
if (expectedRecord.InitialHydrationCompleted
|
||||||
&& !expectedRecord.CreateProjectionSynchronizationPending)
|
&& !expectedRecord.CreateProjectionSynchronizationPending)
|
||||||
{
|
{
|
||||||
|
|
@ -822,19 +943,20 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
createIntegrationVersion);
|
createIntegrationVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedRecord.FullCellId != 0
|
if (expectedCanonical.FullCellId != 0
|
||||||
|| expectedRecord.IsSpatiallyProjected)
|
|| expectedRecord?.IsSpatiallyProjected is true)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedRecord.WorldEntity is null)
|
if (expectedRecord?.WorldEntity is null)
|
||||||
{
|
{
|
||||||
// A pickup can supersede initial hydration before a render
|
// A pickup can supersede initial hydration before a render
|
||||||
// projection exists. The logical snapshot/table state is
|
// projection exists. The logical snapshot/table state is
|
||||||
// already canonical; ready remains false so a later world
|
// already canonical; ready remains false so a later world
|
||||||
// Position still constructs and publishes the first owner.
|
// Position still constructs and publishes the first owner.
|
||||||
return !expectedRecord.CreateProjectionSynchronizationPending
|
return expectedRecord is null
|
||||||
|
|| !expectedRecord.CreateProjectionSynchronizationPending
|
||||||
|| _runtime.TryCompleteCreateProjectionSynchronization(
|
|| _runtime.TryCompleteCreateProjectionSynchronization(
|
||||||
expectedRecord,
|
expectedRecord,
|
||||||
createIntegrationVersion);
|
createIntegrationVersion);
|
||||||
|
|
@ -854,19 +976,26 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
|
||||||
}
|
}
|
||||||
|
|
||||||
bool materialized = _materializer.TryMaterialize(
|
bool materialized = _materializer.TryMaterialize(
|
||||||
expectedRecord,
|
expectedCanonical,
|
||||||
expectedRecord.Snapshot,
|
expectedCanonical.Snapshot,
|
||||||
purpose,
|
purpose,
|
||||||
createIntegrationVersion);
|
createIntegrationVersion);
|
||||||
if (!materialized
|
if (!materialized
|
||||||
|| !_runtime.IsCurrentCreateIntegration(
|
|| !_runtime.IsCurrentCreateIntegration(
|
||||||
expectedRecord,
|
expectedCanonical,
|
||||||
createIntegrationVersion)
|
createIntegrationVersion)
|
||||||
|| purpose is LiveProjectionPurpose.AppearanceMutation)
|
|| purpose is LiveProjectionPurpose.AppearanceMutation)
|
||||||
{
|
{
|
||||||
return materialized;
|
return materialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_runtime.TryGetProjection(
|
||||||
|
expectedCanonical,
|
||||||
|
out expectedRecord))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!PublishReady(
|
if (!PublishReady(
|
||||||
expectedRecord,
|
expectedRecord,
|
||||||
createIntegrationVersion))
|
createIntegrationVersion))
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
namespace AcDream.App.World;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Protects GUID-keyed runtime state while an older incarnation is draining.
|
|
||||||
/// A teardown tombstone may outlive the active record and a newer generation
|
|
||||||
/// may already own the same server GUID; only state captured by reference may
|
|
||||||
/// be removed unconditionally in that case.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class LiveEntityIncarnationCleanup
|
|
||||||
{
|
|
||||||
private readonly LiveEntityRecord _owner;
|
|
||||||
private readonly Func<uint, LiveEntityRecord?> _resolveCurrent;
|
|
||||||
|
|
||||||
public LiveEntityIncarnationCleanup(
|
|
||||||
LiveEntityRecord owner,
|
|
||||||
Func<uint, LiveEntityRecord?> resolveCurrent)
|
|
||||||
{
|
|
||||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
|
||||||
_resolveCurrent = resolveCurrent
|
|
||||||
?? throw new ArgumentNullException(nameof(resolveCurrent));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs a GUID-scoped mutation only while no replacement incarnation owns
|
|
||||||
/// that GUID. The original record may still be current (projection-only
|
|
||||||
/// withdrawal) or may already have moved into the teardown ledger.
|
|
||||||
/// </summary>
|
|
||||||
public void RunIfNoReplacement(Action mutation)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(mutation);
|
|
||||||
LiveEntityRecord? current = _resolveCurrent(_owner.ServerGuid);
|
|
||||||
if (current is null || ReferenceEquals(current, _owner))
|
|
||||||
mutation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a reference-owned dictionary entry only when the entry still
|
|
||||||
/// points at this incarnation's captured owner. This remains safe even if
|
|
||||||
/// a replacement has overwritten the same GUID between plan creation and
|
|
||||||
/// execution.
|
|
||||||
/// </summary>
|
|
||||||
public void RemoveCaptured<T>(
|
|
||||||
IDictionary<uint, T> owners,
|
|
||||||
T captured)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(owners);
|
|
||||||
ArgumentNullException.ThrowIfNull(captured);
|
|
||||||
if (owners.TryGetValue(_owner.ServerGuid, out T? current)
|
|
||||||
&& ReferenceEquals(current, captured))
|
|
||||||
{
|
|
||||||
owners.Remove(_owner.ServerGuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.World;
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
internal readonly record struct LiveEntityLivenessSample(
|
internal readonly record struct LiveEntityLivenessSample(
|
||||||
|
RuntimeEntityKey Key,
|
||||||
uint ServerGuid,
|
uint ServerGuid,
|
||||||
ushort Generation,
|
|
||||||
bool IsConservativelyVisible,
|
bool IsConservativelyVisible,
|
||||||
bool HasNonWorldRetention);
|
bool HasNonWorldRetention);
|
||||||
|
|
||||||
internal readonly record struct LiveEntityPruneCandidate(
|
internal readonly record struct LiveEntityPruneCandidate(
|
||||||
|
RuntimeEntityKey Key,
|
||||||
uint ServerGuid,
|
uint ServerGuid,
|
||||||
ushort Generation);
|
ushort Generation)
|
||||||
|
{
|
||||||
|
public LiveEntityPruneCandidate(RuntimeEntityKey key, uint serverGuid)
|
||||||
|
: this(key, serverGuid, key.Incarnation)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deadline state for ACE's retained KnownObjects behavior. Retail
|
/// Deadline state for ACE's retained KnownObjects behavior. Retail
|
||||||
|
|
@ -23,7 +31,10 @@ internal sealed class LiveEntityLivenessTracker
|
||||||
{
|
{
|
||||||
internal const double DestructionTimeoutSeconds = 25.0;
|
internal const double DestructionTimeoutSeconds = 25.0;
|
||||||
|
|
||||||
private readonly Dictionary<uint, Deadline> _deadlines = new();
|
private readonly Dictionary<RuntimeEntityKey, double> _deadlines = [];
|
||||||
|
private readonly HashSet<RuntimeEntityKey> _present = [];
|
||||||
|
private readonly List<RuntimeEntityKey> _stale = [];
|
||||||
|
private readonly List<LiveEntityPruneCandidate> _due = [];
|
||||||
|
|
||||||
internal int DeadlineCount => _deadlines.Count;
|
internal int DeadlineCount => _deadlines.Count;
|
||||||
|
|
||||||
|
|
@ -31,46 +42,44 @@ internal sealed class LiveEntityLivenessTracker
|
||||||
double now,
|
double now,
|
||||||
IReadOnlyList<LiveEntityLivenessSample> samples)
|
IReadOnlyList<LiveEntityLivenessSample> samples)
|
||||||
{
|
{
|
||||||
var present = new HashSet<uint>(samples.Count);
|
_present.Clear();
|
||||||
var due = new List<LiveEntityPruneCandidate>();
|
_due.Clear();
|
||||||
for (int i = 0; i < samples.Count; i++)
|
for (int i = 0; i < samples.Count; i++)
|
||||||
{
|
{
|
||||||
LiveEntityLivenessSample sample = samples[i];
|
LiveEntityLivenessSample sample = samples[i];
|
||||||
present.Add(sample.ServerGuid);
|
_present.Add(sample.Key);
|
||||||
if (sample.IsConservativelyVisible || sample.HasNonWorldRetention)
|
if (sample.IsConservativelyVisible || sample.HasNonWorldRetention)
|
||||||
{
|
{
|
||||||
_deadlines.Remove(sample.ServerGuid);
|
_deadlines.Remove(sample.Key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_deadlines.TryGetValue(sample.ServerGuid, out Deadline deadline)
|
if (!_deadlines.TryGetValue(sample.Key, out double expiresAt))
|
||||||
|| deadline.Generation != sample.Generation)
|
|
||||||
{
|
{
|
||||||
_deadlines[sample.ServerGuid] = new Deadline(
|
_deadlines[sample.Key] = now + DestructionTimeoutSeconds;
|
||||||
sample.Generation,
|
|
||||||
now + DestructionTimeoutSeconds);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deadline.ExpiresAt > now)
|
if (expiresAt > now)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
due.Add(new LiveEntityPruneCandidate(sample.ServerGuid, sample.Generation));
|
_due.Add(new LiveEntityPruneCandidate(sample.Key, sample.ServerGuid));
|
||||||
_deadlines.Remove(sample.ServerGuid);
|
_deadlines.Remove(sample.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint[] stale = _deadlines.Keys
|
_stale.Clear();
|
||||||
.Where(guid => !present.Contains(guid))
|
foreach (RuntimeEntityKey key in _deadlines.Keys)
|
||||||
.ToArray();
|
{
|
||||||
for (int i = 0; i < stale.Length; i++)
|
if (!_present.Contains(key))
|
||||||
_deadlines.Remove(stale[i]);
|
_stale.Add(key);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < _stale.Count; i++)
|
||||||
|
_deadlines.Remove(_stale[i]);
|
||||||
|
|
||||||
return due;
|
return _due;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Clear() => _deadlines.Clear();
|
internal void Clear() => _deadlines.Clear();
|
||||||
|
|
||||||
private readonly record struct Deadline(ushort Generation, double ExpiresAt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -134,8 +143,11 @@ internal sealed class LiveEntityLivenessController
|
||||||
|| NonZero(record.Snapshot.WielderId)
|
|| NonZero(record.Snapshot.WielderId)
|
||||||
|| NonZero(record.Snapshot.ParentGuid);
|
|| NonZero(record.Snapshot.ParentGuid);
|
||||||
_samples.Add(new LiveEntityLivenessSample(
|
_samples.Add(new LiveEntityLivenessSample(
|
||||||
|
record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Materialized liveness owner 0x{record.ServerGuid:X8}/" +
|
||||||
|
$"{record.Generation} has no exact projection key."),
|
||||||
record.ServerGuid,
|
record.ServerGuid,
|
||||||
record.Generation,
|
|
||||||
record.IsSpatiallyVisible
|
record.IsSpatiallyVisible
|
||||||
|| IsWithinConservativeVisibility(playerPosition, position),
|
|| IsWithinConservativeVisibility(playerPosition, position),
|
||||||
retained));
|
retained));
|
||||||
|
|
@ -145,8 +157,8 @@ internal sealed class LiveEntityLivenessController
|
||||||
for (int i = 0; i < due.Count; i++)
|
for (int i = 0; i < due.Count; i++)
|
||||||
{
|
{
|
||||||
LiveEntityPruneCandidate candidate = due[i];
|
LiveEntityPruneCandidate candidate = due[i];
|
||||||
if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current)
|
if (_runtime.TryGetRecord(candidate.Key, out LiveEntityRecord current)
|
||||||
&& current.Generation == candidate.Generation)
|
&& current.ServerGuid == candidate.ServerGuid)
|
||||||
{
|
{
|
||||||
_prune.Prune(candidate);
|
_prune.Prune(candidate);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.World;
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
|
@ -28,9 +29,9 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
private readonly Func<(int X, int Y)> _liveCenter;
|
private readonly Func<(int X, int Y)> _liveCenter;
|
||||||
private readonly Action<uint>? _onShadowRestored;
|
private readonly Action<uint>? _onShadowRestored;
|
||||||
private readonly LiveEntityPartArrayEnterWorldPort _partArrayEnterWorld;
|
private readonly LiveEntityPartArrayEnterWorldPort _partArrayEnterWorld;
|
||||||
private readonly Dictionary<uint, ushort> _readyGenerationByGuid = new();
|
private readonly HashSet<RuntimeEntityKey> _readyOwners = [];
|
||||||
private readonly Dictionary<uint, ushort> _suspendedShadowGenerationByGuid = new();
|
private readonly HashSet<RuntimeEntityKey> _suspendedShadowOwners = [];
|
||||||
private readonly Dictionary<uint, ushort> _activePlacementGenerationByGuid = new();
|
private readonly HashSet<RuntimeEntityKey> _activePlacementOwners = [];
|
||||||
private readonly HashSet<LiveEntityRecord> _drainingRecords = new();
|
private readonly HashSet<LiveEntityRecord> _drainingRecords = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
@ -70,7 +71,8 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_readyGenerationByGuid[serverGuid] = record.Generation;
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
|
_readyOwners.Add(key);
|
||||||
if (!ApplyPendingTransitions(record)
|
if (!ApplyPendingTransitions(record)
|
||||||
|| record.WorldEntity is not { } entity
|
|| record.WorldEntity is not { } entity
|
||||||
|| !IsCurrent(record, entity))
|
|| !IsCurrent(record, entity))
|
||||||
|
|
@ -91,8 +93,8 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
public bool OnStateAccepted(uint serverGuid)
|
public bool OnStateAccepted(uint serverGuid)
|
||||||
{
|
{
|
||||||
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||||
|| !_readyGenerationByGuid.TryGetValue(serverGuid, out ushort generation)
|
|| !TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||||
|| generation != record.Generation)
|
|| !_readyOwners.Contains(key))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -126,21 +128,13 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_activePlacementGenerationByGuid.TryGetValue(
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
serverGuid,
|
_activePlacementOwners.Remove(key);
|
||||||
out ushort activeGeneration)
|
|
||||||
&& activeGeneration == generation)
|
|
||||||
{
|
|
||||||
_activePlacementGenerationByGuid.Remove(serverGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deferShadowRestore)
|
if (deferShadowRestore)
|
||||||
_suspendedShadowGenerationByGuid[serverGuid] = generation;
|
_suspendedShadowOwners.Add(key);
|
||||||
else if (_suspendedShadowGenerationByGuid.TryGetValue(
|
else
|
||||||
serverGuid,
|
_suspendedShadowOwners.Remove(key);
|
||||||
out ushort deferredGeneration)
|
|
||||||
&& deferredGeneration == generation)
|
|
||||||
_suspendedShadowGenerationByGuid.Remove(serverGuid);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,57 +152,41 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_activePlacementGenerationByGuid[serverGuid] = generation;
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
if (_suspendedShadowGenerationByGuid.TryGetValue(
|
_activePlacementOwners.Add(key);
|
||||||
serverGuid,
|
_suspendedShadowOwners.Remove(key);
|
||||||
out ushort deferredGeneration)
|
|
||||||
&& deferredGeneration == generation)
|
|
||||||
{
|
|
||||||
_suspendedShadowGenerationByGuid.Remove(serverGuid);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool HasDeferredShadowRestore(uint serverGuid) =>
|
internal bool HasDeferredShadowRestore(uint serverGuid) =>
|
||||||
_suspendedShadowGenerationByGuid.ContainsKey(serverGuid);
|
TryGetCurrentProjectionKey(serverGuid, out RuntimeEntityKey key)
|
||||||
|
&& _suspendedShadowOwners.Contains(key);
|
||||||
|
|
||||||
internal bool HasActivePlacement(uint serverGuid) =>
|
internal bool HasActivePlacement(uint serverGuid) =>
|
||||||
_activePlacementGenerationByGuid.ContainsKey(serverGuid);
|
TryGetCurrentProjectionKey(serverGuid, out RuntimeEntityKey key)
|
||||||
|
&& _activePlacementOwners.Contains(key);
|
||||||
|
|
||||||
internal int ReadyOwnerCount => _readyGenerationByGuid.Count;
|
internal int ReadyOwnerCount => _readyOwners.Count;
|
||||||
internal int DeferredShadowRestoreCount => _suspendedShadowGenerationByGuid.Count;
|
internal int DeferredShadowRestoreCount => _suspendedShadowOwners.Count;
|
||||||
internal int ActivePlacementCount => _activePlacementGenerationByGuid.Count;
|
internal int ActivePlacementCount => _activePlacementOwners.Count;
|
||||||
|
|
||||||
public void Forget(LiveEntityRecord record)
|
public void Forget(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
if (_readyGenerationByGuid.TryGetValue(record.ServerGuid, out ushort generation)
|
if (TryGetProjectionKey(record, out RuntimeEntityKey key))
|
||||||
&& generation == record.Generation)
|
|
||||||
{
|
{
|
||||||
_readyGenerationByGuid.Remove(record.ServerGuid);
|
_readyOwners.Remove(key);
|
||||||
}
|
_suspendedShadowOwners.Remove(key);
|
||||||
if (_suspendedShadowGenerationByGuid.TryGetValue(
|
_activePlacementOwners.Remove(key);
|
||||||
record.ServerGuid,
|
|
||||||
out ushort suspendedGeneration)
|
|
||||||
&& suspendedGeneration == record.Generation)
|
|
||||||
{
|
|
||||||
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
|
|
||||||
}
|
|
||||||
if (_activePlacementGenerationByGuid.TryGetValue(
|
|
||||||
record.ServerGuid,
|
|
||||||
out ushort activeGeneration)
|
|
||||||
&& activeGeneration == record.Generation)
|
|
||||||
{
|
|
||||||
_activePlacementGenerationByGuid.Remove(record.ServerGuid);
|
|
||||||
}
|
}
|
||||||
_drainingRecords.Remove(record);
|
_drainingRecords.Remove(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_readyGenerationByGuid.Clear();
|
_readyOwners.Clear();
|
||||||
_suspendedShadowGenerationByGuid.Clear();
|
_suspendedShadowOwners.Clear();
|
||||||
_activePlacementGenerationByGuid.Clear();
|
_activePlacementOwners.Clear();
|
||||||
_drainingRecords.Clear();
|
_drainingRecords.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +234,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
_shadows.Suspend(entity.Id);
|
_shadows.Suspend(entity.Id);
|
||||||
if (!IsPlacementActive(record))
|
if (!IsPlacementActive(record))
|
||||||
_suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation;
|
_suspendedShadowOwners.Add(RequireProjectionKey(record));
|
||||||
// Retail CPhysicsObj::set_hidden @ 0x00514C60 calls
|
// Retail CPhysicsObj::set_hidden @ 0x00514C60 calls
|
||||||
// CPartArray::HandleEnterWorld after hiding the object
|
// CPartArray::HandleEnterWorld after hiding the object
|
||||||
// from its cell. Despite the name, this is the motion
|
// from its cell. Despite the name, this is the motion
|
||||||
|
|
@ -288,7 +266,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
if (!IsCurrent(record, entity))
|
if (!IsCurrent(record, entity))
|
||||||
return false;
|
return false;
|
||||||
if (restored)
|
if (restored)
|
||||||
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
|
_suspendedShadowOwners.Remove(RequireProjectionKey(record));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -345,12 +323,9 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
|
|
||||||
if ((record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0
|
if ((record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0
|
||||||
|| IsPlacementActive(record)
|
|| IsPlacementActive(record)
|
||||||
|| !_readyGenerationByGuid.TryGetValue(record.ServerGuid, out ushort readyGeneration)
|
|| !TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||||
|| readyGeneration != record.Generation
|
|| !_readyOwners.Contains(key)
|
||||||
|| !_suspendedShadowGenerationByGuid.TryGetValue(
|
|| !_suspendedShadowOwners.Contains(key))
|
||||||
record.ServerGuid,
|
|
||||||
out ushort suspendedGeneration)
|
|
||||||
|| suspendedGeneration != record.Generation)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -359,7 +334,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
if (!IsCurrent(record, entity))
|
if (!IsCurrent(record, entity))
|
||||||
return;
|
return;
|
||||||
if (restored)
|
if (restored)
|
||||||
_suspendedShadowGenerationByGuid.Remove(record.ServerGuid);
|
_suspendedShadowOwners.Remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SuspendOrdinaryShadowOutsideProjection(
|
private void SuspendOrdinaryShadowOutsideProjection(
|
||||||
|
|
@ -369,16 +344,14 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
if (record.IsSpatiallyVisible
|
if (record.IsSpatiallyVisible
|
||||||
|| record.ProjectileRuntime is not null
|
|| record.ProjectileRuntime is not null
|
||||||
|| IsPlacementActive(record)
|
|| IsPlacementActive(record)
|
||||||
|| !_readyGenerationByGuid.TryGetValue(
|
|| !TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||||
record.ServerGuid,
|
|| !_readyOwners.Contains(key)
|
||||||
out ushort readyGeneration)
|
|
||||||
|| readyGeneration != record.Generation
|
|
||||||
|| !_shadows.Suspend(entity.Id))
|
|| !_shadows.Suspend(entity.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_suspendedShadowGenerationByGuid[record.ServerGuid] = record.Generation;
|
_suspendedShadowOwners.Add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsCurrent(
|
private bool IsCurrent(
|
||||||
|
|
@ -390,8 +363,42 @@ public sealed class LiveEntityPresentationController : IDisposable
|
||||||
&& ReferenceEquals(current.WorldEntity, entity);
|
&& ReferenceEquals(current.WorldEntity, entity);
|
||||||
|
|
||||||
private bool IsPlacementActive(LiveEntityRecord record) =>
|
private bool IsPlacementActive(LiveEntityRecord record) =>
|
||||||
_activePlacementGenerationByGuid.TryGetValue(
|
TryGetProjectionKey(record, out RuntimeEntityKey key)
|
||||||
record.ServerGuid,
|
&& _activePlacementOwners.Contains(key);
|
||||||
out ushort generation)
|
|
||||||
&& generation == record.Generation;
|
private bool TryGetCurrentProjectionKey(
|
||||||
|
uint serverGuid,
|
||||||
|
out RuntimeEntityKey key)
|
||||||
|
{
|
||||||
|
if (_liveEntities.TryGetRecord(
|
||||||
|
serverGuid,
|
||||||
|
out LiveEntityRecord record))
|
||||||
|
{
|
||||||
|
return TryGetProjectionKey(record, out key);
|
||||||
|
}
|
||||||
|
|
||||||
|
key = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetProjectionKey(
|
||||||
|
LiveEntityRecord record,
|
||||||
|
out RuntimeEntityKey key)
|
||||||
|
{
|
||||||
|
if (record.ProjectionKey is { } projectionKey)
|
||||||
|
{
|
||||||
|
key = projectionKey;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeEntityKey RequireProjectionKey(
|
||||||
|
LiveEntityRecord record) =>
|
||||||
|
record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Live entity 0x{record.ServerGuid:X8}/{record.Generation} " +
|
||||||
|
"has no exact projection key.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
313
src/AcDream.App/World/LiveEntityProjectionStore.cs
Normal file
313
src/AcDream.App/World/LiveEntityProjectionStore.cs
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// App-only projection storage. RuntimeEntityDirectory remains the sole
|
||||||
|
/// server-GUID/incarnation/local-ID authority; every App sidecar exists only
|
||||||
|
/// after Runtime has issued its exact local-ID plus INSTANCE_TS key.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveEntityProjectionStore
|
||||||
|
{
|
||||||
|
private readonly RuntimeEntityDirectory _directory;
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _materialized = new();
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _visible = new();
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _teardown = new();
|
||||||
|
private readonly Dictionary<RuntimeEntityRecord, LiveEntityRecord>
|
||||||
|
_unmaterializedTeardown = new(ReferenceEqualityComparer.Instance);
|
||||||
|
private readonly List<LiveEntityRecord> _materializedInRegistrationOrder = [];
|
||||||
|
private readonly TeardownRecordCollection _teardownRecords;
|
||||||
|
|
||||||
|
public LiveEntityProjectionStore(RuntimeEntityDirectory directory)
|
||||||
|
{
|
||||||
|
_directory = directory ?? throw new ArgumentNullException(nameof(directory));
|
||||||
|
_teardownRecords = new TeardownRecordCollection(
|
||||||
|
_teardown,
|
||||||
|
_unmaterializedTeardown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ActiveCount => _materializedInRegistrationOrder.Count;
|
||||||
|
public int MaterializedCount => _materialized.Count + _teardown.Count;
|
||||||
|
public int TeardownCount => _teardown.Count + _unmaterializedTeardown.Count;
|
||||||
|
public IReadOnlyList<LiveEntityRecord> ActiveRecords =>
|
||||||
|
_materializedInRegistrationOrder;
|
||||||
|
public IReadOnlyList<LiveEntityRecord> Values =>
|
||||||
|
_materializedInRegistrationOrder;
|
||||||
|
public IReadOnlyCollection<LiveEntityRecord> MaterializedRecords =>
|
||||||
|
_materialized.Values;
|
||||||
|
public IReadOnlyCollection<LiveEntityRecord> VisibleRecords => _visible.Values;
|
||||||
|
public IReadOnlyCollection<LiveEntityRecord> TeardownRecords =>
|
||||||
|
_teardownRecords;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the exact App sidecar after Runtime has claimed a local ID.
|
||||||
|
/// Publication is synchronous so no input or presentation callback moves
|
||||||
|
/// to a later frame.
|
||||||
|
/// </summary>
|
||||||
|
public LiveEntityRecord AddMaterializing(RuntimeEntityRecord canonical)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(canonical);
|
||||||
|
if (!_directory.IsCurrent(canonical))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"An App projection sidecar can only be added for the current Runtime incarnation.");
|
||||||
|
}
|
||||||
|
RuntimeEntityKey key = canonical.Key
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Runtime must claim the local projection ID before App creates a sidecar.");
|
||||||
|
if (_materialized.ContainsKey(key))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Runtime entity 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} already has an App sidecar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = new LiveEntityRecord(_directory, canonical)
|
||||||
|
{
|
||||||
|
ProjectionKey = key,
|
||||||
|
};
|
||||||
|
_materialized.Add(key, record);
|
||||||
|
_materializedInRegistrationOrder.Add(record);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetCurrent(uint serverGuid, out LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (_directory.TryGetActive(serverGuid, out RuntimeEntityRecord canonical))
|
||||||
|
return TryGet(canonical, out record);
|
||||||
|
|
||||||
|
record = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsCurrent(uint serverGuid) =>
|
||||||
|
TryGetCurrent(serverGuid, out _);
|
||||||
|
|
||||||
|
public LiveEntityRecord? GetCurrentOrDefault(uint serverGuid) =>
|
||||||
|
TryGetCurrent(serverGuid, out LiveEntityRecord record) ? record : null;
|
||||||
|
|
||||||
|
public bool TryGet(RuntimeEntityRecord canonical, out LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (canonical.Key is { } key
|
||||||
|
&& _materialized.TryGetValue(key, out LiveEntityRecord? candidate)
|
||||||
|
&& ReferenceEquals(candidate.Canonical, canonical))
|
||||||
|
{
|
||||||
|
record = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
record = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(RuntimeEntityKey key, out LiveEntityRecord record) =>
|
||||||
|
_materialized.TryGetValue(key, out record!);
|
||||||
|
|
||||||
|
public bool TryGetByLocalId(uint localEntityId, out LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (_directory.TryGetByLocalId(
|
||||||
|
localEntityId,
|
||||||
|
out RuntimeEntityRecord canonical))
|
||||||
|
{
|
||||||
|
return TryGet(canonical, out record);
|
||||||
|
}
|
||||||
|
|
||||||
|
record = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCurrent(LiveEntityRecord record) =>
|
||||||
|
_directory.IsCurrent(record.Canonical)
|
||||||
|
&& TryGet(record.Canonical, out LiveEntityRecord current)
|
||||||
|
&& ReferenceEquals(current, record);
|
||||||
|
|
||||||
|
public void SetVisible(LiveEntityRecord record, bool visible)
|
||||||
|
{
|
||||||
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
|
if (!_materialized.TryGetValue(key, out LiveEntityRecord? materialized)
|
||||||
|
|| !ReferenceEquals(materialized, record))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Only a current exact App sidecar can change visibility.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible)
|
||||||
|
_visible[key] = record;
|
||||||
|
else
|
||||||
|
_visible.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetVisibleCurrent(uint serverGuid, out LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (_directory.TryGetActive(serverGuid, out RuntimeEntityRecord canonical)
|
||||||
|
&& canonical.Key is { } key
|
||||||
|
&& _visible.TryGetValue(key, out LiveEntityRecord? visible)
|
||||||
|
&& ReferenceEquals(visible.Canonical, canonical))
|
||||||
|
{
|
||||||
|
record = visible;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
record = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveCurrent(
|
||||||
|
uint serverGuid,
|
||||||
|
out LiveEntityRecord? record)
|
||||||
|
{
|
||||||
|
if (!_directory.TryGetActive(
|
||||||
|
serverGuid,
|
||||||
|
out RuntimeEntityRecord canonical)
|
||||||
|
|| !TryGet(canonical, out LiveEntityRecord sidecar)
|
||||||
|
|| !RemoveActive(sidecar))
|
||||||
|
{
|
||||||
|
record = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
record = sidecar;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveCurrent(uint serverGuid) =>
|
||||||
|
RemoveCurrent(serverGuid, out _);
|
||||||
|
|
||||||
|
public bool RemoveActive(LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (record.ProjectionKey is not { } key)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_visible.Remove(key);
|
||||||
|
bool removed = _materialized.Remove(
|
||||||
|
key,
|
||||||
|
out LiveEntityRecord? materialized)
|
||||||
|
&& ReferenceEquals(materialized, record);
|
||||||
|
if (!removed)
|
||||||
|
return false;
|
||||||
|
if (!_materializedInRegistrationOrder.Remove(record))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The exact App sidecar was absent from materialization order.");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RetainTeardown(LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (record.ProjectionKey is { } key)
|
||||||
|
AddExact(_teardown, key, record);
|
||||||
|
else
|
||||||
|
AddExact(_unmaterializedTeardown, record.Canonical, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetTeardown(
|
||||||
|
RuntimeEntityRecord canonical,
|
||||||
|
out LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (canonical.Key is { } key
|
||||||
|
&& _teardown.TryGetValue(key, out record!))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
foreach (LiveEntityRecord retained in _teardown.Values)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(retained.Canonical, canonical))
|
||||||
|
{
|
||||||
|
record = retained;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _unmaterializedTeardown.TryGetValue(canonical, out record!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseTeardown(LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
if (record.ProjectionKey is { } key)
|
||||||
|
{
|
||||||
|
RemoveExact(_teardown, key, record);
|
||||||
|
record.ProjectionKey = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveExact(_unmaterializedTeardown, record.Canonical, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRetainedTeardown(LiveEntityRecord record) =>
|
||||||
|
TryGetTeardown(record.Canonical, out LiveEntityRecord retained)
|
||||||
|
&& ReferenceEquals(retained, record);
|
||||||
|
|
||||||
|
public void ClearConverged()
|
||||||
|
{
|
||||||
|
if (_materializedInRegistrationOrder.Count != 0 || TeardownCount != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Projection storage cannot clear before active and teardown ownership converges.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_materialized.Clear();
|
||||||
|
_visible.Clear();
|
||||||
|
_teardown.Clear();
|
||||||
|
_unmaterializedTeardown.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeEntityKey RequireProjectionKey(LiveEntityRecord record) =>
|
||||||
|
record.ProjectionKey
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Generation} has no App projection key.");
|
||||||
|
|
||||||
|
private static void AddExact<TKey>(
|
||||||
|
Dictionary<TKey, LiveEntityRecord> destination,
|
||||||
|
TKey key,
|
||||||
|
LiveEntityRecord record)
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
if (destination.TryGetValue(key, out LiveEntityRecord? retained)
|
||||||
|
&& !ReferenceEquals(retained, record))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"App teardown sidecar collision for 0x{record.ServerGuid:X8}/{record.Generation}.");
|
||||||
|
}
|
||||||
|
destination[key] = record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveExact<TKey>(
|
||||||
|
Dictionary<TKey, LiveEntityRecord> source,
|
||||||
|
TKey key,
|
||||||
|
LiveEntityRecord record)
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
if (source.TryGetValue(key, out LiveEntityRecord? retained)
|
||||||
|
&& ReferenceEquals(retained, record))
|
||||||
|
{
|
||||||
|
source.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TeardownRecordCollection :
|
||||||
|
IReadOnlyCollection<LiveEntityRecord>
|
||||||
|
{
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _keyed;
|
||||||
|
private readonly Dictionary<RuntimeEntityRecord, LiveEntityRecord> _unmaterialized;
|
||||||
|
|
||||||
|
public TeardownRecordCollection(
|
||||||
|
Dictionary<RuntimeEntityKey, LiveEntityRecord> keyed,
|
||||||
|
Dictionary<RuntimeEntityRecord, LiveEntityRecord> unmaterialized)
|
||||||
|
{
|
||||||
|
_keyed = keyed;
|
||||||
|
_unmaterialized = unmaterialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _keyed.Count + _unmaterialized.Count;
|
||||||
|
|
||||||
|
public IEnumerator<LiveEntityRecord> GetEnumerator()
|
||||||
|
{
|
||||||
|
foreach (LiveEntityRecord record in _keyed.Values)
|
||||||
|
yield return record;
|
||||||
|
foreach (LiveEntityRecord record in _unmaterialized.Values)
|
||||||
|
yield return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
|
||||||
|
GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -106,11 +106,6 @@ internal sealed class LiveEntityRuntimeTeardownController
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = _runtime!;
|
LiveEntityRuntime runtime = _runtime!;
|
||||||
uint serverGuid = record.ServerGuid;
|
uint serverGuid = record.ServerGuid;
|
||||||
var incarnation = new LiveEntityIncarnationCleanup(
|
|
||||||
record,
|
|
||||||
guid => runtime.TryGetRecord(guid, out LiveEntityRecord current)
|
|
||||||
? current
|
|
||||||
: null);
|
|
||||||
var cleanups = new List<Action>
|
var cleanups = new List<Action>
|
||||||
{
|
{
|
||||||
() => _presentation!.Forget(record),
|
() => _presentation!.Forget(record),
|
||||||
|
|
@ -152,17 +147,17 @@ internal sealed class LiveEntityRuntimeTeardownController
|
||||||
cleanups.Add(physicsHost.NotifyExitWorld);
|
cleanups.Add(physicsHost.NotifyExitWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanups.Add(() => _animations.Remove(existingEntity.Id));
|
cleanups.Add(() => _animations.Remove(record));
|
||||||
cleanups.Add(() => _classification!.InvalidateEntity(existingEntity.Id));
|
cleanups.Add(() => _classification!.InvalidateEntity(existingEntity.Id));
|
||||||
cleanups.Add(() => incarnation.RunIfNoReplacement(
|
if (record.ProjectionKey is { } projectionKey)
|
||||||
() => _remoteMovementObservations!.Remove(serverGuid)));
|
cleanups.Add(() => _remoteMovementObservations!.Remove(projectionKey));
|
||||||
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
|
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
|
||||||
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
|
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
|
||||||
record,
|
record,
|
||||||
_identity!.ServerGuid));
|
_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(record));
|
||||||
return new LiveEntityTeardownPlan(cleanups);
|
return new LiveEntityTeardownPlan(cleanups);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
internal LiveEntityAnimationRuntimeView(ILiveEntityRuntimeSource runtime) =>
|
internal LiveEntityAnimationRuntimeView(ILiveEntityRuntimeSource runtime) =>
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
|
||||||
public int Count => _runtime.Current?.SpatialAnimationRuntimes.Count ?? 0;
|
public int Count => _runtime.Current?.SpatialAnimationRuntimeCount ?? 0;
|
||||||
public IEnumerable<uint> Keys =>
|
|
||||||
_runtime.Current?.SpatialAnimationRuntimes.Keys ?? Array.Empty<uint>();
|
|
||||||
|
|
||||||
public TAnimation this[uint localEntityId]
|
public TAnimation this[uint localEntityId]
|
||||||
{
|
{
|
||||||
|
|
@ -52,6 +50,12 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
&& runtime.ClearAnimationRuntime(guid);
|
&& runtime.ClearAnimationRuntime(guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool Remove(LiveEntityRecord record)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
return _runtime.Current?.ClearAnimationRuntime(record) == true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Copies only currently resident local IDs without boxing a
|
/// <summary>Copies only currently resident local IDs without boxing a
|
||||||
/// dictionary-key enumerator.</summary>
|
/// dictionary-key enumerator.</summary>
|
||||||
public void CopySpatialIdsTo(HashSet<uint> destination)
|
public void CopySpatialIdsTo(HashSet<uint> destination)
|
||||||
|
|
@ -110,10 +114,9 @@ public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||||
while (++_index < _snapshot.Count)
|
while (++_index < _snapshot.Count)
|
||||||
{
|
{
|
||||||
KeyValuePair<uint, TAnimation> pair = _snapshot[_index];
|
KeyValuePair<uint, TAnimation> pair = _snapshot[_index];
|
||||||
if (_runtime?.SpatialAnimationRuntimes.TryGetValue(
|
if (_runtime?.IsCurrentSpatialAnimation(
|
||||||
pair.Key,
|
pair.Key,
|
||||||
out ILiveEntityAnimationRuntime? indexed) == true
|
pair.Value) == true)
|
||||||
&& ReferenceEquals(indexed, pair.Value))
|
|
||||||
{
|
{
|
||||||
Current = pair;
|
Current = pair;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using AcDream.App.Net;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
@ -91,7 +92,7 @@ public sealed class InteractionUiRuntimeSourcesTests
|
||||||
|
|
||||||
var provider = new RadarSnapshotProvider(
|
var provider = new RadarSnapshotProvider(
|
||||||
new AcDream.Core.Items.ClientObjectTable(),
|
new AcDream.Core.Items.ClientObjectTable(),
|
||||||
static () => new Dictionary<uint, WorldEntity>(),
|
EmptyRadarSource.Instance,
|
||||||
static () => new Dictionary<uint, WorldSession.EntitySpawn>(),
|
static () => new Dictionary<uint, WorldSession.EntitySpawn>(),
|
||||||
static () => 0u,
|
static () => 0u,
|
||||||
static () => 0f,
|
static () => 0f,
|
||||||
|
|
@ -253,6 +254,29 @@ public sealed class InteractionUiRuntimeSourcesTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class EmptyRadarSource : ILiveEntityRadarSource
|
||||||
|
{
|
||||||
|
public static EmptyRadarSource Instance { get; } = new();
|
||||||
|
|
||||||
|
public bool TryGetMaterialized(
|
||||||
|
uint serverGuid,
|
||||||
|
out WorldEntity entity)
|
||||||
|
{
|
||||||
|
entity = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetVisible(uint serverGuid, out WorldEntity entity)
|
||||||
|
{
|
||||||
|
entity = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyVisibleTo(
|
||||||
|
List<KeyValuePair<uint, WorldEntity>> destination) =>
|
||||||
|
destination.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
private static T Stub<T>() where T : class =>
|
private static T Stub<T>() where T : class =>
|
||||||
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
|
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -501,13 +501,15 @@ public sealed class SelectionInteractionControllerTests
|
||||||
public void OldRemovalClearsCapturedPickupButDoesNotClearReplacementSelection()
|
public void OldRemovalClearsCapturedPickupButDoesNotClearReplacementSelection()
|
||||||
{
|
{
|
||||||
var h = new Harness();
|
var h = new Harness();
|
||||||
h.SetApproach(closeRange: true, localEntityId: 101u);
|
var oldRecord = LiveEntityTestFixture.CreateExactProjectionRecord(
|
||||||
|
Spawn(Target, instance: 1));
|
||||||
|
uint localEntityId = oldRecord.LocalEntityId!.Value;
|
||||||
|
h.SetApproach(closeRange: true, localEntityId: localEntityId);
|
||||||
h.Selection.Select(Target, SelectionChangeSource.World);
|
h.Selection.Select(Target, SelectionChangeSource.World);
|
||||||
Assert.True(h.Items.PlaceWorldItemInBackpack(Target));
|
Assert.True(h.Items.PlaceWorldItemInBackpack(Target));
|
||||||
var oldRecord = new LiveEntityRecord(Spawn(Target, instance: 1));
|
|
||||||
oldRecord.WorldEntity = new WorldEntity
|
oldRecord.WorldEntity = new WorldEntity
|
||||||
{
|
{
|
||||||
Id = 101u,
|
Id = localEntityId,
|
||||||
ServerGuid = Target,
|
ServerGuid = Target,
|
||||||
SourceGfxObjOrSetupId = 0x0200_0001u,
|
SourceGfxObjOrSetupId = 0x0200_0001u,
|
||||||
Position = Vector3.Zero,
|
Position = Vector3.Zero,
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,9 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
scale: 2f,
|
scale: 2f,
|
||||||
itemType: (uint)ItemType.Creature,
|
itemType: (uint)ItemType.Creature,
|
||||||
descriptionFlags: 0x28u);
|
descriptionFlags: 0x28u);
|
||||||
var record = new LiveEntityRecord(spawn)
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
{
|
record.FinalPhysicsState =
|
||||||
FinalPhysicsState = PhysicsStateFlags.Hidden | PhysicsStateFlags.Static,
|
PhysicsStateFlags.Hidden | PhysicsStateFlags.Static;
|
||||||
};
|
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
var builder = Builder();
|
var builder = Builder();
|
||||||
|
|
@ -65,7 +64,7 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
var setup = new Setup();
|
var setup = new Setup();
|
||||||
setup.Parts.Add(part);
|
setup.Parts.Add(part);
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1.5f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1.5f);
|
||||||
var record = new LiveEntityRecord(spawn);
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
var builder = new LiveEntityCollisionBuilder(
|
var builder = new LiveEntityCollisionBuilder(
|
||||||
|
|
@ -90,7 +89,7 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
var setup = new Setup();
|
var setup = new Setup();
|
||||||
setup.Parts.Add(basePart);
|
setup.Parts.Add(basePart);
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
||||||
var record = new LiveEntityRecord(spawn);
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
var builder = new LiveEntityCollisionBuilder(
|
var builder = new LiveEntityCollisionBuilder(
|
||||||
|
|
@ -115,7 +114,7 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
var setup = new Setup();
|
var setup = new Setup();
|
||||||
setup.Parts.Add(basePart);
|
setup.Parts.Add(basePart);
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
||||||
var record = new LiveEntityRecord(spawn);
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
var builder = new LiveEntityCollisionBuilder(
|
var builder = new LiveEntityCollisionBuilder(
|
||||||
|
|
@ -145,7 +144,7 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
var setup = new Setup();
|
var setup = new Setup();
|
||||||
setup.Parts.Add(basePart);
|
setup.Parts.Add(basePart);
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
||||||
var record = new LiveEntityRecord(spawn);
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
var builder = new LiveEntityCollisionBuilder(
|
var builder = new LiveEntityCollisionBuilder(
|
||||||
|
|
@ -178,7 +177,7 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
var setup = new Setup();
|
var setup = new Setup();
|
||||||
setup.Parts.Add(basePart);
|
setup.Parts.Add(basePart);
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
||||||
var record = new LiveEntityRecord(spawn);
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
var builder = new LiveEntityCollisionBuilder(
|
var builder = new LiveEntityCollisionBuilder(
|
||||||
|
|
@ -243,7 +242,7 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
var setup = new Setup();
|
var setup = new Setup();
|
||||||
setup.Parts.Add(basePart);
|
setup.Parts.Add(basePart);
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
||||||
var record = new LiveEntityRecord(spawn);
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
var builder = new LiveEntityCollisionBuilder(
|
var builder = new LiveEntityCollisionBuilder(
|
||||||
|
|
@ -277,7 +276,7 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
public void ShapelessSetup_ProducesNoRegistration()
|
public void ShapelessSetup_ProducesNoRegistration()
|
||||||
{
|
{
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
||||||
var record = new LiveEntityRecord(spawn);
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
record.WorldEntity = entity;
|
record.WorldEntity = entity;
|
||||||
|
|
||||||
|
|
@ -294,8 +293,9 @@ public sealed class LiveEntityCollisionBuilderTests
|
||||||
public void Build_RejectsDifferentRecordIncarnation()
|
public void Build_RejectsDifferentRecordIncarnation()
|
||||||
{
|
{
|
||||||
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
WorldSession.EntitySpawn spawn = Spawn(scale: 1f);
|
||||||
var expected = new LiveEntityRecord(spawn);
|
var expected = LiveEntityTestFixture.CreateExactProjectionRecord(spawn);
|
||||||
var other = new LiveEntityRecord(spawn with { InstanceSequence = 2 });
|
var other = LiveEntityTestFixture.CreateExactProjectionRecord(
|
||||||
|
spawn with { InstanceSequence = 2 });
|
||||||
WorldEntity entity = Entity();
|
WorldEntity entity = Entity();
|
||||||
expected.WorldEntity = entity;
|
expected.WorldEntity = entity;
|
||||||
other.WorldEntity = entity;
|
other.WorldEntity = entity;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Motion_StrictFreshnessAndWrapAreOwnedBeforePresentation()
|
public void Motion_StrictFreshnessAndWrapAreOwnedBeforePresentation()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 7, movement: 0xFFFE));
|
Register(runtime, Spawn(Guid, instance: 7, movement: 0xFFFE));
|
||||||
var published = new List<AcceptedPhysicsTimestamps>();
|
var published = new List<AcceptedPhysicsTimestamps>();
|
||||||
var gate = new LiveEntityInboundAuthorityGate(
|
var gate = new LiveEntityInboundAuthorityGate(
|
||||||
runtime,
|
runtime,
|
||||||
|
|
@ -52,7 +52,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void AutonomousLocalMotion_ConsumesTimestampButDoesNotPublishPayload()
|
public void AutonomousLocalMotion_ConsumesTimestampButDoesNotPublishPayload()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 1));
|
Register(runtime, Spawn(Guid, instance: 1));
|
||||||
int publishCount = 0;
|
int publishCount = 0;
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => publishCount++);
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => publishCount++);
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Vector_InvalidPayloadDoesNotConsumeSequenceAndDuplicateIsRejected()
|
public void Vector_InvalidPayloadDoesNotConsumeSequenceAndDuplicateIsRejected()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 2));
|
Register(runtime, Spawn(Guid, instance: 2));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
||||||
var update = new VectorUpdate.Parsed(
|
var update = new VectorUpdate.Parsed(
|
||||||
Guid,
|
Guid,
|
||||||
|
|
@ -94,7 +94,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Vector_WrappedSequenceIsAccepted()
|
public void Vector_WrappedSequenceIsAccepted()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 2, vector: 0xFFFE));
|
Register(runtime, Spawn(Guid, instance: 2, vector: 0xFFFE));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
||||||
|
|
||||||
Assert.True(gate.TryAcceptVector(
|
Assert.True(gate.TryAcceptVector(
|
||||||
|
|
@ -112,7 +112,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void State_EqualIsRejectedAndWrappedSequenceIsAccepted()
|
public void State_EqualIsRejectedAndWrappedSequenceIsAccepted()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 3, state: 0xFFFE));
|
Register(runtime, Spawn(Guid, instance: 3, state: 0xFFFE));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
||||||
|
|
||||||
Assert.False(gate.TryAcceptState(
|
Assert.False(gate.TryAcceptState(
|
||||||
|
|
@ -149,7 +149,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
Assert.Equal(0xAABB0001u, gate.LastLivePlayerLandblockId);
|
Assert.Equal(0xAABB0001u, gate.LastLivePlayerLandblockId);
|
||||||
Assert.Equal(0, publishCount);
|
Assert.Equal(0, publishCount);
|
||||||
|
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 4, position: 1));
|
Register(runtime, Spawn(Guid, instance: 4, position: 1));
|
||||||
Assert.True(gate.TryAcceptPosition(
|
Assert.True(gate.TryAcceptPosition(
|
||||||
update,
|
update,
|
||||||
Guid,
|
Guid,
|
||||||
|
|
@ -169,7 +169,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Position_InvalidPayloadDoesNotConsumeSequence()
|
public void Position_InvalidPayloadDoesNotConsumeSequence()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 5, position: 1));
|
Register(runtime, Spawn(Guid, instance: 5, position: 1));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
||||||
WorldSession.EntityPositionUpdate update = Position(Guid, 5, 2, 0x01010001u);
|
WorldSession.EntityPositionUpdate update = Position(Guid, 5, 2, 0x01010001u);
|
||||||
|
|
||||||
|
|
@ -185,7 +185,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Position_WrappedSequenceIsAccepted()
|
public void Position_WrappedSequenceIsAccepted()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 5, position: 0xFFFE));
|
Register(runtime, Spawn(Guid, instance: 5, position: 0xFFFE));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
||||||
|
|
||||||
Assert.True(gate.TryAcceptPosition(
|
Assert.True(gate.TryAcceptPosition(
|
||||||
|
|
@ -201,7 +201,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Vector_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
|
public void Vector_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 5, vector: 1));
|
Register(runtime, Spawn(Guid, instance: 5, vector: 1));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
||||||
Assert.True(gate.TryAcceptVector(
|
Assert.True(gate.TryAcceptVector(
|
||||||
new VectorUpdate.Parsed(Guid, Vector3.One, Vector3.Zero, 5, 2),
|
new VectorUpdate.Parsed(Guid, Vector3.One, Vector3.Zero, 5, 2),
|
||||||
|
|
@ -220,7 +220,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void State_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
|
public void State_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 5, state: 1));
|
Register(runtime, Spawn(Guid, instance: 5, state: 1));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
var gate = new LiveEntityInboundAuthorityGate(runtime, (_, _) => { });
|
||||||
Assert.True(gate.TryAcceptState(
|
Assert.True(gate.TryAcceptState(
|
||||||
new SetState.Parsed(Guid, (uint)PhysicsStateFlags.Hidden, 5, 2),
|
new SetState.Parsed(Guid, (uint)PhysicsStateFlags.Hidden, 5, 2),
|
||||||
|
|
@ -238,7 +238,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Motion_PublisherReplacementInvalidatesCapturedIncarnation()
|
public void Motion_PublisherReplacementInvalidatesCapturedIncarnation()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 6));
|
Register(runtime, Spawn(Guid, instance: 6));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(
|
var gate = new LiveEntityInboundAuthorityGate(
|
||||||
runtime,
|
runtime,
|
||||||
(_, _) => runtime.RegisterLiveEntity(Spawn(Guid, instance: 7)));
|
(_, _) => runtime.RegisterLiveEntity(Spawn(Guid, instance: 7)));
|
||||||
|
|
@ -249,15 +249,17 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
out _,
|
out _,
|
||||||
out bool timestampAccepted));
|
out bool timestampAccepted));
|
||||||
Assert.True(timestampAccepted);
|
Assert.True(timestampAccepted);
|
||||||
Assert.True(runtime.TryGetRecord(Guid, out LiveEntityRecord replacement));
|
Assert.True(runtime.TryGetCanonical(Guid, out RuntimeEntityRecord replacement));
|
||||||
Assert.Equal((ushort)7, replacement.Generation);
|
Assert.Equal((ushort)7, replacement.Incarnation);
|
||||||
|
Assert.Null(replacement.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(Guid, out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Position_PublisherNewerChannelInvalidatesCapturedAuthority()
|
public void Position_PublisherNewerChannelInvalidatesCapturedAuthority()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 8, position: 1));
|
Register(runtime, Spawn(Guid, instance: 8, position: 1));
|
||||||
var nested = Position(Guid, 8, 3, 0x01010002u);
|
var nested = Position(Guid, 8, 3, 0x01010002u);
|
||||||
LiveEntityInboundAuthorityGate? gate = null;
|
LiveEntityInboundAuthorityGate? gate = null;
|
||||||
gate = new LiveEntityInboundAuthorityGate(
|
gate = new LiveEntityInboundAuthorityGate(
|
||||||
|
|
@ -286,7 +288,7 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
public void Position_PublisherNewerVectorPreservesIndependentPositionAuthority()
|
public void Position_PublisherNewerVectorPreservesIndependentPositionAuthority()
|
||||||
{
|
{
|
||||||
LiveEntityRuntime runtime = Runtime();
|
LiveEntityRuntime runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(Guid, instance: 9, position: 1, vector: 1));
|
Register(runtime, Spawn(Guid, instance: 9, position: 1, vector: 1));
|
||||||
var gate = new LiveEntityInboundAuthorityGate(
|
var gate = new LiveEntityInboundAuthorityGate(
|
||||||
runtime,
|
runtime,
|
||||||
(_, _) => runtime.TryApplyVector(
|
(_, _) => runtime.TryApplyVector(
|
||||||
|
|
@ -317,6 +319,11 @@ public sealed class LiveEntityInboundAuthorityGateTests
|
||||||
new GpuWorldState(),
|
new GpuWorldState(),
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
|
|
||||||
|
private static LiveEntityRecord Register(
|
||||||
|
LiveEntityRuntime runtime,
|
||||||
|
WorldSession.EntitySpawn spawn) =>
|
||||||
|
runtime.RegisterAndMaterializeProjection(spawn);
|
||||||
|
|
||||||
private static WorldSession.EntityMotionUpdate Motion(
|
private static WorldSession.EntityMotionUpdate Motion(
|
||||||
ushort instance,
|
ushort instance,
|
||||||
ushort movement,
|
ushort movement,
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,8 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
||||||
var live = new LiveEntityRuntime(
|
var live = new LiveEntityRuntime(
|
||||||
spatial,
|
spatial,
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn()).Record!;
|
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
|
||||||
WorldEntity entity = live.MaterializeLiveEntity(
|
Spawn(),
|
||||||
Guid,
|
|
||||||
SourceCell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -39,7 +37,8 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = SourceCell,
|
ParentCellId = SourceCell,
|
||||||
})!;
|
});
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
|
|
||||||
var remote = new AcDream.App.Physics.RemoteMotion
|
var remote = new AcDream.App.Physics.RemoteMotion
|
||||||
{
|
{
|
||||||
|
|
@ -94,10 +93,8 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
||||||
var live = new LiveEntityRuntime(
|
var live = new LiveEntityRuntime(
|
||||||
spatial,
|
spatial,
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn()).Record!;
|
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
|
||||||
WorldEntity entity = live.MaterializeLiveEntity(
|
Spawn(),
|
||||||
Guid,
|
|
||||||
SourceCell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -107,7 +104,8 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = SourceCell,
|
ParentCellId = SourceCell,
|
||||||
})!;
|
});
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
var remote = new AcDream.App.Physics.RemoteMotion
|
var remote = new AcDream.App.Physics.RemoteMotion
|
||||||
{
|
{
|
||||||
CellId = SourceCell,
|
CellId = SourceCell,
|
||||||
|
|
|
||||||
|
|
@ -243,18 +243,18 @@ public sealed class ProjectileControllerTests
|
||||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
|
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
|
||||||
Assert.True(record.PhysicsBody!.InWorld);
|
Assert.True(record.PhysicsBody!.InWorld);
|
||||||
Assert.True(record.PhysicsBody.IsActive);
|
Assert.True(record.PhysicsBody.IsActive);
|
||||||
Assert.Single(fixture.Live.SpatialProjectileRuntimes);
|
Assert.Equal(1, fixture.Live.SpatialProjectileRuntimeCount);
|
||||||
|
|
||||||
fixture.Spatial.RemoveLandblock(0x0101FFFFu);
|
fixture.Spatial.RemoveLandblock(0x0101FFFFu);
|
||||||
|
|
||||||
Assert.False(record.IsSpatiallyVisible);
|
Assert.False(record.IsSpatiallyVisible);
|
||||||
Assert.Empty(fixture.Live.SpatialProjectileRuntimes);
|
Assert.Equal(0, fixture.Live.SpatialProjectileRuntimeCount);
|
||||||
Assert.False(record.PhysicsBody.InWorld);
|
Assert.False(record.PhysicsBody.InWorld);
|
||||||
Assert.False(record.PhysicsBody.IsActive);
|
Assert.False(record.PhysicsBody.IsActive);
|
||||||
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
|
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
|
||||||
|
|
||||||
fixture.Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
fixture.Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
Assert.Single(fixture.Live.SpatialProjectileRuntimes);
|
Assert.Equal(1, fixture.Live.SpatialProjectileRuntimeCount);
|
||||||
Assert.True(record.PhysicsBody.InWorld);
|
Assert.True(record.PhysicsBody.InWorld);
|
||||||
Assert.True(record.PhysicsBody.IsActive);
|
Assert.True(record.PhysicsBody.IsActive);
|
||||||
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
|
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
|
||||||
|
|
@ -545,7 +545,8 @@ public sealed class ProjectileControllerTests
|
||||||
Assert.Same(runtime, record.ProjectileRuntime);
|
Assert.Same(runtime, record.ProjectileRuntime);
|
||||||
Assert.Same(body, record.PhysicsBody);
|
Assert.Same(body, record.PhysicsBody);
|
||||||
Assert.True(body.IsActive); // State preserves an already-active body.
|
Assert.True(body.IsActive); // State preserves an already-active body.
|
||||||
Assert.Same(record.WorldEntity, fixture.Live.MaterializedWorldEntities[Guid]);
|
Assert.True(fixture.Live.TryGetWorldEntity(Guid, out WorldEntity entity));
|
||||||
|
Assert.Same(record.WorldEntity, entity);
|
||||||
Assert.Equal(1, fixture.Resources.Registers);
|
Assert.Equal(1, fixture.Resources.Registers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1501,12 +1502,8 @@ public sealed class ProjectileControllerTests
|
||||||
angularVelocity ?? Vector3.Zero,
|
angularVelocity ?? Vector3.Zero,
|
||||||
friction,
|
friction,
|
||||||
elasticity);
|
elasticity);
|
||||||
LiveEntityRegistrationResult registration = Live.RegisterLiveEntity(spawn);
|
LiveEntityRecord record = Live.RegisterAndMaterializeProjection(
|
||||||
Assert.NotNull(registration.Record);
|
spawn,
|
||||||
registration.Record!.HasPartArray = true;
|
|
||||||
WorldEntity? entity = Live.MaterializeLiveEntity(
|
|
||||||
Guid,
|
|
||||||
cellId,
|
|
||||||
localId => new WorldEntity
|
localId => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = localId,
|
Id = localId,
|
||||||
|
|
@ -1518,10 +1515,11 @@ public sealed class ProjectileControllerTests
|
||||||
? new[] { new MeshRef(0x01000001u, Matrix4x4.Identity) }
|
? new[] { new MeshRef(0x01000001u, Matrix4x4.Identity) }
|
||||||
: Array.Empty<MeshRef>(),
|
: Array.Empty<MeshRef>(),
|
||||||
ParentCellId = cellId,
|
ParentCellId = cellId,
|
||||||
});
|
},
|
||||||
Assert.NotNull(entity);
|
initializeProjection: exact => exact.HasPartArray = true);
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
Poses.PublishMeshRefs(entity);
|
Poses.PublishMeshRefs(entity);
|
||||||
return registration.Record!;
|
return record;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -697,7 +697,7 @@ public sealed class RemotePhysicsUpdaterTests
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
BindHiddenRemote(live, firstGuid);
|
BindHiddenRemote(live, firstGuid);
|
||||||
BindHiddenRemote(live, secondGuid);
|
BindHiddenRemote(live, secondGuid);
|
||||||
Assert.Equal(2, live.SpatialRemoteMotionRuntimes.Count);
|
Assert.Equal(2, live.SpatialRemoteMotionRuntimeCount);
|
||||||
|
|
||||||
var updater = new RemotePhysicsUpdater(
|
var updater = new RemotePhysicsUpdater(
|
||||||
new PhysicsEngine(),
|
new PhysicsEngine(),
|
||||||
|
|
@ -721,7 +721,7 @@ public sealed class RemotePhysicsUpdaterTests
|
||||||
|
|
||||||
Assert.Single(published);
|
Assert.Single(published);
|
||||||
Assert.Equal(published, partPoseDirty);
|
Assert.Equal(published, partPoseDirty);
|
||||||
Assert.Single(live.SpatialRemoteMotionRuntimes);
|
Assert.Equal(1, live.SpatialRemoteMotionRuntimeCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PhysicsEngine BuildBoundaryEngine()
|
private static PhysicsEngine BuildBoundaryEngine()
|
||||||
|
|
@ -908,14 +908,8 @@ public sealed class RemotePhysicsUpdaterTests
|
||||||
Vector3 position,
|
Vector3 position,
|
||||||
bool enqueueDestination)
|
bool enqueueDestination)
|
||||||
{
|
{
|
||||||
LiveEntityRegistrationResult registration = Live.RegisterLiveEntity(
|
LiveEntityRecord record = Live.RegisterAndMaterializeProjection(
|
||||||
Spawn(instanceSequence, position));
|
Spawn(instanceSequence, position),
|
||||||
LiveEntityRecord record = Assert.IsType<LiveEntityRecord>(
|
|
||||||
registration.Record);
|
|
||||||
WorldEntity entity = Assert.IsType<WorldEntity>(
|
|
||||||
Live.MaterializeLiveEntity(
|
|
||||||
Guid,
|
|
||||||
SourceCell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -925,7 +919,8 @@ public sealed class RemotePhysicsUpdaterTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = SourceCell,
|
ParentCellId = SourceCell,
|
||||||
}));
|
});
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||||
remote.Body.Position = position;
|
remote.Body.Position = position;
|
||||||
remote.Body.Orientation = Quaternion.Identity;
|
remote.Body.Orientation = Quaternion.Identity;
|
||||||
|
|
|
||||||
|
|
@ -1089,13 +1089,14 @@ public sealed class RemoteTeleportControllerTests
|
||||||
const uint sourceCell = 0xA9B40039u;
|
const uint sourceCell = 0xA9B40039u;
|
||||||
const uint destinationCell = 0xAAB40001u;
|
const uint destinationCell = 0xAAB40001u;
|
||||||
RemoteTeleportController? controller = null;
|
RemoteTeleportController? controller = null;
|
||||||
|
LiveEntityRecord? owner = null;
|
||||||
bool cleanupAfterFailureRan = false;
|
bool cleanupAfterFailureRan = false;
|
||||||
var resources = new DelegateLiveEntityResourceLifecycle(
|
var resources = new DelegateLiveEntityResourceLifecycle(
|
||||||
_ => { },
|
_ => { },
|
||||||
_ => LiveEntityTeardown.Run(
|
_ => LiveEntityTeardown.Run(
|
||||||
[
|
[
|
||||||
() => throw new InvalidOperationException("effect teardown failed"),
|
() => throw new InvalidOperationException("effect teardown failed"),
|
||||||
() => controller!.Forget(guid),
|
() => controller!.Forget(owner!),
|
||||||
() => cleanupAfterFailureRan = true,
|
() => cleanupAfterFailureRan = true,
|
||||||
]));
|
]));
|
||||||
var spatial = new GpuWorldState();
|
var spatial = new GpuWorldState();
|
||||||
|
|
@ -1117,6 +1118,7 @@ public sealed class RemoteTeleportControllerTests
|
||||||
var remote = new AcDream.App.Physics.RemoteMotion();
|
var remote = new AcDream.App.Physics.RemoteMotion();
|
||||||
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
|
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
|
||||||
live.SetRemoteMotionRuntime(guid, remote);
|
live.SetRemoteMotionRuntime(guid, remote);
|
||||||
|
Assert.True(live.TryGetRecord(guid, out owner));
|
||||||
controller = new RemoteTeleportController(
|
controller = new RemoteTeleportController(
|
||||||
BuildEngine(includeDestination: false),
|
BuildEngine(includeDestination: false),
|
||||||
live,
|
live,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public sealed class AttachmentUpdateOrderTests
|
||||||
var calls = new List<uint>();
|
var calls = new List<uint>();
|
||||||
|
|
||||||
var children = insertionOrder.ToDictionary(id => id, id => parents[id]);
|
var children = insertionOrder.ToDictionary(id => id, id => parents[id]);
|
||||||
new AttachmentUpdateOrder<uint?>().ForEachParentFirst(
|
new AttachmentUpdateOrder<uint, uint?>().ForEachParentFirst(
|
||||||
children,
|
children,
|
||||||
static parentId => parentId,
|
static parentId => parentId,
|
||||||
id =>
|
id =>
|
||||||
|
|
@ -44,7 +44,7 @@ public sealed class AttachmentUpdateOrderTests
|
||||||
};
|
};
|
||||||
var calls = new List<uint>();
|
var calls = new List<uint>();
|
||||||
|
|
||||||
IReadOnlyList<uint> failed = new AttachmentUpdateOrder<uint?>().ForEachParentFirst(
|
IReadOnlyList<uint> failed = new AttachmentUpdateOrder<uint, uint?>().ForEachParentFirst(
|
||||||
children,
|
children,
|
||||||
static parentId => parentId,
|
static parentId => parentId,
|
||||||
id =>
|
id =>
|
||||||
|
|
@ -128,7 +128,7 @@ public sealed class AttachmentUpdateOrderTests
|
||||||
[20u] = 10u,
|
[20u] = 10u,
|
||||||
[30u] = 20u,
|
[30u] = 20u,
|
||||||
};
|
};
|
||||||
var order = new AttachmentUpdateOrder<uint?>().CollectSubtreePostOrder(
|
var order = new AttachmentUpdateOrder<uint, uint?>().CollectSubtreePostOrder(
|
||||||
children,
|
children,
|
||||||
10u,
|
10u,
|
||||||
static parentId => parentId);
|
static parentId => parentId);
|
||||||
|
|
@ -146,7 +146,7 @@ public sealed class AttachmentUpdateOrderTests
|
||||||
};
|
};
|
||||||
var realized = new List<uint>();
|
var realized = new List<uint>();
|
||||||
|
|
||||||
new AttachmentUpdateOrder<uint?>().RealizeDescendants(
|
new AttachmentUpdateOrder<uint, uint?>().RealizeDescendants(
|
||||||
10u,
|
10u,
|
||||||
parent => waitingByParent.TryGetValue(parent, out var waiting)
|
parent => waitingByParent.TryGetValue(parent, out var waiting)
|
||||||
? waiting
|
? waiting
|
||||||
|
|
@ -170,7 +170,7 @@ public sealed class AttachmentUpdateOrderTests
|
||||||
};
|
};
|
||||||
var attempted = new List<uint>();
|
var attempted = new List<uint>();
|
||||||
|
|
||||||
new AttachmentUpdateOrder<uint?>().RealizeDescendants(
|
new AttachmentUpdateOrder<uint, uint?>().RealizeDescendants(
|
||||||
10u,
|
10u,
|
||||||
parent => waitingByParent.TryGetValue(parent, out var waiting)
|
parent => waitingByParent.TryGetValue(parent, out var waiting)
|
||||||
? waiting
|
? waiting
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,8 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
LiveEntityProjectionKind.Attached);
|
LiveEntityProjectionKind.Attached);
|
||||||
fixture.InstallAttached(parent, oldChild);
|
fixture.InstallAttached(parent, oldChild);
|
||||||
|
|
||||||
LiveEntityRecord replacement = fixture.Live.RegisterLiveEntity(
|
LiveEntityRecord replacement = fixture.Live.RegisterAndMaterializeProjection(
|
||||||
ControllerFixture.SpawnData(0x70000201u, generation: 2)).Record!;
|
ControllerFixture.SpawnData(0x70000201u, generation: 2));
|
||||||
|
|
||||||
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
||||||
Assert.True(fixture.Live.TryGetRecord(0x70000201u, out LiveEntityRecord current));
|
Assert.True(fixture.Live.TryGetRecord(0x70000201u, out LiveEntityRecord current));
|
||||||
|
|
@ -136,7 +136,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
fixture.Controller.ProjectionRemoved -= Throw;
|
fixture.Controller.ProjectionRemoved -= Throw;
|
||||||
Assert.Equal(1, fixture.Live.RetryPendingTeardowns());
|
Assert.Equal(1, fixture.Live.RetryPendingTeardowns());
|
||||||
|
|
||||||
static void Throw(uint _) =>
|
static void Throw(LiveEntityRecord _) =>
|
||||||
throw new InvalidOperationException("injected projection observer failure");
|
throw new InvalidOperationException("injected projection observer failure");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,11 +424,11 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
|
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
|
||||||
invalidParent.ServerGuid, childGuid, 1, 0, 1, 2));
|
invalidParent.ServerGuid, childGuid, 1, 0, 1, 2));
|
||||||
|
|
||||||
LiveEntityRecord child = fixture.RegisterOnly(
|
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
|
||||||
childGuid,
|
childGuid,
|
||||||
generation: 1,
|
generation: 1,
|
||||||
hasPosition: true);
|
hasPosition: true);
|
||||||
fixture.Materialize(child);
|
LiveEntityRecord child = fixture.Materialize(childIdentity);
|
||||||
Assert.True(fixture.Live.TryGetSnapshot(
|
Assert.True(fixture.Live.TryGetSnapshot(
|
||||||
childGuid,
|
childGuid,
|
||||||
out WorldSession.EntitySpawn childSpawn));
|
out WorldSession.EntitySpawn childSpawn));
|
||||||
|
|
@ -455,7 +455,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
ExactProjectionWithdrawalDisposition.Completed,
|
ExactProjectionWithdrawalDisposition.Completed,
|
||||||
Failure: null));
|
Failure: null));
|
||||||
LiveEntityRecord parent = fixture.Spawn(0x70000254u, generation: 1);
|
LiveEntityRecord parent = fixture.Spawn(0x70000254u, generation: 1);
|
||||||
LiveEntityRecord child = fixture.RegisterOnly(
|
RuntimeEntityRecord child = fixture.RegisterOnly(
|
||||||
0x70000255u,
|
0x70000255u,
|
||||||
generation: 1,
|
generation: 1,
|
||||||
hasPosition: false);
|
hasPosition: false);
|
||||||
|
|
@ -475,7 +475,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
out WorldSession.EntitySpawn snapshot));
|
out WorldSession.EntitySpawn snapshot));
|
||||||
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
|
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
|
||||||
Assert.Null(snapshot.Position);
|
Assert.Null(snapshot.Position);
|
||||||
Assert.Null(child.WorldEntity);
|
Assert.False(fixture.Live.TryGetRecord(child.ServerGuid, out _));
|
||||||
var relation = new ParentAttachmentRelation(
|
var relation = new ParentAttachmentRelation(
|
||||||
parent.ServerGuid,
|
parent.ServerGuid,
|
||||||
child.ServerGuid,
|
child.ServerGuid,
|
||||||
|
|
@ -490,9 +490,14 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
Array.Empty<Matrix4x4>());
|
Array.Empty<Matrix4x4>());
|
||||||
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
||||||
|
|
||||||
Assert.NotNull(child.WorldEntity);
|
Assert.True(fixture.Live.TryGetRecord(
|
||||||
Assert.True(child.IsSpatiallyProjected);
|
child.ServerGuid,
|
||||||
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
|
out LiveEntityRecord childProjection));
|
||||||
|
Assert.NotNull(childProjection.WorldEntity);
|
||||||
|
Assert.True(childProjection.IsSpatiallyProjected);
|
||||||
|
Assert.Equal(
|
||||||
|
LiveEntityProjectionKind.Attached,
|
||||||
|
childProjection.ProjectionKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -513,13 +518,12 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
PlacementId = null,
|
PlacementId = null,
|
||||||
PositionSequence = 0,
|
PositionSequence = 0,
|
||||||
};
|
};
|
||||||
LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!;
|
fixture.Live.RegisterLiveEntity(childSpawn);
|
||||||
|
|
||||||
fixture.Controller.OnSpawn(childSpawn);
|
fixture.Controller.OnSpawn(childSpawn);
|
||||||
|
|
||||||
var expected = new ParentAttachmentRelation(
|
var expected = new ParentAttachmentRelation(
|
||||||
parent.ServerGuid,
|
parent.ServerGuid,
|
||||||
child.ServerGuid,
|
childSpawn.Guid,
|
||||||
ParentLocation: 0,
|
ParentLocation: 0,
|
||||||
PlacementId: 0,
|
PlacementId: 0,
|
||||||
ParentInstanceSequence: 0,
|
ParentInstanceSequence: 0,
|
||||||
|
|
@ -527,6 +531,9 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
Assert.True(fixture.Live.ParentAttachments.IsCommitted(expected));
|
Assert.True(fixture.Live.ParentAttachments.IsCommitted(expected));
|
||||||
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
|
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
|
||||||
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
||||||
|
Assert.True(fixture.Live.TryGetRecord(
|
||||||
|
childSpawn.Guid,
|
||||||
|
out LiveEntityRecord child));
|
||||||
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
|
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
|
||||||
Assert.NotNull(child.WorldEntity);
|
Assert.NotNull(child.WorldEntity);
|
||||||
}
|
}
|
||||||
|
|
@ -560,10 +567,13 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
PlacementId = 0,
|
PlacementId = 0,
|
||||||
PositionSequence = 0,
|
PositionSequence = 0,
|
||||||
};
|
};
|
||||||
LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!;
|
fixture.Live.RegisterLiveEntity(childSpawn);
|
||||||
fixture.Controller.OnSpawn(childSpawn);
|
fixture.Controller.OnSpawn(childSpawn);
|
||||||
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
|
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
|
||||||
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
||||||
|
Assert.True(fixture.Live.TryGetRecord(
|
||||||
|
childSpawn.Guid,
|
||||||
|
out LiveEntityRecord child));
|
||||||
WorldEntity entity = child.WorldEntity!;
|
WorldEntity entity = child.WorldEntity!;
|
||||||
Assert.Null(entity.PaletteOverride);
|
Assert.Null(entity.PaletteOverride);
|
||||||
WorldSession.EntitySpawn grandchildSpawn = ControllerFixture.SpawnData(
|
WorldSession.EntitySpawn grandchildSpawn = ControllerFixture.SpawnData(
|
||||||
|
|
@ -576,9 +586,11 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
PlacementId = 0,
|
PlacementId = 0,
|
||||||
PositionSequence = 0,
|
PositionSequence = 0,
|
||||||
};
|
};
|
||||||
LiveEntityRecord grandchild = fixture.Live.RegisterLiveEntity(
|
fixture.Live.RegisterLiveEntity(grandchildSpawn);
|
||||||
grandchildSpawn).Record!;
|
|
||||||
fixture.Controller.OnSpawn(grandchildSpawn);
|
fixture.Controller.OnSpawn(grandchildSpawn);
|
||||||
|
Assert.True(fixture.Live.TryGetRecord(
|
||||||
|
grandchildSpawn.Guid,
|
||||||
|
out LiveEntityRecord grandchild));
|
||||||
WorldEntity grandchildEntity = grandchild.WorldEntity!;
|
WorldEntity grandchildEntity = grandchild.WorldEntity!;
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
LiveEntityProjectionKind.Attached,
|
LiveEntityProjectionKind.Attached,
|
||||||
|
|
@ -642,12 +654,12 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
using (fixture)
|
using (fixture)
|
||||||
{
|
{
|
||||||
LiveEntityRecord parent = fixture.Spawn(0x7000025Cu, generation: 1);
|
LiveEntityRecord parent = fixture.Spawn(0x7000025Cu, generation: 1);
|
||||||
LiveEntityRecord child = fixture.RegisterOnly(
|
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
|
||||||
0x7000025Du,
|
0x7000025Du,
|
||||||
generation: 1,
|
generation: 1,
|
||||||
hasPosition: true,
|
hasPosition: true,
|
||||||
hasSetup: false);
|
hasSetup: false);
|
||||||
fixture.Materialize(child);
|
LiveEntityRecord child = fixture.Materialize(childIdentity);
|
||||||
child.HasPartArray = false;
|
child.HasPartArray = false;
|
||||||
var update = new ParentEvent.Parsed(
|
var update = new ParentEvent.Parsed(
|
||||||
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
|
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
|
||||||
|
|
@ -763,7 +775,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
0x70000257u,
|
0x70000257u,
|
||||||
generation: 1,
|
generation: 1,
|
||||||
LiveEntityProjectionKind.Attached);
|
LiveEntityProjectionKind.Attached);
|
||||||
LiveEntityRecord waitingParent = fixture.RegisterOnly(
|
RuntimeEntityRecord waitingParent = fixture.RegisterOnly(
|
||||||
0x70000258u,
|
0x70000258u,
|
||||||
generation: 1,
|
generation: 1,
|
||||||
hasPosition: true);
|
hasPosition: true);
|
||||||
|
|
@ -1117,7 +1129,8 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LiveEntityRecord ChildRecord() => new(
|
private static LiveEntityRecord ChildRecord() =>
|
||||||
|
LiveEntityTestFixture.CreateExactProjectionRecord(
|
||||||
new WorldSession.EntitySpawn(
|
new WorldSession.EntitySpawn(
|
||||||
0x70000100u,
|
0x70000100u,
|
||||||
new CreateObject.ServerPosition(
|
new CreateObject.ServerPosition(
|
||||||
|
|
@ -1188,12 +1201,12 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
ushort generation,
|
ushort generation,
|
||||||
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
|
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
|
||||||
{
|
{
|
||||||
LiveEntityRecord record = RegisterOnly(guid, generation, hasPosition: true);
|
RuntimeEntityRecord canonical =
|
||||||
Materialize(record, kind);
|
RegisterOnly(guid, generation, hasPosition: true);
|
||||||
return record;
|
return Materialize(canonical, kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal LiveEntityRecord RegisterOnly(
|
internal RuntimeEntityRecord RegisterOnly(
|
||||||
uint guid,
|
uint guid,
|
||||||
ushort generation,
|
ushort generation,
|
||||||
bool hasPosition,
|
bool hasPosition,
|
||||||
|
|
@ -1204,28 +1217,35 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
spawn = spawn with { Position = null };
|
spawn = spawn with { Position = null };
|
||||||
if (!hasSetup)
|
if (!hasSetup)
|
||||||
spawn = spawn with { SetupTableId = null };
|
spawn = spawn with { SetupTableId = null };
|
||||||
return Live.RegisterLiveEntity(spawn).Record!;
|
return Assert.IsType<RuntimeEntityRecord>(
|
||||||
|
Live.RegisterLiveEntity(spawn).Canonical);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Materialize(
|
internal LiveEntityRecord Materialize(
|
||||||
LiveEntityRecord record,
|
RuntimeEntityRecord canonical,
|
||||||
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
|
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
|
||||||
{
|
{
|
||||||
Live.MaterializeLiveEntity(
|
WorldEntity? entity = Live.MaterializeLiveEntity(
|
||||||
record.ServerGuid,
|
canonical,
|
||||||
Cell,
|
Cell,
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
ServerGuid = record.ServerGuid,
|
ServerGuid = canonical.ServerGuid,
|
||||||
SourceGfxObjOrSetupId = 0x02000001u,
|
SourceGfxObjOrSetupId = 0x02000001u,
|
||||||
Position = Vector3.Zero,
|
Position = Vector3.Zero,
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = Cell,
|
ParentCellId = Cell,
|
||||||
},
|
},
|
||||||
kind);
|
kind,
|
||||||
|
initializeProjection: null,
|
||||||
|
out LiveEntityRecord? projected);
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
LiveEntityRecord record =
|
||||||
|
Assert.IsType<LiveEntityRecord>(projected);
|
||||||
record.HasPartArray = true;
|
record.HasPartArray = true;
|
||||||
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation) => new(
|
internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation) => new(
|
||||||
|
|
@ -1277,7 +1297,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
"_attachedByChild",
|
"_attachedByChild",
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
var map = (IDictionary)mapField.GetValue(Controller)!;
|
var map = (IDictionary)mapField.GetValue(Controller)!;
|
||||||
map.Add(child.ServerGuid, attached);
|
map.Add(child.ProjectionKey!.Value, attached);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void CommitRenderedRelation(ParentAttachmentRelation relation)
|
internal void CommitRenderedRelation(ParentAttachmentRelation relation)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public sealed class LiveAppearanceAnimationTests
|
||||||
var runtime = new AcDream.App.World.LiveEntityRuntime(
|
var runtime = new AcDream.App.World.LiveEntityRuntime(
|
||||||
new AcDream.App.Streaming.GpuWorldState(),
|
new AcDream.App.Streaming.GpuWorldState(),
|
||||||
new AcDream.App.World.DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new AcDream.App.World.DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
var record = runtime.RegisterLiveEntity(new AcDream.Core.Net.WorldSession.EntitySpawn(
|
var spawn = new AcDream.Core.Net.WorldSession.EntitySpawn(
|
||||||
guid,
|
guid,
|
||||||
Position: null,
|
Position: null,
|
||||||
SetupTableId: null,
|
SetupTableId: null,
|
||||||
|
|
@ -33,8 +33,11 @@ public sealed class LiveAppearanceAnimationTests
|
||||||
ItemType: null,
|
ItemType: null,
|
||||||
MotionState: null,
|
MotionState: null,
|
||||||
MotionTableId: null,
|
MotionTableId: null,
|
||||||
InstanceSequence: 1)).Record!;
|
InstanceSequence: 1);
|
||||||
WorldEntity entity = Entity(0x70000002u, 0x01000001u);
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(
|
||||||
|
spawn,
|
||||||
|
id => Entity(id, 0x01000001u, guid));
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
var state = new LiveEntityAnimationState
|
var state = new LiveEntityAnimationState
|
||||||
{
|
{
|
||||||
Entity = entity,
|
Entity = entity,
|
||||||
|
|
@ -47,7 +50,6 @@ public sealed class LiveAppearanceAnimationTests
|
||||||
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
|
||||||
PartAvailability = Array.Empty<bool>(),
|
PartAvailability = Array.Empty<bool>(),
|
||||||
};
|
};
|
||||||
record.WorldEntity = entity;
|
|
||||||
record.AnimationRuntime = state;
|
record.AnimationRuntime = state;
|
||||||
|
|
||||||
LiveEntityAppearanceUpdateState captured = Assert.IsType<LiveEntityAppearanceUpdateState>(
|
LiveEntityAppearanceUpdateState captured = Assert.IsType<LiveEntityAppearanceUpdateState>(
|
||||||
|
|
@ -188,10 +190,13 @@ public sealed class LiveAppearanceAnimationTests
|
||||||
MotionTableId: null,
|
MotionTableId: null,
|
||||||
InstanceSequence: instance);
|
InstanceSequence: instance);
|
||||||
|
|
||||||
private static WorldEntity Entity(uint id, uint gfxObjId) => new()
|
private static WorldEntity Entity(
|
||||||
|
uint id,
|
||||||
|
uint gfxObjId,
|
||||||
|
uint serverGuid = 0x50000001u) => new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
ServerGuid = 0x50000001u,
|
ServerGuid = serverGuid,
|
||||||
SourceGfxObjOrSetupId = 0x02000001u,
|
SourceGfxObjOrSetupId = 0x02000001u,
|
||||||
Position = Vector3.Zero,
|
Position = Vector3.Zero,
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
|
@ -64,9 +65,9 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
old.Live.SetAnimationRuntime(Guid, replacement);
|
old.Live.SetAnimationRuntime(Guid, replacement);
|
||||||
var presenter = Presenter(old.Live, new EntityEffectPoseRegistry(), new Context());
|
var presenter = Presenter(old.Live, new EntityEffectPoseRegistry(), new Context());
|
||||||
|
|
||||||
presenter.Present(new Dictionary<uint, LiveEntityAnimationSchedule>
|
presenter.Present(new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
{
|
{
|
||||||
[old.Entity.Id] = stale,
|
[old.Record.ProjectionKey!.Value] = stale,
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Empty(replacement.MeshRefsScratch);
|
Assert.Empty(replacement.MeshRefsScratch);
|
||||||
|
|
@ -178,9 +179,9 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
[true]);
|
[true]);
|
||||||
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
|
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
|
||||||
|
|
||||||
presenter.Present(new Dictionary<uint, LiveEntityAnimationSchedule>
|
presenter.Present(new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
{
|
{
|
||||||
[fixture.Entity.Id] = stale,
|
[fixture.Record.ProjectionKey!.Value] = stale,
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Empty(fixture.State.MeshRefsScratch);
|
Assert.Empty(fixture.State.MeshRefsScratch);
|
||||||
|
|
@ -252,11 +253,11 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
Assert.Equal(2, nested.Count);
|
Assert.Equal(2, nested.Count);
|
||||||
};
|
};
|
||||||
var presenter = Presenter(first.Live, poses, new Context());
|
var presenter = Presenter(first.Live, poses, new Context());
|
||||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
{
|
{
|
||||||
[first.Entity.Id] = Schedule(first,
|
[first.Record.ProjectionKey!.Value] = Schedule(first,
|
||||||
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
||||||
[second.Entity.Id] = Schedule(second,
|
[second.Record.ProjectionKey!.Value] = Schedule(second,
|
||||||
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
|
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -283,11 +284,11 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
first.Live.SetAnimationRuntime(Guid + 1, replacement);
|
first.Live.SetAnimationRuntime(Guid + 1, replacement);
|
||||||
};
|
};
|
||||||
var presenter = Presenter(first.Live, poses, new Context());
|
var presenter = Presenter(first.Live, poses, new Context());
|
||||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
{
|
{
|
||||||
[first.Entity.Id] = Schedule(first,
|
[first.Record.ProjectionKey!.Value] = Schedule(first,
|
||||||
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
||||||
[second.Entity.Id] = Schedule(second,
|
[second.Record.ProjectionKey!.Value] = Schedule(second,
|
||||||
[new PartTransform(new Vector3(99f, 0f, 0f), Quaternion.Identity)]),
|
[new PartTransform(new Vector3(99f, 0f, 0f), Quaternion.Identity)]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -305,11 +306,11 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
var second = Add(first.Live, Guid + 1, partCount: 1);
|
var second = Add(first.Live, Guid + 1, partCount: 1);
|
||||||
var poses = new EntityEffectPoseRegistry();
|
var poses = new EntityEffectPoseRegistry();
|
||||||
var presenter = Presenter(first.Live, poses, new Context());
|
var presenter = Presenter(first.Live, poses, new Context());
|
||||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
{
|
{
|
||||||
[first.Entity.Id] = Schedule(first,
|
[first.Record.ProjectionKey!.Value] = Schedule(first,
|
||||||
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
||||||
[second.Entity.Id] = Schedule(second,
|
[second.Record.ProjectionKey!.Value] = Schedule(second,
|
||||||
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
|
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
|
||||||
};
|
};
|
||||||
bool recursed = false;
|
bool recursed = false;
|
||||||
|
|
@ -359,9 +360,9 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
fixture.Record.ObjectClockEpoch,
|
fixture.Record.ObjectClockEpoch,
|
||||||
fixture.Record.ProjectionMutationVersion,
|
fixture.Record.ProjectionMutationVersion,
|
||||||
fixture.State.PresentationRevision);
|
fixture.State.PresentationRevision);
|
||||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
var schedules = new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
{
|
{
|
||||||
[fixture.Entity.Id] = schedule,
|
[fixture.Record.ProjectionKey!.Value] = schedule,
|
||||||
};
|
};
|
||||||
var poses = new EntityEffectPoseRegistry();
|
var poses = new EntityEffectPoseRegistry();
|
||||||
var presenter = Presenter(fixture.Live, poses, new Context());
|
var presenter = Presenter(fixture.Live, poses, new Context());
|
||||||
|
|
@ -407,12 +408,12 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> Schedules(
|
private static IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> Schedules(
|
||||||
Fixture fixture,
|
Fixture fixture,
|
||||||
IReadOnlyList<PartTransform> frames) =>
|
IReadOnlyList<PartTransform> frames) =>
|
||||||
new Dictionary<uint, LiveEntityAnimationSchedule>
|
new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||||
{
|
{
|
||||||
[fixture.Entity.Id] = Schedule(fixture, frames),
|
[fixture.Record.ProjectionKey!.Value] = Schedule(fixture, frames),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static LiveEntityAnimationSchedule Schedule(
|
private static LiveEntityAnimationSchedule Schedule(
|
||||||
|
|
@ -452,10 +453,8 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
float scale = 1f,
|
float scale = 1f,
|
||||||
bool withSequencer = true)
|
bool withSequencer = true)
|
||||||
{
|
{
|
||||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid)).Record!;
|
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
|
||||||
WorldEntity entity = live.MaterializeLiveEntity(
|
Spawn(guid),
|
||||||
guid,
|
|
||||||
Cell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -465,7 +464,8 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = Cell,
|
ParentCellId = Cell,
|
||||||
})!;
|
});
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
LiveEntityAnimationState state = State(entity, partCount, scale, withSequencer);
|
LiveEntityAnimationState state = State(entity, partCount, scale, withSequencer);
|
||||||
entity.SetIndexedPartPoses(
|
entity.SetIndexedPartPoses(
|
||||||
Enumerable.Repeat(Matrix4x4.Identity, partCount).ToArray(),
|
Enumerable.Repeat(Matrix4x4.Identity, partCount).ToArray(),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -36,14 +37,14 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
localHiddenPartPoseDirty: true,
|
localHiddenPartPoseDirty: true,
|
||||||
liveCenterX: 0,
|
liveCenterX: 0,
|
||||||
liveCenterY: 0)
|
liveCenterY: 0)
|
||||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked));
|
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule marked));
|
||||||
bool repeated = scheduler.Tick(
|
bool repeated = scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum,
|
PhysicsBody.MaxQuantum,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
liveCenterX: 0,
|
liveCenterX: 0,
|
||||||
liveCenterY: 0)
|
liveCenterY: 0)
|
||||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed);
|
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule consumed);
|
||||||
|
|
||||||
Assert.True(marked.ComposeParts);
|
Assert.True(marked.ComposeParts);
|
||||||
Assert.NotNull(marked.SequenceFrames);
|
Assert.NotNull(marked.SequenceFrames);
|
||||||
|
|
@ -73,14 +74,14 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
liveCenterX: 0,
|
liveCenterX: 0,
|
||||||
liveCenterY: 0)
|
liveCenterY: 0)
|
||||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule marked));
|
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule marked));
|
||||||
bool repeated = scheduler.Tick(
|
bool repeated = scheduler.Tick(
|
||||||
elapsedSeconds: 0f,
|
elapsedSeconds: 0f,
|
||||||
playerPosition: null,
|
playerPosition: null,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
liveCenterX: 0,
|
liveCenterX: 0,
|
||||||
liveCenterY: 0)
|
liveCenterY: 0)
|
||||||
.TryGetValue(animation.Entity.Id, out LiveEntityAnimationSchedule consumed);
|
.TryGetValue(record.ProjectionKey!.Value, out LiveEntityAnimationSchedule consumed);
|
||||||
|
|
||||||
Assert.True(marked.ComposeParts);
|
Assert.True(marked.ComposeParts);
|
||||||
Assert.NotNull(marked.SequenceFrames);
|
Assert.NotNull(marked.SequenceFrames);
|
||||||
|
|
@ -91,7 +92,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VisibleAnimationWithoutRemoteMotion_AppliesCompleteRootFrame()
|
public void VisibleAnimationWithoutRemoteMotion_AppliesCompleteRootFrame()
|
||||||
{
|
{
|
||||||
var (live, _, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
||||||
var retainedBodyOwner = BuildRemote(animation.Entity);
|
var retainedBodyOwner = BuildRemote(animation.Entity);
|
||||||
retainedBodyOwner.Body.SnapToCell(
|
retainedBodyOwner.Body.SnapToCell(
|
||||||
Cell,
|
Cell,
|
||||||
|
|
@ -108,7 +109,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
liveCenterX: 0,
|
liveCenterX: 0,
|
||||||
liveCenterY: 0)
|
liveCenterY: 0)
|
||||||
.ContainsKey(animation.Entity.Id));
|
.ContainsKey(record.ProjectionKey!.Value));
|
||||||
|
|
||||||
Assert.True(animation.Entity.Position.X > startX + 1f);
|
Assert.True(animation.Entity.Position.X > startX + 1f);
|
||||||
}
|
}
|
||||||
|
|
@ -116,14 +117,14 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ScheduleOwnsPartFramesAcrossLaterSequencerSampling()
|
public void ScheduleOwnsPartFramesAcrossLaterSequencerSampling()
|
||||||
{
|
{
|
||||||
var (live, _, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(RemoteGuid);
|
||||||
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
|
LiveEntityAnimationScheduler scheduler = BuildScheduler(live, LocalGuid);
|
||||||
LiveEntityAnimationSchedule schedule = scheduler.Tick(
|
LiveEntityAnimationSchedule schedule = scheduler.Tick(
|
||||||
0.1f,
|
0.1f,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
liveCenterX: 0,
|
liveCenterX: 0,
|
||||||
liveCenterY: 0)[animation.Entity.Id];
|
liveCenterY: 0)[record.ProjectionKey!.Value];
|
||||||
PartTransform retained = schedule.SequenceFrames![0];
|
PartTransform retained = schedule.SequenceFrames![0];
|
||||||
animation.CaptureSequenceFrames(
|
animation.CaptureSequenceFrames(
|
||||||
[new PartTransform(new Vector3(77f, 76f, 75f), Quaternion.Identity)]);
|
[new PartTransform(new Vector3(77f, 76f, 75f), Quaternion.Identity)]);
|
||||||
|
|
@ -194,7 +195,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
liveCenterY: 0);
|
liveCenterY: 0);
|
||||||
|
|
||||||
Assert.True(entity.Position.X > startX);
|
Assert.True(entity.Position.X > startX);
|
||||||
Assert.Same(record, live.SpatialRootObjects[RemoteGuid]);
|
Assert.True(live.IsCurrentSpatialRootObject(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -224,7 +225,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
|
|
||||||
Assert.True(entity.Position.X > startX + 0.5f);
|
Assert.True(entity.Position.X > startX + 0.5f);
|
||||||
Assert.True(remote.Interp.IsActive || entity.Position.X >= startX + 0.99f);
|
Assert.True(remote.Interp.IsActive || entity.Position.X >= startX + 0.99f);
|
||||||
Assert.Same(record, live.SpatialRootObjects[RemoteGuid]);
|
Assert.True(live.IsCurrentSpatialRootObject(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -343,14 +344,14 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
physics: physics,
|
physics: physics,
|
||||||
projectiles: projectiles);
|
projectiles: projectiles);
|
||||||
|
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||||
0.1f,
|
0.1f,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
liveCenterX: 1,
|
liveCenterX: 1,
|
||||||
liveCenterY: 1);
|
liveCenterY: 1);
|
||||||
|
|
||||||
Assert.Contains(animation.Entity.Id, schedules.Keys);
|
Assert.Contains(record.ProjectionKey!.Value, schedules.Keys);
|
||||||
Assert.Equal(1, rootPublishes);
|
Assert.Equal(1, rootPublishes);
|
||||||
float distance = animation.Entity.Position.X - startX;
|
float distance = animation.Entity.Position.X - startX;
|
||||||
Assert.InRange(distance, 0.79f, 0.81f);
|
Assert.InRange(distance, 0.79f, 0.81f);
|
||||||
|
|
@ -374,7 +375,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
isLocalPlayer: false);
|
isLocalPlayer: false);
|
||||||
});
|
});
|
||||||
|
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum * 2.5f,
|
PhysicsBody.MaxQuantum * 2.5f,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
|
|
@ -407,7 +408,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
},
|
},
|
||||||
_ => rootPublishes++);
|
_ => rootPublishes++);
|
||||||
|
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum * 2.5f,
|
PhysicsBody.MaxQuantum * 2.5f,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
|
|
@ -441,7 +442,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
},
|
},
|
||||||
_ => rootPublishes++);
|
_ => rootPublishes++);
|
||||||
|
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum * 2.5f,
|
PhysicsBody.MaxQuantum * 2.5f,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
|
|
@ -472,7 +473,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
},
|
},
|
||||||
_ => rootPublishes++);
|
_ => rootPublishes++);
|
||||||
|
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum * 2.5f,
|
PhysicsBody.MaxQuantum * 2.5f,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
|
|
@ -509,7 +510,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
},
|
},
|
||||||
_ => rootPublishes++);
|
_ => rootPublishes++);
|
||||||
|
|
||||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
IReadOnlyDictionary<RuntimeEntityKey, LiveEntityAnimationSchedule> schedules = scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum * 2.5f,
|
PhysicsBody.MaxQuantum * 2.5f,
|
||||||
animation.Entity.Position,
|
animation.Entity.Position,
|
||||||
localHiddenPartPoseDirty: false,
|
localHiddenPartPoseDirty: false,
|
||||||
|
|
@ -572,7 +573,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
_ => throw new InvalidOperationException(),
|
_ => throw new InvalidOperationException(),
|
||||||
LiveEntityProjectionKind.Attached));
|
LiveEntityProjectionKind.Attached));
|
||||||
Assert.False(record.ObjectClock.IsActive);
|
Assert.False(record.ObjectClock.IsActive);
|
||||||
Assert.DoesNotContain(RemoteGuid, live.SpatialRootObjects.Keys);
|
Assert.False(live.IsCurrentSpatialRootObject(record));
|
||||||
|
|
||||||
scheduler.Tick(
|
scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum,
|
PhysicsBody.MaxQuantum,
|
||||||
|
|
@ -590,7 +591,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
_ => throw new InvalidOperationException(),
|
_ => throw new InvalidOperationException(),
|
||||||
LiveEntityProjectionKind.World));
|
LiveEntityProjectionKind.World));
|
||||||
Assert.True(record.ObjectClock.IsActive);
|
Assert.True(record.ObjectClock.IsActive);
|
||||||
Assert.Contains(RemoteGuid, live.SpatialRootObjects.Keys);
|
Assert.True(live.IsCurrentSpatialRootObject(record));
|
||||||
|
|
||||||
scheduler.Tick(
|
scheduler.Tick(
|
||||||
PhysicsBody.MaxQuantum,
|
PhysicsBody.MaxQuantum,
|
||||||
|
|
@ -634,8 +635,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
(_, _) => (0.48f, 1.835f));
|
(_, _) => (0.48f, 1.835f));
|
||||||
var identity = new LocalPlayerIdentityState { ServerGuid = localPlayerGuid };
|
var identity = new LocalPlayerIdentityState { ServerGuid = localPlayerGuid };
|
||||||
var poses = new EntityEffectPoseRegistry();
|
var poses = new EntityEffectPoseRegistry();
|
||||||
foreach (WorldEntity entity in live.MaterializedWorldEntities.Values)
|
foreach (LiveEntityRecord record in live.MaterializedRecords)
|
||||||
poses.PublishMeshRefs(entity);
|
poses.PublishMeshRefs(record.WorldEntity!);
|
||||||
IAnimationHookCaptureSink animationHooks = captureHooks is null
|
IAnimationHookCaptureSink animationHooks = captureHooks is null
|
||||||
? new AnimationHookCaptureSink(new AnimationHookFrameQueue(
|
? new AnimationHookCaptureSink(new AnimationHookFrameQueue(
|
||||||
new AnimationHookRouter(),
|
new AnimationHookRouter(),
|
||||||
|
|
@ -681,10 +682,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
var live = new LiveEntityRuntime(
|
var live = new LiveEntityRuntime(
|
||||||
spatial,
|
spatial,
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid)).Record!;
|
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
|
||||||
WorldEntity entity = live.MaterializeLiveEntity(
|
Spawn(guid),
|
||||||
guid,
|
|
||||||
Cell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -694,7 +693,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = Cell,
|
ParentCellId = Cell,
|
||||||
})!;
|
});
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
var setup = new Setup();
|
var setup = new Setup();
|
||||||
var animation = new LiveEntityAnimationState
|
var animation = new LiveEntityAnimationState
|
||||||
{
|
{
|
||||||
|
|
@ -803,10 +803,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
var live = new LiveEntityRuntime(
|
var live = new LiveEntityRuntime(
|
||||||
spatial,
|
spatial,
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
LiveEntityRecord record = live.RegisterLiveEntity(Spawn(guid, state)).Record!;
|
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
|
||||||
WorldEntity entity = live.MaterializeLiveEntity(
|
Spawn(guid, state),
|
||||||
guid,
|
|
||||||
Cell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -816,7 +814,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = Cell,
|
ParentCellId = Cell,
|
||||||
})!;
|
});
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
return (live, record, entity);
|
return (live, record, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,10 +294,8 @@ public sealed class LiveEntityCreateSupersessionRecoveryTests
|
||||||
|
|
||||||
private static LiveEntityRecord RegisterAndMaterialize(LiveEntityRuntime runtime)
|
private static LiveEntityRecord RegisterAndMaterialize(LiveEntityRuntime runtime)
|
||||||
{
|
{
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn()).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(
|
||||||
runtime.MaterializeLiveEntity(
|
Spawn(),
|
||||||
Guid,
|
|
||||||
Cell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
|
||||||
|
|
@ -147,8 +147,8 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
incarnation,
|
incarnation,
|
||||||
harness.Journal.Pending[0].Record.OwnerIncarnation);
|
harness.Journal.Pending[0].Record.OwnerIncarnation);
|
||||||
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
harness.Projections.OnProjectionRemoved(record);
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
harness.Projections.OnProjectionRemoved(record);
|
||||||
Assert.Equal(2, harness.Journal.Count);
|
Assert.Equal(2, harness.Journal.Count);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
RenderProjectionDeltaKind.UpdateFlags,
|
RenderProjectionDeltaKind.UpdateFlags,
|
||||||
|
|
@ -166,13 +166,10 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
uint firstLocalId = first.WorldEntity!.Id;
|
uint firstLocalId = first.WorldEntity!.Id;
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
LiveEntityRegistrationResult replacement =
|
LiveEntityRecord second = harness.Runtime.RegisterAndMaterializeProjection(
|
||||||
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
|
Spawn(Guid, instance: 5),
|
||||||
LiveEntityRecord second = replacement.Record!;
|
localId => Entity(localId, Guid));
|
||||||
WorldEntity secondEntity = harness.Runtime.MaterializeLiveEntity(
|
WorldEntity secondEntity = Assert.IsType<WorldEntity>(second.WorldEntity);
|
||||||
Guid,
|
|
||||||
CellId,
|
|
||||||
localId => Entity(localId, Guid))!;
|
|
||||||
second.InitialHydrationCompleted = true;
|
second.InitialHydrationCompleted = true;
|
||||||
LiveEntityReadyCandidate current = LiveEntityReadyCandidate.Capture(second);
|
LiveEntityReadyCandidate current = LiveEntityReadyCandidate.Capture(second);
|
||||||
Assert.True(harness.Projections.OnEntityReady(current));
|
Assert.True(harness.Projections.OnEntityReady(current));
|
||||||
|
|
@ -209,7 +206,7 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
|
|
||||||
LiveEntityRegistrationResult duplicate =
|
LiveEntityRegistrationResult duplicate =
|
||||||
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
|
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
|
||||||
Assert.Same(record, duplicate.Record);
|
Assert.Same(record, duplicate.Projection);
|
||||||
Assert.True(harness.Projections.OnEntityReady(
|
Assert.True(harness.Projections.OnEntityReady(
|
||||||
LiveEntityReadyCandidate.Capture(record)));
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
|
|
||||||
|
|
@ -285,7 +282,7 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
LiveEntityReadyCandidate.Capture(record)));
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
harness.Projections.OnProjectionRemoved(record);
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
harness.Projections.SynchronizeActiveSources();
|
harness.Projections.SynchronizeActiveSources();
|
||||||
|
|
@ -311,7 +308,7 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
LiveEntityReadyCandidate.Capture(record)));
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
harness.Projections.OnProjectionRemoved(record);
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, CellId));
|
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, CellId));
|
||||||
|
|
||||||
|
|
@ -337,7 +334,7 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
LiveEntityReadyCandidate.Capture(record)));
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
harness.Projections.OnProjectionRemoved(record);
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
Assert.True(harness.Runtime.UnregisterLiveEntity(
|
Assert.True(harness.Runtime.UnregisterLiveEntity(
|
||||||
|
|
@ -383,14 +380,11 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
LiveEntityProjectionKind projectionKind =
|
LiveEntityProjectionKind projectionKind =
|
||||||
LiveEntityProjectionKind.World)
|
LiveEntityProjectionKind.World)
|
||||||
{
|
{
|
||||||
LiveEntityRecord record =
|
LiveEntityRecord record = harness.Runtime.RegisterAndMaterializeProjection(
|
||||||
harness.Runtime.RegisterLiveEntity(Spawn(guid, instance)).Record!;
|
Spawn(guid, instance),
|
||||||
WorldEntity? entity = harness.Runtime.MaterializeLiveEntity(
|
|
||||||
guid,
|
|
||||||
CellId,
|
|
||||||
localId => Entity(localId, guid),
|
localId => Entity(localId, guid),
|
||||||
projectionKind);
|
projectionKind);
|
||||||
Assert.NotNull(entity);
|
Assert.NotNull(record.WorldEntity);
|
||||||
record.InitialHydrationCompleted = true;
|
record.InitialHydrationCompleted = true;
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -635,11 +635,10 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
||||||
body.SnapToCell(0x01010001u, entity.Position, Vector3.Zero);
|
body.SnapToCell(0x01010001u, entity.Position, Vector3.Zero);
|
||||||
LiveEntityAnimationState state = LiveState(entity, setup, sequencer);
|
LiveEntityAnimationState state = LiveState(entity, setup, sequencer);
|
||||||
Assert.True(scheduler.BindLiveOwner(entity, state, body));
|
Assert.True(scheduler.BindLiveOwner(entity, state, body));
|
||||||
var record = new LiveEntityRecord(Spawn(serverGuid))
|
var record = LiveEntityTestFixture.CreateExactProjectionRecord(
|
||||||
{
|
Spawn(serverGuid));
|
||||||
WorldEntity = entity,
|
record.WorldEntity = entity;
|
||||||
AnimationRuntime = state,
|
record.AnimationRuntime = state;
|
||||||
};
|
|
||||||
int motionDone = 0;
|
int motionDone = 0;
|
||||||
sequencer.MotionDoneTarget = (_, success) =>
|
sequencer.MotionDoneTarget = (_, success) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -119,12 +119,12 @@ public sealed class EntityEffectControllerTests
|
||||||
EntityEffectProfile? profile = null)
|
EntityEffectProfile? profile = null)
|
||||||
{
|
{
|
||||||
WorldSession.EntitySpawn spawn = Spawn(guid, generation);
|
WorldSession.EntitySpawn spawn = Spawn(guid, generation);
|
||||||
Runtime.RegisterLiveEntity(spawn);
|
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(
|
||||||
Runtime.SetEffectProfile(guid, profile ?? LiveProfile());
|
spawn,
|
||||||
WorldEntity entity = Runtime.MaterializeLiveEntity(
|
id => Entity(id, guid),
|
||||||
guid,
|
initializeProjection: exact =>
|
||||||
spawn.Position!.Value.LandblockId,
|
exact.EffectProfile = profile ?? LiveProfile());
|
||||||
id => Entity(id, guid))!;
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
Assert.True(Controller.OnLiveEntityReady(guid));
|
Assert.True(Controller.OnLiveEntityReady(guid));
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
@ -172,12 +172,11 @@ public sealed class EntityEffectControllerTests
|
||||||
var fixture = new Fixture();
|
var fixture = new Fixture();
|
||||||
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
|
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
|
||||||
WorldSession.EntitySpawn spawn = Spawn(Guid, 1);
|
WorldSession.EntitySpawn spawn = Spawn(Guid, 1);
|
||||||
fixture.Runtime.RegisterLiveEntity(spawn);
|
fixture.Runtime.RegisterAndMaterializeProjection(
|
||||||
fixture.Runtime.SetEffectProfile(Guid, Fixture.LiveProfile());
|
spawn,
|
||||||
fixture.Runtime.MaterializeLiveEntity(
|
id => Entity(id, Guid),
|
||||||
Guid,
|
initializeProjection: record =>
|
||||||
spawn.Position!.Value.LandblockId,
|
record.EffectProfile = Fixture.LiveProfile());
|
||||||
id => Entity(id, Guid));
|
|
||||||
|
|
||||||
Assert.True(fixture.Controller.PrepareLiveEntityOwner(Guid));
|
Assert.True(fixture.Controller.PrepareLiveEntityOwner(Guid));
|
||||||
Assert.Equal(1, fixture.Controller.PendingPacketCount);
|
Assert.Equal(1, fixture.Controller.PendingPacketCount);
|
||||||
|
|
@ -279,12 +278,13 @@ public sealed class EntityEffectControllerTests
|
||||||
var fixture = new Fixture();
|
var fixture = new Fixture();
|
||||||
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
|
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
|
||||||
WorldSession.EntitySpawn spawn = Spawn(Guid, 1);
|
WorldSession.EntitySpawn spawn = Spawn(Guid, 1);
|
||||||
fixture.Runtime.RegisterLiveEntity(spawn);
|
LiveEntityRecord record = fixture.Runtime.RegisterAndMaterializeProjection(
|
||||||
fixture.Runtime.SetEffectProfile(Guid, Fixture.LiveProfile());
|
spawn,
|
||||||
WorldEntity entity = fixture.Runtime.MaterializeLiveEntity(
|
id => Entity(id, Guid),
|
||||||
Guid,
|
initializeProjection: exact =>
|
||||||
0x02020001u,
|
exact.EffectProfile = Fixture.LiveProfile());
|
||||||
id => Entity(id, Guid))!;
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
|
fixture.Runtime.RebucketLiveEntity(Guid, 0x02020001u);
|
||||||
|
|
||||||
Assert.True(fixture.Controller.OnLiveEntityReady(Guid));
|
Assert.True(fixture.Controller.OnLiveEntityReady(Guid));
|
||||||
Assert.Equal(1, fixture.Controller.PendingPacketCount);
|
Assert.Equal(1, fixture.Controller.PendingPacketCount);
|
||||||
|
|
@ -360,6 +360,7 @@ public sealed class EntityEffectControllerTests
|
||||||
WorldSession.EntitySpawn first = Spawn(Guid, generation: 1);
|
WorldSession.EntitySpawn first = Spawn(Guid, generation: 1);
|
||||||
fixture.Runtime.RegisterLiveEntity(first);
|
fixture.Runtime.RegisterLiveEntity(first);
|
||||||
|
|
||||||
|
fixture.Controller.ForgetUnknownOwner(Guid);
|
||||||
Assert.True(fixture.Runtime.UnregisterLiveEntity(
|
Assert.True(fixture.Runtime.UnregisterLiveEntity(
|
||||||
new DeleteObject.Parsed(Guid, 1),
|
new DeleteObject.Parsed(Guid, 1),
|
||||||
isLocalPlayer: false));
|
isLocalPlayer: false));
|
||||||
|
|
@ -383,11 +384,11 @@ public sealed class EntityEffectControllerTests
|
||||||
isLocalPlayer: false));
|
isLocalPlayer: false));
|
||||||
Assert.Equal(1, fixture.Controller.PendingPacketCount);
|
Assert.Equal(1, fixture.Controller.PendingPacketCount);
|
||||||
|
|
||||||
fixture.Runtime.SetEffectProfile(Guid, Fixture.LiveProfile());
|
fixture.Runtime.RegisterAndMaterializeProjection(
|
||||||
fixture.Runtime.MaterializeLiveEntity(
|
current,
|
||||||
Guid,
|
id => Entity(id, Guid),
|
||||||
current.Position!.Value.LandblockId,
|
initializeProjection: record =>
|
||||||
id => Entity(id, Guid));
|
record.EffectProfile = Fixture.LiveProfile());
|
||||||
Assert.True(fixture.Controller.OnLiveEntityReady(Guid));
|
Assert.True(fixture.Controller.OnLiveEntityReady(Guid));
|
||||||
fixture.Runner.Tick(0.0);
|
fixture.Runner.Tick(0.0);
|
||||||
Assert.Equal([1u], EmitterIds(fixture.Sink));
|
Assert.Equal([1u], EmitterIds(fixture.Sink));
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
||||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||||
WorldEntity oldEntity = MakeEntity(61u, guid);
|
WorldEntity oldEntity = MakeEntity(61u, guid);
|
||||||
oldEntity.MeshRefs = [new MeshRef(0x01000100u, Matrix4x4.Identity)];
|
oldEntity.MeshRefs = [new MeshRef(0x01000100u, Matrix4x4.Identity)];
|
||||||
WorldEntity replacement = MakeEntity(62u, guid);
|
WorldEntity replacement = MakeEntity(61u, guid);
|
||||||
replacement.MeshRefs = [new MeshRef(0x01000200u, Matrix4x4.Identity)];
|
replacement.MeshRefs = [new MeshRef(0x01000200u, Matrix4x4.Identity)];
|
||||||
|
|
||||||
adapter.OnCreate(oldEntity);
|
adapter.OnCreate(oldEntity);
|
||||||
|
|
@ -280,7 +280,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
||||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||||
WorldEntity oldEntity = MakeEntity(101u, guid);
|
WorldEntity oldEntity = MakeEntity(101u, guid);
|
||||||
oldEntity.MeshRefs = [new MeshRef((uint)oldMeshId, Matrix4x4.Identity)];
|
oldEntity.MeshRefs = [new MeshRef((uint)oldMeshId, Matrix4x4.Identity)];
|
||||||
WorldEntity replacement = MakeEntity(102u, guid);
|
WorldEntity replacement = MakeEntity(101u, guid);
|
||||||
AnimatedEntityState oldState = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(oldEntity));
|
AnimatedEntityState oldState = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(oldEntity));
|
||||||
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
|
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
|
||||||
|
|
||||||
|
|
@ -682,7 +682,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
||||||
adapter.OnCreate(oldEntity);
|
adapter.OnCreate(oldEntity);
|
||||||
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
|
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
|
||||||
|
|
||||||
WorldEntity replacement = MakeEntity(202u, guid);
|
WorldEntity replacement = MakeEntity(201u, guid);
|
||||||
replacement.MeshRefs = [new MeshRef((uint)replacementMesh, Matrix4x4.Identity)];
|
replacement.MeshRefs = [new MeshRef((uint)replacementMesh, Matrix4x4.Identity)];
|
||||||
adapter.OnCreate(replacement);
|
adapter.OnCreate(replacement);
|
||||||
Assert.True(adapter.SetPresentationResident(replacement, resident: true));
|
Assert.True(adapter.SetPresentationResident(replacement, resident: true));
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,12 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Assert.Equal(Harness.PlayerGuid, harness.Identity.ServerGuid);
|
Assert.Equal(Harness.PlayerGuid, harness.Identity.ServerGuid);
|
||||||
Assert.Equal(RuntimeLifecycleState.InWorld, harness.Runtime.Lifecycle.State);
|
Assert.Equal(RuntimeLifecycleState.InWorld, harness.Runtime.Lifecycle.State);
|
||||||
|
|
||||||
LiveEntityRegistrationResult registration =
|
LiveEntityRecord liveRecord =
|
||||||
harness.Entities.RegisterLiveEntity(Spawn(
|
harness.Entities.RegisterAndMaterializeProjection(Spawn(
|
||||||
Harness.TargetGuid,
|
Harness.TargetGuid,
|
||||||
instance: 3,
|
instance: 3,
|
||||||
cell: 0x12340001u));
|
cell: 0x12340001u));
|
||||||
Assert.NotNull(registration.Record);
|
Assert.NotNull(liveRecord.WorldEntity);
|
||||||
|
|
||||||
var item = new ClientObject
|
var item = new ClientObject
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -227,14 +227,15 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
WorldEntity entity = Entity(1u, 0x73600001u);
|
WorldEntity entity = Entity(1u, 0x73600001u);
|
||||||
state.PlaceLiveEntityProjection(landblock, entity);
|
state.PlaceLiveEntityProjection(landblock, entity);
|
||||||
var edges = new List<(uint Guid, bool Visible)>();
|
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (guid, visible) => edges.Add((guid, visible));
|
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
||||||
|
edges.Add((localEntityId, visible));
|
||||||
|
|
||||||
state.RemoveLandblock(landblock);
|
state.RemoveLandblock(landblock);
|
||||||
|
|
||||||
Assert.Empty(state.Entities);
|
Assert.Empty(state.Entities);
|
||||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||||
Assert.False(state.IsLiveEntityVisible(entity.ServerGuid));
|
Assert.False(state.IsLiveEntityVisible(entity.Id));
|
||||||
|
|
||||||
state.AddLandblock(new LoadedLandblock(
|
state.AddLandblock(new LoadedLandblock(
|
||||||
landblock,
|
landblock,
|
||||||
|
|
@ -244,9 +245,9 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Assert.Same(entity, Assert.Single(state.Entities));
|
Assert.Same(entity, Assert.Single(state.Entities));
|
||||||
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? reloaded));
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? reloaded));
|
||||||
Assert.Same(entity, Assert.Single(reloaded!.Entities));
|
Assert.Same(entity, Assert.Single(reloaded!.Entities));
|
||||||
Assert.True(state.IsLiveEntityVisible(entity.ServerGuid));
|
Assert.True(state.IsLiveEntityVisible(entity.Id));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[(entity.ServerGuid, false), (entity.ServerGuid, true)],
|
[(entity.Id, false), (entity.Id, true)],
|
||||||
edges);
|
edges);
|
||||||
|
|
||||||
state.RemoveLiveEntityProjection(entity);
|
state.RemoveLiveEntityProjection(entity);
|
||||||
|
|
@ -279,9 +280,9 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
state.MarkPersistent(playerGuid);
|
state.MarkPersistent(playerGuid);
|
||||||
state.PlaceLiveEntityProjection(secondLandblock, player);
|
state.PlaceLiveEntityProjection(secondLandblock, player);
|
||||||
state.PlaceLiveEntityProjection(pendingLandblock, pending);
|
state.PlaceLiveEntityProjection(pendingLandblock, pending);
|
||||||
var edges = new List<(uint Guid, bool Visible)>();
|
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
||||||
edges.Add((guid, visible));
|
edges.Add((localEntityId, visible));
|
||||||
|
|
||||||
GpuWorldRecenterRetirement result =
|
GpuWorldRecenterRetirement result =
|
||||||
state.DetachAllForOriginRecenter();
|
state.DetachAllForOriginRecenter();
|
||||||
|
|
@ -316,7 +317,7 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Assert.Equal(2, state.PendingLiveEntityCount);
|
Assert.Equal(2, state.PendingLiveEntityCount);
|
||||||
Assert.Same(player, Assert.Single(state.DrainRescued()));
|
Assert.Same(player, Assert.Single(state.DrainRescued()));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[(remote.ServerGuid, false), (player.ServerGuid, false)],
|
[(remote.Id, false), (player.Id, false)],
|
||||||
edges);
|
edges);
|
||||||
|
|
||||||
state.AddLandblock(new LoadedLandblock(
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
|
@ -378,12 +379,12 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
WorldEntity entity = Entity(1u, 0x73700001u);
|
WorldEntity entity = Entity(1u, 0x73700001u);
|
||||||
int callbacks = 0;
|
int callbacks = 0;
|
||||||
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
||||||
{
|
{
|
||||||
callbacks++;
|
callbacks++;
|
||||||
Assert.Equal(entity.ServerGuid, guid);
|
Assert.Equal(entity.Id, localEntityId);
|
||||||
Assert.True(visible);
|
Assert.True(visible);
|
||||||
Assert.True(state.IsLiveEntityVisible(guid));
|
Assert.True(state.IsLiveEntityVisible(localEntityId));
|
||||||
Assert.Same(entity, Assert.Single(state.Entities));
|
Assert.Same(entity, Assert.Single(state.Entities));
|
||||||
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
||||||
Assert.Same(entity, Assert.Single(loaded!.Entities));
|
Assert.Same(entity, Assert.Single(loaded!.Entities));
|
||||||
|
|
@ -414,13 +415,14 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
WorldEntity entity = Entity(1u, 0x73800001u);
|
WorldEntity entity = Entity(1u, 0x73800001u);
|
||||||
state.PlaceLiveEntityProjection(sourceLandblock, entity);
|
state.PlaceLiveEntityProjection(sourceLandblock, entity);
|
||||||
var edges = new List<(uint Guid, bool Visible)>();
|
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (guid, visible) => edges.Add((guid, visible));
|
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
||||||
|
edges.Add((localEntityId, visible));
|
||||||
|
|
||||||
state.RebucketLiveEntity(entity, targetLandblock);
|
state.RebucketLiveEntity(entity, targetLandblock);
|
||||||
|
|
||||||
Assert.Empty(edges);
|
Assert.Empty(edges);
|
||||||
Assert.True(state.IsLiveEntityVisible(entity.ServerGuid));
|
Assert.True(state.IsLiveEntityVisible(entity.Id));
|
||||||
Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source));
|
Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source));
|
||||||
Assert.Empty(source!.Entities);
|
Assert.Empty(source!.Entities);
|
||||||
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target));
|
||||||
|
|
@ -429,7 +431,7 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SameGuidOverlap_ExactRemovalPromotesSecondaryWithoutVisibilityPulse()
|
public void SameGuidOverlap_TracksExactProjectionVisibilityIndependently()
|
||||||
{
|
{
|
||||||
const uint landblock = 0x0101FFFFu;
|
const uint landblock = 0x0101FFFFu;
|
||||||
const uint guid = 0x73900001u;
|
const uint guid = 0x73900001u;
|
||||||
|
|
@ -442,20 +444,21 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
WorldEntity second = Entity(2u, guid);
|
WorldEntity second = Entity(2u, guid);
|
||||||
state.PlaceLiveEntityProjection(landblock, first);
|
state.PlaceLiveEntityProjection(landblock, first);
|
||||||
state.PlaceLiveEntityProjection(landblock, second);
|
state.PlaceLiveEntityProjection(landblock, second);
|
||||||
var edges = new List<(uint Guid, bool Visible)>();
|
var edges = new List<(uint LocalEntityId, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (edgeGuid, visible) =>
|
state.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
||||||
edges.Add((edgeGuid, visible));
|
edges.Add((localEntityId, visible));
|
||||||
|
|
||||||
state.RemoveLiveEntityProjection(first);
|
state.RemoveLiveEntityProjection(first);
|
||||||
|
|
||||||
Assert.Empty(edges);
|
Assert.Equal([(first.Id, false)], edges);
|
||||||
Assert.True(state.IsLiveEntityVisible(guid));
|
Assert.False(state.IsLiveEntityVisible(first.Id));
|
||||||
|
Assert.True(state.IsLiveEntityVisible(second.Id));
|
||||||
Assert.Same(second, Assert.Single(state.Entities));
|
Assert.Same(second, Assert.Single(state.Entities));
|
||||||
|
|
||||||
state.RemoveLiveEntityProjection(guid);
|
state.RemoveLiveEntityProjection(guid);
|
||||||
|
|
||||||
Assert.Equal([(guid, false)], edges);
|
Assert.Equal([(first.Id, false), (second.Id, false)], edges);
|
||||||
Assert.False(state.IsLiveEntityVisible(guid));
|
Assert.False(state.IsLiveEntityVisible(second.Id));
|
||||||
Assert.Empty(state.Entities);
|
Assert.Empty(state.Entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,7 +491,7 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
var state = new GpuWorldState();
|
var state = new GpuWorldState();
|
||||||
state.PlaceLiveEntityProjection(landblock, first);
|
state.PlaceLiveEntityProjection(landblock, first);
|
||||||
state.PlaceLiveEntityProjection(landblock, second);
|
state.PlaceLiveEntityProjection(landblock, second);
|
||||||
var observed = new List<(uint Guid, bool Visible)>();
|
var observed = new List<(uint LocalEntityId, bool Visible)>();
|
||||||
state.LiveProjectionVisibilityChanged += (_, _) =>
|
state.LiveProjectionVisibilityChanged += (_, _) =>
|
||||||
throw new InvalidOperationException("fixture observer failure");
|
throw new InvalidOperationException("fixture observer failure");
|
||||||
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
||||||
|
|
@ -502,10 +505,10 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
|
|
||||||
Assert.Equal(2, error.InnerExceptions.Count);
|
Assert.Equal(2, error.InnerExceptions.Count);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[(first.ServerGuid, true), (second.ServerGuid, true)],
|
[(first.Id, true), (second.Id, true)],
|
||||||
observed.OrderBy(edge => edge.Guid).ToArray());
|
observed.OrderBy(edge => edge.LocalEntityId).ToArray());
|
||||||
Assert.True(state.IsLiveEntityVisible(first.ServerGuid));
|
Assert.True(state.IsLiveEntityVisible(first.Id));
|
||||||
Assert.True(state.IsLiveEntityVisible(second.ServerGuid));
|
Assert.True(state.IsLiveEntityVisible(second.Id));
|
||||||
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
||||||
Assert.Equal(2, state.Entities.Count);
|
Assert.Equal(2, state.Entities.Count);
|
||||||
}
|
}
|
||||||
|
|
@ -574,14 +577,14 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
cell,
|
cell,
|
||||||
id => Entity(id, guid));
|
id => Entity(id, guid));
|
||||||
|
|
||||||
Assert.False(state.IsLiveEntityVisible(guid));
|
|
||||||
Assert.True(state.IsLiveEntityProjectionResident(guid));
|
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||||
|
Assert.False(state.IsLiveEntityVisible(record.WorldEntity!.Id));
|
||||||
|
Assert.True(state.IsLiveEntityProjectionResident(record.WorldEntity.Id));
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
Assert.Equal([true], edges);
|
Assert.Equal([true], edges);
|
||||||
|
|
||||||
Assert.True(availability.End(7));
|
Assert.True(availability.End(7));
|
||||||
Assert.True(state.IsLiveEntityVisible(guid));
|
Assert.True(state.IsLiveEntityVisible(record.WorldEntity.Id));
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
world.SetLandblockAabb(LandblockId, Vector3.Zero, Vector3.One);
|
world.SetLandblockAabb(LandblockId, Vector3.Zero, Vector3.One);
|
||||||
var nearby = new List<KeyValuePair<uint, WorldEntity>>();
|
var nearby = new List<KeyValuePair<uint, WorldEntity>>();
|
||||||
|
|
||||||
Assert.True(world.IsLiveEntityVisible(ServerGuid));
|
Assert.True(world.IsLiveEntityVisible(entity.Id));
|
||||||
Assert.Single(world.LandblockEntries);
|
Assert.Single(world.LandblockEntries);
|
||||||
Assert.Single(world.LandblockBounds);
|
Assert.Single(world.LandblockBounds);
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
|
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
|
||||||
|
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
Assert.False(world.IsLiveEntityVisible(ServerGuid));
|
Assert.False(world.IsLiveEntityVisible(entity.Id));
|
||||||
Assert.Empty(world.LandblockEntries);
|
Assert.Empty(world.LandblockEntries);
|
||||||
Assert.Empty(world.LandblockBounds);
|
Assert.Empty(world.LandblockBounds);
|
||||||
Assert.Empty(nearby);
|
Assert.Empty(nearby);
|
||||||
|
|
@ -44,7 +44,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
Assert.False(availability.End(16));
|
Assert.False(availability.End(16));
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
Assert.True(availability.End(17));
|
Assert.True(availability.End(17));
|
||||||
Assert.True(world.IsLiveEntityVisible(ServerGuid));
|
Assert.True(world.IsLiveEntityVisible(entity.Id));
|
||||||
Assert.Same(entity, Assert.Single(world.LandblockEntries).Entities.Single());
|
Assert.Same(entity, Assert.Single(world.LandblockEntries).Entities.Single());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +57,8 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
LandblockId,
|
LandblockId,
|
||||||
new LandBlock(),
|
new LandBlock(),
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
world.PlaceLiveEntityProjection(LandblockId, Entity());
|
WorldEntity entity = Entity();
|
||||||
|
world.PlaceLiveEntityProjection(LandblockId, entity);
|
||||||
var selection = new SelectionState();
|
var selection = new SelectionState();
|
||||||
selection.Select(ServerGuid, SelectionChangeSource.World);
|
selection.Select(ServerGuid, SelectionChangeSource.World);
|
||||||
var audio = new RecordingAudioQuiescence();
|
var audio = new RecordingAudioQuiescence();
|
||||||
|
|
@ -65,6 +66,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
availability,
|
availability,
|
||||||
selection,
|
selection,
|
||||||
world,
|
world,
|
||||||
|
guid => guid == ServerGuid ? entity.Id : null,
|
||||||
audio);
|
audio);
|
||||||
|
|
||||||
quiescence.Begin(1);
|
quiescence.Begin(1);
|
||||||
|
|
@ -95,6 +97,7 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
availability,
|
availability,
|
||||||
selection,
|
selection,
|
||||||
world,
|
world,
|
||||||
|
_ => null,
|
||||||
audio: null);
|
audio: null);
|
||||||
|
|
||||||
quiescence.Begin(3);
|
quiescence.Begin(3);
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
availability,
|
availability,
|
||||||
new SelectionState(),
|
new SelectionState(),
|
||||||
world,
|
world,
|
||||||
|
_ => null,
|
||||||
audio);
|
audio);
|
||||||
var coordinator = new WorldRevealCoordinator(
|
var coordinator = new WorldRevealCoordinator(
|
||||||
isRenderNeighborhoodReady: (_, _) => true,
|
isRenderNeighborhoodReady: (_, _) => true,
|
||||||
|
|
@ -165,6 +166,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
availability,
|
availability,
|
||||||
new SelectionState(),
|
new SelectionState(),
|
||||||
world,
|
world,
|
||||||
|
_ => null,
|
||||||
audio);
|
audio);
|
||||||
var scheduler = new RecordingDestinationScheduler();
|
var scheduler = new RecordingDestinationScheduler();
|
||||||
var coordinator = new WorldRevealCoordinator(
|
var coordinator = new WorldRevealCoordinator(
|
||||||
|
|
@ -215,6 +217,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
availability,
|
availability,
|
||||||
new SelectionState(),
|
new SelectionState(),
|
||||||
world,
|
world,
|
||||||
|
_ => null,
|
||||||
audio: null);
|
audio: null);
|
||||||
var scheduler = new RecordingDestinationScheduler();
|
var scheduler = new RecordingDestinationScheduler();
|
||||||
var coordinator = new WorldRevealCoordinator(
|
var coordinator = new WorldRevealCoordinator(
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public sealed class RadarSnapshotProviderTests
|
||||||
|
|
||||||
uint? selected = monster;
|
uint? selected = monster;
|
||||||
var provider = new RadarSnapshotProvider(
|
var provider = new RadarSnapshotProvider(
|
||||||
objects, () => entities, () => spawns,
|
objects, new RadarEntities(() => entities), () => spawns,
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
playerYawRadians: () => MathF.PI / 2f, // retail heading 0 degrees
|
playerYawRadians: () => MathF.PI / 2f, // retail heading 0 degrees
|
||||||
playerCellId: () => 0xA9B40001u,
|
playerCellId: () => 0xA9B40001u,
|
||||||
|
|
@ -83,7 +83,7 @@ public sealed class RadarSnapshotProviderTests
|
||||||
[hidden] = Spawn(hidden),
|
[hidden] = Spawn(hidden),
|
||||||
};
|
};
|
||||||
var provider = new RadarSnapshotProvider(
|
var provider = new RadarSnapshotProvider(
|
||||||
objects, () => entities, () => spawns,
|
objects, new RadarEntities(() => entities), () => spawns,
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
playerYawRadians: () => 0f,
|
playerYawRadians: () => 0f,
|
||||||
playerCellId: () => 0xA9B40100u, // EnvCell: no landscape coordinate
|
playerCellId: () => 0xA9B40100u, // EnvCell: no landscape coordinate
|
||||||
|
|
@ -122,15 +122,16 @@ public sealed class RadarSnapshotProviderTests
|
||||||
};
|
};
|
||||||
var provider = new RadarSnapshotProvider(
|
var provider = new RadarSnapshotProvider(
|
||||||
objects,
|
objects,
|
||||||
() => interactionVisible,
|
new RadarEntities(
|
||||||
|
() => interactionVisible,
|
||||||
|
() => canonical),
|
||||||
() => spawns,
|
() => spawns,
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
playerYawRadians: () => 0f,
|
playerYawRadians: () => 0f,
|
||||||
playerCellId: () => 0xA9B40001u,
|
playerCellId: () => 0xA9B40001u,
|
||||||
selectedGuid: () => hiddenTarget,
|
selectedGuid: () => hiddenTarget,
|
||||||
coordinatesOnRadar: () => true,
|
coordinatesOnRadar: () => true,
|
||||||
uiLocked: () => false,
|
uiLocked: () => false);
|
||||||
playerEntities: () => canonical);
|
|
||||||
|
|
||||||
var snapshot = provider.BuildSnapshot();
|
var snapshot = provider.BuildSnapshot();
|
||||||
|
|
||||||
|
|
@ -158,15 +159,14 @@ public sealed class RadarSnapshotProviderTests
|
||||||
new Dictionary<uint, WorldSession.EntitySpawn>();
|
new Dictionary<uint, WorldSession.EntitySpawn>();
|
||||||
var provider = new RadarSnapshotProvider(
|
var provider = new RadarSnapshotProvider(
|
||||||
objects,
|
objects,
|
||||||
() => visible,
|
new RadarEntities(() => visible, () => canonical),
|
||||||
() => spawns,
|
() => spawns,
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
playerYawRadians: () => MathF.PI / 2f,
|
playerYawRadians: () => MathF.PI / 2f,
|
||||||
playerCellId: () => 0xA9B40001u,
|
playerCellId: () => 0xA9B40001u,
|
||||||
selectedGuid: () => null,
|
selectedGuid: () => null,
|
||||||
coordinatesOnRadar: () => true,
|
coordinatesOnRadar: () => true,
|
||||||
uiLocked: () => false,
|
uiLocked: () => false);
|
||||||
playerEntities: () => canonical);
|
|
||||||
|
|
||||||
Assert.Null(provider.BuildSnapshot().CoordinatesText);
|
Assert.Null(provider.BuildSnapshot().CoordinatesText);
|
||||||
|
|
||||||
|
|
@ -215,7 +215,7 @@ public sealed class RadarSnapshotProviderTests
|
||||||
new KeyValuePair<uint, WorldEntity>(nearby, entities[nearby]));
|
new KeyValuePair<uint, WorldEntity>(nearby, entities[nearby]));
|
||||||
var provider = new RadarSnapshotProvider(
|
var provider = new RadarSnapshotProvider(
|
||||||
objects,
|
objects,
|
||||||
() => entities,
|
new RadarEntities(() => entities),
|
||||||
() => spawns,
|
() => spawns,
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
playerYawRadians: () => 0f,
|
playerYawRadians: () => 0f,
|
||||||
|
|
@ -252,7 +252,7 @@ public sealed class RadarSnapshotProviderTests
|
||||||
ILiveEntitySpatialQuery currentSpatialQuery = new RecordingSpatialQuery();
|
ILiveEntitySpatialQuery currentSpatialQuery = new RecordingSpatialQuery();
|
||||||
var provider = new RadarSnapshotProvider(
|
var provider = new RadarSnapshotProvider(
|
||||||
objects,
|
objects,
|
||||||
() => entities,
|
new RadarEntities(() => entities),
|
||||||
() => spawns,
|
() => spawns,
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
playerYawRadians: () => 0f,
|
playerYawRadians: () => 0f,
|
||||||
|
|
@ -287,6 +287,35 @@ public sealed class RadarSnapshotProviderTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RadarEntities(
|
||||||
|
Func<IReadOnlyDictionary<uint, WorldEntity>> visible,
|
||||||
|
Func<IReadOnlyDictionary<uint, WorldEntity>>? materialized = null)
|
||||||
|
: ILiveEntityRadarSource
|
||||||
|
{
|
||||||
|
private readonly Func<IReadOnlyDictionary<uint, WorldEntity>> _visible =
|
||||||
|
visible;
|
||||||
|
private readonly Func<IReadOnlyDictionary<uint, WorldEntity>> _materialized =
|
||||||
|
materialized ?? visible;
|
||||||
|
|
||||||
|
public bool TryGetMaterialized(
|
||||||
|
uint serverGuid,
|
||||||
|
out WorldEntity entity) =>
|
||||||
|
_materialized().TryGetValue(serverGuid, out entity!);
|
||||||
|
|
||||||
|
public bool TryGetVisible(
|
||||||
|
uint serverGuid,
|
||||||
|
out WorldEntity entity) =>
|
||||||
|
_visible().TryGetValue(serverGuid, out entity!);
|
||||||
|
|
||||||
|
public void CopyVisibleTo(
|
||||||
|
List<KeyValuePair<uint, WorldEntity>> destination)
|
||||||
|
{
|
||||||
|
destination.Clear();
|
||||||
|
foreach (KeyValuePair<uint, WorldEntity> pair in _visible())
|
||||||
|
destination.Add(pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static WorldEntity Entity(uint guid, Vector3 position, Quaternion rotation) => new()
|
private static WorldEntity Entity(uint guid, Vector3 position, Quaternion rotation) => new()
|
||||||
{
|
{
|
||||||
Id = guid,
|
Id = guid,
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
||||||
bridge);
|
bridge);
|
||||||
WorldSession.EntitySpawn spawn = CreateSpawn(guid);
|
WorldSession.EntitySpawn spawn = CreateSpawn(guid);
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(spawn).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(spawn);
|
||||||
var delete = new DeleteObject.Parsed(guid, InstanceSequence: 1);
|
var delete = new DeleteObject.Parsed(guid, InstanceSequence: 1);
|
||||||
|
|
||||||
Assert.Throws<AggregateException>(() =>
|
Assert.Throws<AggregateException>(() =>
|
||||||
|
|
@ -139,7 +139,8 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
|
||||||
var runtime = new LiveEntityRuntime(
|
var runtime = new LiveEntityRuntime(
|
||||||
new GpuWorldState(),
|
new GpuWorldState(),
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
return runtime.RegisterLiveEntity(CreateSpawn(0x70000001u)).Record!;
|
return runtime.RegisterAndMaterializeProjection(
|
||||||
|
CreateSpawn(0x70000001u));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WorldSession.EntitySpawn CreateSpawn(uint guid) =>
|
private static WorldSession.EntitySpawn CreateSpawn(uint guid) =>
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,18 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
foreach (FieldInfo field in helperType.GetFields(
|
foreach (FieldInfo field in helperType.GetFields(
|
||||||
BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic))
|
BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic))
|
||||||
{
|
{
|
||||||
Assert.False(typeof(IDictionary).IsAssignableFrom(field.FieldType),
|
bool isExactSynchronousProjectionContext =
|
||||||
$"{helperType.Name}.{field.Name} must resolve identity through LiveEntityRuntime.");
|
helperType == typeof(LiveEntityHydrationController)
|
||||||
|
&& field.Name == "_projectionOperations"
|
||||||
|
&& field.FieldType.IsGenericType
|
||||||
|
&& field.FieldType.GetGenericArguments()[0]
|
||||||
|
== typeof(AcDream.Runtime.Entities.RuntimeEntityRecord);
|
||||||
|
if (!isExactSynchronousProjectionContext)
|
||||||
|
{
|
||||||
|
Assert.False(
|
||||||
|
typeof(IDictionary).IsAssignableFrom(field.FieldType),
|
||||||
|
$"{helperType.Name}.{field.Name} must resolve identity through LiveEntityRuntime.");
|
||||||
|
}
|
||||||
string typeName = field.FieldType.FullName ?? field.FieldType.Name;
|
string typeName = field.FieldType.FullName ?? field.FieldType.Name;
|
||||||
Assert.DoesNotContain("Silk.NET", typeName, StringComparison.Ordinal);
|
Assert.DoesNotContain("Silk.NET", typeName, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("OpenGL", typeName, StringComparison.OrdinalIgnoreCase);
|
Assert.DoesNotContain("OpenGL", typeName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
|
||||||
|
|
@ -78,14 +78,16 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
|
|
||||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||||
|
|
||||||
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
|
RuntimeEntityRecord canonical = fixture.Canonical;
|
||||||
Assert.Null(record.WorldEntity);
|
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||||
|
Assert.Null(canonical.LocalEntityId);
|
||||||
Assert.Equal(0, fixture.Resources.RegisterCount);
|
Assert.Equal(0, fixture.Resources.RegisterCount);
|
||||||
Assert.Empty(fixture.Materializer.Calls);
|
Assert.Empty(fixture.Materializer.Calls);
|
||||||
|
|
||||||
fixture.Origin.IsKnownValue = true;
|
fixture.Origin.IsKnownValue = true;
|
||||||
fixture.Controller.OnLandblockLoaded(Cell);
|
fixture.Controller.OnLandblockLoaded(Cell);
|
||||||
|
|
||||||
|
LiveEntityRecord record = fixture.Record;
|
||||||
Assert.NotNull(record.WorldEntity);
|
Assert.NotNull(record.WorldEntity);
|
||||||
Assert.Single(fixture.Materializer.Calls);
|
Assert.Single(fixture.Materializer.Calls);
|
||||||
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[0].Purpose);
|
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[0].Purpose);
|
||||||
|
|
@ -130,11 +132,13 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
fixture.Materializer.SkipMaterializationCount = 1;
|
fixture.Materializer.SkipMaterializationCount = 1;
|
||||||
WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1);
|
WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1);
|
||||||
fixture.Controller.OnCreate(spawn);
|
fixture.Controller.OnCreate(spawn);
|
||||||
LiveEntityRecord record = fixture.Record;
|
RuntimeEntityRecord canonical = fixture.Canonical;
|
||||||
Assert.Null(record.WorldEntity);
|
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||||
|
Assert.Null(canonical.LocalEntityId);
|
||||||
|
|
||||||
Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||||
|
|
||||||
|
LiveEntityRecord record = fixture.Record;
|
||||||
Assert.NotNull(record.WorldEntity);
|
Assert.NotNull(record.WorldEntity);
|
||||||
Assert.Equal(2, fixture.Materializer.Calls.Count);
|
Assert.Equal(2, fixture.Materializer.Calls.Count);
|
||||||
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[1].Purpose);
|
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[1].Purpose);
|
||||||
|
|
@ -516,13 +520,15 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)));
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)));
|
||||||
|
|
||||||
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
|
RuntimeEntityRecord canonical = fixture.Canonical;
|
||||||
Assert.Null(record.WorldEntity);
|
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||||
|
Assert.Null(canonical.LocalEntityId);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
|
|
||||||
fixture.Controller.OnLandblockLoaded(Cell);
|
fixture.Controller.OnLandblockLoaded(Cell);
|
||||||
|
|
||||||
|
LiveEntityRecord record = fixture.Record;
|
||||||
Assert.NotNull(record.WorldEntity);
|
Assert.NotNull(record.WorldEntity);
|
||||||
Assert.Equal(2, resources.RegisterCount);
|
Assert.Equal(2, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
|
|
@ -644,7 +650,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
ClientObject retainedObject = fixture.Objects.Get(Guid)!;
|
ClientObject retainedObject = fixture.Objects.Get(Guid)!;
|
||||||
|
|
||||||
Assert.True(fixture.Controller.OnPrune(
|
Assert.True(fixture.Controller.OnPrune(
|
||||||
new LiveEntityPruneCandidate(Guid, Generation: 1)));
|
new LiveEntityPruneCandidate(
|
||||||
|
fixture.Record.ProjectionKey!.Value,
|
||||||
|
Guid)));
|
||||||
|
|
||||||
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||||
Assert.Same(retainedObject, fixture.Objects.Get(Guid));
|
Assert.Same(retainedObject, fixture.Objects.Get(Guid));
|
||||||
|
|
@ -658,7 +666,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
using var fixture = new Fixture(originKnown: true);
|
using var fixture = new Fixture(originKnown: true);
|
||||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||||
Assert.True(fixture.Controller.OnPrune(
|
Assert.True(fixture.Controller.OnPrune(
|
||||||
new LiveEntityPruneCandidate(Guid, Generation: 1)));
|
new LiveEntityPruneCandidate(
|
||||||
|
fixture.Record.ProjectionKey!.Value,
|
||||||
|
Guid)));
|
||||||
|
|
||||||
fixture.Controller.OnLandblockLoaded(Cell);
|
fixture.Controller.OnLandblockLoaded(Cell);
|
||||||
|
|
||||||
|
|
@ -677,7 +687,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
using var fixture = new Fixture(originKnown: true);
|
using var fixture = new Fixture(originKnown: true);
|
||||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||||
Assert.True(fixture.Controller.OnPrune(
|
Assert.True(fixture.Controller.OnPrune(
|
||||||
new LiveEntityPruneCandidate(Guid, Generation: 1)));
|
new LiveEntityPruneCandidate(
|
||||||
|
fixture.Record.ProjectionKey!.Value,
|
||||||
|
Guid)));
|
||||||
|
|
||||||
Assert.True(fixture.Controller.OnDelete(
|
Assert.True(fixture.Controller.OnDelete(
|
||||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||||
|
|
@ -694,7 +706,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
using var fixture = new Fixture(originKnown: true);
|
using var fixture = new Fixture(originKnown: true);
|
||||||
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1));
|
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1));
|
||||||
Assert.True(fixture.Controller.OnPrune(
|
Assert.True(fixture.Controller.OnPrune(
|
||||||
new LiveEntityPruneCandidate(Guid, Generation: 2)));
|
new LiveEntityPruneCandidate(
|
||||||
|
fixture.Record.ProjectionKey!.Value,
|
||||||
|
Guid)));
|
||||||
|
|
||||||
fixture.Controller.OnCreate(Spawn(
|
fixture.Controller.OnCreate(Spawn(
|
||||||
Generation: 1,
|
Generation: 1,
|
||||||
|
|
@ -723,7 +737,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
PositionSequence: 5);
|
PositionSequence: 5);
|
||||||
fixture.Controller.OnCreate(accepted);
|
fixture.Controller.OnCreate(accepted);
|
||||||
Assert.True(fixture.Controller.OnPrune(
|
Assert.True(fixture.Controller.OnPrune(
|
||||||
new LiveEntityPruneCandidate(Guid, Generation: 1)));
|
new LiveEntityPruneCandidate(
|
||||||
|
fixture.Record.ProjectionKey!.Value,
|
||||||
|
Guid)));
|
||||||
|
|
||||||
CreateObject.ServerPosition stalePosition =
|
CreateObject.ServerPosition stalePosition =
|
||||||
accepted.Position!.Value with
|
accepted.Position!.Value with
|
||||||
|
|
@ -760,7 +776,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Value = 123,
|
Value = 123,
|
||||||
});
|
});
|
||||||
Assert.True(fixture.Controller.OnPrune(
|
Assert.True(fixture.Controller.OnPrune(
|
||||||
new LiveEntityPruneCandidate(Guid, Generation: 1)));
|
new LiveEntityPruneCandidate(
|
||||||
|
fixture.Record.ProjectionKey!.Value,
|
||||||
|
Guid)));
|
||||||
|
|
||||||
fixture.Controller.OnCreate(
|
fixture.Controller.OnCreate(
|
||||||
Spawn(Generation: 2, PositionSequence: 1) with
|
Spawn(Generation: 2, PositionSequence: 1) with
|
||||||
|
|
@ -831,10 +849,11 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Assert.True(fixture.Controller.OnDelete(
|
Assert.True(fixture.Controller.OnDelete(
|
||||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||||
|
|
||||||
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord replacement));
|
RuntimeEntityRecord replacement = fixture.Canonical;
|
||||||
Assert.NotSame(old, replacement);
|
|
||||||
Assert.Equal((ushort)2, replacement.Generation);
|
Assert.Equal((ushort)2, replacement.Generation);
|
||||||
Assert.Equal("replacement", replacement.Snapshot.Name);
|
Assert.Equal("replacement", replacement.Snapshot.Name);
|
||||||
|
Assert.Null(replacement.LocalEntityId);
|
||||||
|
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||||
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
|
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1348,10 +1367,9 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
|
|
||||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||||
|
|
||||||
LiveEntityRecord record = fixture.Record;
|
RuntimeEntityRecord canonical = fixture.Canonical;
|
||||||
Assert.False(record.CreateProjectionSynchronizationPending);
|
Assert.Null(canonical.LocalEntityId);
|
||||||
Assert.False(record.InitialHydrationCompleted);
|
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||||
Assert.Null(record.WorldEntity);
|
|
||||||
Assert.Equal(0, fixture.Resources.RegisterCount);
|
Assert.Equal(0, fixture.Resources.RegisterCount);
|
||||||
|
|
||||||
fixture.Materializer.BeforeMaterialize = null;
|
fixture.Materializer.BeforeMaterialize = null;
|
||||||
|
|
@ -1371,6 +1389,7 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
};
|
};
|
||||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 3));
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 3));
|
||||||
|
|
||||||
|
LiveEntityRecord record = fixture.Record;
|
||||||
Assert.True(record.InitialHydrationCompleted);
|
Assert.True(record.InitialHydrationCompleted);
|
||||||
Assert.NotNull(record.WorldEntity);
|
Assert.NotNull(record.WorldEntity);
|
||||||
Assert.Equal(1, fixture.Resources.RegisterCount);
|
Assert.Equal(1, fixture.Resources.RegisterCount);
|
||||||
|
|
@ -1481,12 +1500,14 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
};
|
};
|
||||||
fixture.Relationships.OnSpawnAction = _ =>
|
fixture.Relationships.OnSpawnAction = _ =>
|
||||||
{
|
{
|
||||||
LiveEntityRecord record = fixture.Record;
|
RuntimeEntityRecord canonical = fixture.Canonical;
|
||||||
if (record.WorldEntity is not null)
|
if (fixture.Runtime.TryGetRecord(
|
||||||
|
Guid,
|
||||||
|
out LiveEntityRecord _))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity(
|
WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity(
|
||||||
Guid,
|
canonical,
|
||||||
Cell,
|
Cell,
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
|
|
@ -1498,10 +1519,13 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
MeshRefs = [],
|
MeshRefs = [],
|
||||||
ParentCellId = Cell,
|
ParentCellId = Cell,
|
||||||
},
|
},
|
||||||
LiveEntityProjectionKind.Attached);
|
LiveEntityProjectionKind.Attached,
|
||||||
|
initializeProjection: null,
|
||||||
|
out LiveEntityRecord? record);
|
||||||
Assert.NotNull(attached);
|
Assert.NotNull(attached);
|
||||||
|
Assert.NotNull(record);
|
||||||
fixture.Controller.OnEntityReady(
|
fixture.Controller.OnEntityReady(
|
||||||
LiveEntityReadyCandidate.Capture(record));
|
LiveEntityReadyCandidate.Capture(record!));
|
||||||
};
|
};
|
||||||
|
|
||||||
fixture.Controller.OnCreate(CelllessSpawn(
|
fixture.Controller.OnCreate(CelllessSpawn(
|
||||||
|
|
@ -1757,7 +1781,8 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
record,
|
record,
|
||||||
staleVersion,
|
staleVersion,
|
||||||
accepted));
|
accepted));
|
||||||
Assert.Empty(fixture.Runtime.MaterializedWorldEntities);
|
Assert.Single(fixture.Runtime.MaterializedRecords);
|
||||||
|
Assert.Empty(fixture.Runtime.VisibleRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -1890,6 +1915,17 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RuntimeEntityRecord Canonical
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Assert.True(Runtime.TryGetCanonical(
|
||||||
|
Guid,
|
||||||
|
out RuntimeEntityRecord canonical));
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -1991,24 +2027,24 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
public readonly List<(LiveProjectionPurpose Purpose, ushort Generation)> Calls = [];
|
public readonly List<(LiveProjectionPurpose Purpose, ushort Generation)> Calls = [];
|
||||||
public readonly List<ushort> PositionSequences = [];
|
public readonly List<ushort> PositionSequences = [];
|
||||||
public ulong? InstalledCreateIntegrationVersion { get; private set; }
|
public ulong? InstalledCreateIntegrationVersion { get; private set; }
|
||||||
public Action<LiveEntityRecord, WorldSession.EntitySpawn>? BeforeMaterialize { get; set; }
|
public Action<RuntimeEntityRecord, WorldSession.EntitySpawn>? BeforeMaterialize { get; set; }
|
||||||
public Action<LiveEntityRecord, WorldSession.EntitySpawn>? AfterMaterialize { get; set; }
|
public Action<RuntimeEntityRecord, WorldSession.EntitySpawn>? AfterMaterialize { get; set; }
|
||||||
public LiveProjectionPurpose? ThrowAfterMaterializePurposeOnce { get; set; }
|
public LiveProjectionPurpose? ThrowAfterMaterializePurposeOnce { get; set; }
|
||||||
public int SkipMaterializationCount { get; set; }
|
public int SkipMaterializationCount { get; set; }
|
||||||
|
|
||||||
public bool TryMaterialize(
|
public bool TryMaterialize(
|
||||||
LiveEntityRecord expectedRecord,
|
RuntimeEntityRecord expectedCanonical,
|
||||||
WorldSession.EntitySpawn canonicalSpawn,
|
WorldSession.EntitySpawn canonicalSpawn,
|
||||||
LiveProjectionPurpose purpose,
|
LiveProjectionPurpose purpose,
|
||||||
ulong expectedCreateIntegrationVersion,
|
ulong expectedCreateIntegrationVersion,
|
||||||
AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null)
|
AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null)
|
||||||
{
|
{
|
||||||
Calls.Add((purpose, expectedRecord.Generation));
|
Calls.Add((purpose, expectedCanonical.Generation));
|
||||||
PositionSequences.Add(canonicalSpawn.PositionSequence);
|
PositionSequences.Add(canonicalSpawn.PositionSequence);
|
||||||
operations.Add($"materialize:{purpose}:{expectedRecord.Generation}");
|
operations.Add($"materialize:{purpose}:{expectedCanonical.Generation}");
|
||||||
BeforeMaterialize?.Invoke(expectedRecord, canonicalSpawn);
|
BeforeMaterialize?.Invoke(expectedCanonical, canonicalSpawn);
|
||||||
if (!runtime.IsCurrentCreateIntegration(
|
if (!runtime.IsCurrentCreateIntegration(
|
||||||
expectedRecord,
|
expectedCanonical,
|
||||||
expectedCreateIntegrationVersion))
|
expectedCreateIntegrationVersion))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2030,7 +2066,7 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldEntity? entity = runtime.MaterializeLiveEntity(
|
WorldEntity? entity = runtime.MaterializeLiveEntity(
|
||||||
canonicalSpawn.Guid,
|
expectedCanonical,
|
||||||
position.LandblockId,
|
position.LandblockId,
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
|
|
@ -2041,15 +2077,19 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = [],
|
MeshRefs = [],
|
||||||
ParentCellId = position.LandblockId,
|
ParentCellId = position.LandblockId,
|
||||||
});
|
},
|
||||||
|
LiveEntityProjectionKind.World,
|
||||||
|
initializeProjection: null,
|
||||||
|
out LiveEntityRecord? expectedRecord);
|
||||||
if (ThrowAfterMaterializePurposeOnce == purpose)
|
if (ThrowAfterMaterializePurposeOnce == purpose)
|
||||||
{
|
{
|
||||||
ThrowAfterMaterializePurposeOnce = null;
|
ThrowAfterMaterializePurposeOnce = null;
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"fixture supersession projection failure");
|
"fixture supersession projection failure");
|
||||||
}
|
}
|
||||||
AfterMaterialize?.Invoke(expectedRecord, canonicalSpawn);
|
AfterMaterialize?.Invoke(expectedCanonical, canonicalSpawn);
|
||||||
return entity is not null
|
return entity is not null
|
||||||
|
&& expectedRecord is not null
|
||||||
&& runtime.IsCurrentRecord(expectedRecord);
|
&& runtime.IsCurrentRecord(expectedRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2137,7 +2177,7 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnProjectionRemoved(uint localEntityId)
|
public void OnProjectionRemoved(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
if (record.WorldEntity is { } entity)
|
if (record.WorldEntity is { } entity)
|
||||||
{
|
{
|
||||||
cleanups.Add(() => Engine.ShadowObjects.Deregister(entity.Id));
|
cleanups.Add(() => Engine.ShadowObjects.Deregister(entity.Id));
|
||||||
cleanups.Add(() => _liveLights?.Forget(entity.Id));
|
cleanups.Add(() => _liveLights?.Forget(record));
|
||||||
}
|
}
|
||||||
LiveEntityTeardown.Run(cleanups);
|
LiveEntityTeardown.Run(cleanups);
|
||||||
});
|
});
|
||||||
|
|
@ -402,14 +402,8 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
8f + (ordinal / 12) * 6f,
|
8f + (ordinal / 12) * 6f,
|
||||||
10f);
|
10f);
|
||||||
WorldSession.EntitySpawn spawn = MissileSpawn(guid, generation, position);
|
WorldSession.EntitySpawn spawn = MissileSpawn(guid, generation, position);
|
||||||
LiveEntityRegistrationResult registration = Runtime.RegisterLiveEntity(spawn);
|
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(
|
||||||
LiveEntityRecord record = Assert.IsType<LiveEntityRecord>(registration.Record);
|
spawn,
|
||||||
Runtime.SetEffectProfile(
|
|
||||||
guid,
|
|
||||||
EntityEffectProfile.CreateLive(new Setup(), spawn.Physics!.Value));
|
|
||||||
WorldEntity entity = Assert.IsType<WorldEntity>(Runtime.MaterializeLiveEntity(
|
|
||||||
guid,
|
|
||||||
CellA,
|
|
||||||
localId => new WorldEntity
|
localId => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = localId,
|
Id = localId,
|
||||||
|
|
@ -419,7 +413,12 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = CellA,
|
ParentCellId = CellA,
|
||||||
}));
|
},
|
||||||
|
initializeProjection: exact =>
|
||||||
|
exact.EffectProfile = EntityEffectProfile.CreateLive(
|
||||||
|
new Setup(),
|
||||||
|
spawn.Physics!.Value));
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
Assert.True(Effects.OnLiveEntityReady(guid));
|
Assert.True(Effects.OnLiveEntityReady(guid));
|
||||||
Assert.True(Projectiles.TryBind(record, Setup, 0.0, 1, 1));
|
Assert.True(Projectiles.TryBind(record, Setup, 0.0, 1, 1));
|
||||||
|
|
||||||
|
|
@ -520,13 +519,8 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
Router.Register(Effects);
|
Router.Register(Effects);
|
||||||
|
|
||||||
WorldSession.EntitySpawn spawn = RecallSpawn(Guid, CellOne);
|
WorldSession.EntitySpawn spawn = RecallSpawn(Guid, CellOne);
|
||||||
Runtime.RegisterLiveEntity(spawn);
|
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(
|
||||||
Runtime.SetEffectProfile(
|
spawn,
|
||||||
Guid,
|
|
||||||
EntityEffectProfile.CreateLive(new Setup(), spawn.Physics!.Value));
|
|
||||||
Entity = Runtime.MaterializeLiveEntity(
|
|
||||||
Guid,
|
|
||||||
CellOne,
|
|
||||||
localId => new WorldEntity
|
localId => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = localId,
|
Id = localId,
|
||||||
|
|
@ -536,7 +530,12 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = CellOne,
|
ParentCellId = CellOne,
|
||||||
})!;
|
},
|
||||||
|
initializeProjection: exact =>
|
||||||
|
exact.EffectProfile = EntityEffectProfile.CreateLive(
|
||||||
|
new Setup(),
|
||||||
|
spawn.Physics!.Value));
|
||||||
|
Entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
Assert.True(Effects.OnLiveEntityReady(Guid));
|
Assert.True(Effects.OnLiveEntityReady(Guid));
|
||||||
|
|
||||||
_remote.Body.SnapToCell(CellOne, Entity.Position, Entity.Position);
|
_remote.Body.SnapToCell(CellOne, Entity.Position, Entity.Position);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.World;
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
|
|
@ -14,7 +15,9 @@ public sealed class LiveEntityLivenessControllerTests
|
||||||
Assert.Empty(tracker.Tick(10.0, samples));
|
Assert.Empty(tracker.Tick(10.0, samples));
|
||||||
Assert.Empty(tracker.Tick(34.999, samples));
|
Assert.Empty(tracker.Tick(34.999, samples));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new LiveEntityPruneCandidate(0x7000_0001u, 4),
|
new LiveEntityPruneCandidate(
|
||||||
|
new RuntimeEntityKey(0x7000_0001u, 4),
|
||||||
|
0x7000_0001u),
|
||||||
Assert.Single(tracker.Tick(35.0, samples)));
|
Assert.Single(tracker.Tick(35.0, samples)));
|
||||||
Assert.Equal(0, tracker.DeadlineCount);
|
Assert.Equal(0, tracker.DeadlineCount);
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +50,7 @@ public sealed class LiveEntityLivenessControllerTests
|
||||||
Assert.Empty(tracker.Tick(24.0, [Sample(1, 2, visible: false)]));
|
Assert.Empty(tracker.Tick(24.0, [Sample(1, 2, visible: false)]));
|
||||||
Assert.Empty(tracker.Tick(25.0, [Sample(1, 2, visible: false)]));
|
Assert.Empty(tracker.Tick(25.0, [Sample(1, 2, visible: false)]));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new LiveEntityPruneCandidate(1, 2),
|
new LiveEntityPruneCandidate(new RuntimeEntityKey(1, 2), 1),
|
||||||
Assert.Single(tracker.Tick(49.0, [Sample(1, 2, visible: false)])));
|
Assert.Single(tracker.Tick(49.0, [Sample(1, 2, visible: false)])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +83,11 @@ public sealed class LiveEntityLivenessControllerTests
|
||||||
ushort generation,
|
ushort generation,
|
||||||
bool visible,
|
bool visible,
|
||||||
bool retained = false) =>
|
bool retained = false) =>
|
||||||
new(guid, generation, visible, retained);
|
new(
|
||||||
|
new RuntimeEntityKey(guid, generation),
|
||||||
|
guid,
|
||||||
|
visible,
|
||||||
|
retained);
|
||||||
|
|
||||||
private static CreateObject.ServerPosition Position(
|
private static CreateObject.ServerPosition Position(
|
||||||
uint cell,
|
uint cell,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Physics.Motion;
|
using AcDream.Core.Physics.Motion;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.World;
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
|
|
@ -16,7 +17,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000010u;
|
const uint guid = 0x70000010u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord first = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord first = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var firstMotion = new HostConsumerMotion();
|
var firstMotion = new HostConsumerMotion();
|
||||||
runtime.SetRemoteMotionRuntime(guid, firstMotion);
|
runtime.SetRemoteMotionRuntime(guid, firstMotion);
|
||||||
EntityPhysicsHost firstHost = Host(guid);
|
EntityPhysicsHost firstHost = Host(guid);
|
||||||
|
|
@ -26,7 +27,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
Assert.True(runtime.TryGetPhysicsHost(guid, out IPhysicsObjHost resolved));
|
Assert.True(runtime.TryGetPhysicsHost(guid, out IPhysicsObjHost resolved));
|
||||||
Assert.Same(firstHost, resolved);
|
Assert.Same(firstHost, resolved);
|
||||||
|
|
||||||
LiveEntityRecord second = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
|
LiveEntityRecord second = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
|
||||||
var secondMotion = new HostConsumerMotion();
|
var secondMotion = new HostConsumerMotion();
|
||||||
runtime.SetRemoteMotionRuntime(guid, secondMotion);
|
runtime.SetRemoteMotionRuntime(guid, secondMotion);
|
||||||
EntityPhysicsHost replacement = Host(guid);
|
EntityPhysicsHost replacement = Host(guid);
|
||||||
|
|
@ -42,7 +43,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000011u;
|
const uint guid = 0x70000011u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord first = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord first = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
EntityPhysicsHost original = Host(guid);
|
EntityPhysicsHost original = Host(guid);
|
||||||
runtime.InstallPhysicsHost(first, original);
|
runtime.InstallPhysicsHost(first, original);
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
runtime.InstallPhysicsHost(first, Host(guid)));
|
runtime.InstallPhysicsHost(first, Host(guid)));
|
||||||
|
|
||||||
LiveEntityRecord second = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
|
LiveEntityRecord second = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
runtime.InstallPhysicsHost(first, Host(guid)));
|
runtime.InstallPhysicsHost(first, Host(guid)));
|
||||||
Assert.Null(second.PhysicsHost);
|
Assert.Null(second.PhysicsHost);
|
||||||
|
|
@ -63,8 +64,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
const uint watcherGuid = 0x70000020u;
|
const uint watcherGuid = 0x70000020u;
|
||||||
const uint targetGuid = 0x70000021u;
|
const uint targetGuid = 0x70000021u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord watcherRecord = runtime.RegisterLiveEntity(Spawn(watcherGuid, 1)).Record!;
|
LiveEntityRecord watcherRecord = runtime.RegisterAndMaterializeProjection(Spawn(watcherGuid, 1));
|
||||||
LiveEntityRecord targetRecord = runtime.RegisterLiveEntity(Spawn(targetGuid, 1)).Record!;
|
LiveEntityRecord targetRecord = runtime.RegisterAndMaterializeProjection(Spawn(targetGuid, 1));
|
||||||
IPhysicsObjHost? Resolve(uint id) =>
|
IPhysicsObjHost? Resolve(uint id) =>
|
||||||
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
||||||
? host
|
? host
|
||||||
|
|
@ -129,7 +130,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000025u;
|
const uint guid = 0x70000025u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
EntityPhysicsHost original = Host(
|
EntityPhysicsHost original = Host(
|
||||||
guid,
|
guid,
|
||||||
position: new Vector3(1f, 0f, 0f));
|
position: new Vector3(1f, 0f, 0f));
|
||||||
|
|
@ -163,7 +164,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000022u;
|
const uint guid = 0x70000022u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord first = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord first = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
int firstUpdates = 0;
|
int firstUpdates = 0;
|
||||||
int firstInterrupts = 0;
|
int firstInterrupts = 0;
|
||||||
var firstHost = CallbackHost(
|
var firstHost = CallbackHost(
|
||||||
|
|
@ -172,7 +173,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
() => firstInterrupts++);
|
() => firstInterrupts++);
|
||||||
runtime.InstallPhysicsHost(first, firstHost);
|
runtime.InstallPhysicsHost(first, firstHost);
|
||||||
|
|
||||||
LiveEntityRecord replacement = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
|
LiveEntityRecord replacement = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
|
||||||
int replacementUpdates = 0;
|
int replacementUpdates = 0;
|
||||||
int replacementInterrupts = 0;
|
int replacementInterrupts = 0;
|
||||||
var replacementHost = CallbackHost(
|
var replacementHost = CallbackHost(
|
||||||
|
|
@ -195,7 +196,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000030u;
|
const uint guid = 0x70000030u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var motion = new RemoteMotion(new PhysicsBody());
|
var motion = new RemoteMotion(new PhysicsBody());
|
||||||
runtime.SetRemoteMotionRuntime(guid, motion);
|
runtime.SetRemoteMotionRuntime(guid, motion);
|
||||||
EntityPhysicsHost minimal = Host(guid);
|
EntityPhysicsHost minimal = Host(guid);
|
||||||
|
|
@ -221,7 +222,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000031u;
|
const uint guid = 0x70000031u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var failing = new ThrowingCellConsumerMotion();
|
var failing = new ThrowingCellConsumerMotion();
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
|
@ -240,11 +241,11 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000032u;
|
const uint guid = 0x70000032u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
runtime.RegisterLiveEntity(Spawn(guid, 1));
|
runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var stale = new RemoteMotion(new PhysicsBody());
|
var stale = new RemoteMotion(new PhysicsBody());
|
||||||
runtime.SetRemoteMotionRuntime(guid, stale);
|
runtime.SetRemoteMotionRuntime(guid, stale);
|
||||||
|
|
||||||
LiveEntityRecord replacement = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
|
LiveEntityRecord replacement = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
runtime.SetRemoteMotionRuntime(guid, stale));
|
runtime.SetRemoteMotionRuntime(guid, stale));
|
||||||
Assert.Null(replacement.RemoteMotionRuntime);
|
Assert.Null(replacement.RemoteMotionRuntime);
|
||||||
|
|
@ -261,7 +262,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000033u;
|
const uint guid = 0x70000033u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var firstBody = new PhysicsBody();
|
var firstBody = new PhysicsBody();
|
||||||
var secondBody = new PhysicsBody();
|
var secondBody = new PhysicsBody();
|
||||||
var alternating = new AlternatingBodyMotion(firstBody, secondBody);
|
var alternating = new AlternatingBodyMotion(firstBody, secondBody);
|
||||||
|
|
@ -282,7 +283,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000036u;
|
const uint guid = 0x70000036u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var canonical = new PhysicsBody();
|
var canonical = new PhysicsBody();
|
||||||
var unrelated = new PhysicsBody();
|
var unrelated = new PhysicsBody();
|
||||||
PhysicsStateFlags unrelatedInitialState = unrelated.State;
|
PhysicsStateFlags unrelatedInitialState = unrelated.State;
|
||||||
|
|
@ -302,17 +303,18 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000034u;
|
const uint guid = 0x70000034u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord retired = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord retired = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var motion = new CallbackCanonicalMotion(() =>
|
var motion = new CallbackCanonicalMotion(() =>
|
||||||
runtime.RegisterLiveEntity(Spawn(guid, 2)));
|
runtime.RegisterLiveEntity(Spawn(guid, 2)));
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
runtime.SetRemoteMotionRuntime(guid, motion));
|
runtime.SetRemoteMotionRuntime(guid, motion));
|
||||||
|
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement));
|
Assert.True(runtime.TryGetCanonical(guid, out var replacement));
|
||||||
Assert.Equal((ushort)2, replacement.Generation);
|
Assert.Equal((ushort)2, replacement.Incarnation);
|
||||||
Assert.Null(replacement.RemoteMotionRuntime);
|
Assert.Null(replacement.LocalEntityId);
|
||||||
Assert.Null(replacement.PhysicsBody);
|
Assert.Null(replacement.PhysicsBody);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
Assert.Null(retired.RemoteMotionRuntime);
|
Assert.Null(retired.RemoteMotionRuntime);
|
||||||
Assert.Null(retired.PhysicsBody);
|
Assert.Null(retired.PhysicsBody);
|
||||||
Assert.False(runtime.TryGetRemoteMotionRuntime(guid, out _));
|
Assert.False(runtime.TryGetRemoteMotionRuntime(guid, out _));
|
||||||
|
|
@ -323,7 +325,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000035u;
|
const uint guid = 0x70000035u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord retired = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord retired = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
var motion = new CallbackCanonicalMotion(runtime.Clear);
|
var motion = new CallbackCanonicalMotion(runtime.Clear);
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
|
@ -343,8 +345,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
const uint rightGuid = 0x70000041u;
|
const uint rightGuid = 0x70000041u;
|
||||||
LiveEntityRuntime runtime = null!;
|
LiveEntityRuntime runtime = null!;
|
||||||
runtime = Runtime(record => CleanPhysicsHost(record));
|
runtime = Runtime(record => CleanPhysicsHost(record));
|
||||||
LiveEntityRecord leftRecord = runtime.RegisterLiveEntity(Spawn(leftGuid, 1)).Record!;
|
LiveEntityRecord leftRecord = runtime.RegisterAndMaterializeProjection(Spawn(leftGuid, 1));
|
||||||
LiveEntityRecord rightRecord = runtime.RegisterLiveEntity(Spawn(rightGuid, 1)).Record!;
|
LiveEntityRecord rightRecord = runtime.RegisterAndMaterializeProjection(Spawn(rightGuid, 1));
|
||||||
IPhysicsObjHost? Resolve(uint id) =>
|
IPhysicsObjHost? Resolve(uint id) =>
|
||||||
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
||||||
? host
|
? host
|
||||||
|
|
@ -374,8 +376,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
const uint rightGuid = 0x70000043u;
|
const uint rightGuid = 0x70000043u;
|
||||||
LiveEntityRuntime runtime = null!;
|
LiveEntityRuntime runtime = null!;
|
||||||
runtime = Runtime(record => CleanPhysicsHost(record));
|
runtime = Runtime(record => CleanPhysicsHost(record));
|
||||||
LiveEntityRecord leftRecord = runtime.RegisterLiveEntity(Spawn(leftGuid, 1)).Record!;
|
LiveEntityRecord leftRecord = runtime.RegisterAndMaterializeProjection(Spawn(leftGuid, 1));
|
||||||
LiveEntityRecord rightRecord = runtime.RegisterLiveEntity(Spawn(rightGuid, 1)).Record!;
|
LiveEntityRecord rightRecord = runtime.RegisterAndMaterializeProjection(Spawn(rightGuid, 1));
|
||||||
IPhysicsObjHost? Resolve(uint id) =>
|
IPhysicsObjHost? Resolve(uint id) =>
|
||||||
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
||||||
? host
|
? host
|
||||||
|
|
@ -410,21 +412,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
if (failOnce)
|
if (failOnce)
|
||||||
{
|
{
|
||||||
failOnce = false;
|
failOnce = false;
|
||||||
replacementRecord = runtime.RegisterLiveEntity(Spawn(guid, 2)).Record!;
|
runtime.RegisterLiveEntity(Spawn(guid, 2));
|
||||||
IPhysicsObjHost? Resolve(uint id) =>
|
|
||||||
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
|
||||||
? host
|
|
||||||
: null;
|
|
||||||
replacementHost = Host(guid, Resolve);
|
|
||||||
runtime.InstallPhysicsHost(replacementRecord, replacementHost);
|
|
||||||
replacementHost.PositionManager.StickTo(targetGuid, 0.5f, 1f);
|
|
||||||
throw new InvalidOperationException("fixture failure before old host cleanup");
|
throw new InvalidOperationException("fixture failure before old host cleanup");
|
||||||
}
|
}
|
||||||
|
|
||||||
CleanPhysicsHost(record);
|
CleanPhysicsHost(record);
|
||||||
});
|
});
|
||||||
LiveEntityRecord oldRecord = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord oldRecord = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
LiveEntityRecord targetRecord = runtime.RegisterLiveEntity(Spawn(targetGuid, 1)).Record!;
|
LiveEntityRecord targetRecord = runtime.RegisterAndMaterializeProjection(Spawn(targetGuid, 1));
|
||||||
IPhysicsObjHost? InitialResolve(uint id) =>
|
IPhysicsObjHost? InitialResolve(uint id) =>
|
||||||
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
||||||
? host
|
? host
|
||||||
|
|
@ -438,6 +433,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
Assert.Throws<AggregateException>(() => runtime.UnregisterLiveEntity(
|
Assert.Throws<AggregateException>(() => runtime.UnregisterLiveEntity(
|
||||||
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
||||||
isLocalPlayer: false));
|
isLocalPlayer: false));
|
||||||
|
replacementRecord = runtime.RegisterAndMaterializeProjection(Spawn(guid, 2));
|
||||||
|
IPhysicsObjHost? ResolveReplacement(uint id) =>
|
||||||
|
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
||||||
|
? host
|
||||||
|
: null;
|
||||||
|
replacementHost = Host(guid, ResolveReplacement);
|
||||||
|
runtime.InstallPhysicsHost(replacementRecord, replacementHost);
|
||||||
|
replacementHost.PositionManager.StickTo(targetGuid, 0.5f, 1f);
|
||||||
Assert.Same(oldHost, oldRecord.PhysicsHost);
|
Assert.Same(oldHost, oldRecord.PhysicsHost);
|
||||||
Assert.Same(replacementHost, replacementRecord!.PhysicsHost);
|
Assert.Same(replacementHost, replacementRecord!.PhysicsHost);
|
||||||
Assert.Equal(targetGuid, replacementHost!.PositionManager.GetStickyObjectId());
|
Assert.Equal(targetGuid, replacementHost!.PositionManager.GetStickyObjectId());
|
||||||
|
|
@ -466,8 +469,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
throw new InvalidOperationException("fixture post-exit failure");
|
throw new InvalidOperationException("fixture post-exit failure");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
LiveEntityRecord retiredRecord = runtime.RegisterLiveEntity(Spawn(retiredGuid, 1)).Record!;
|
LiveEntityRecord retiredRecord = runtime.RegisterAndMaterializeProjection(Spawn(retiredGuid, 1));
|
||||||
LiveEntityRecord watcherRecord = runtime.RegisterLiveEntity(Spawn(watcherGuid, 1)).Record!;
|
LiveEntityRecord watcherRecord = runtime.RegisterAndMaterializeProjection(Spawn(watcherGuid, 1));
|
||||||
IPhysicsObjHost? Resolve(uint id) =>
|
IPhysicsObjHost? Resolve(uint id) =>
|
||||||
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host)
|
||||||
? host
|
? host
|
||||||
|
|
@ -493,7 +496,7 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x70000048u;
|
const uint guid = 0x70000048u;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
record.FullCellId = 0x01010123u;
|
record.FullCellId = 0x01010123u;
|
||||||
record.WorldEntity = new AcDream.Core.World.WorldEntity
|
record.WorldEntity = new AcDream.Core.World.WorldEntity
|
||||||
{
|
{
|
||||||
|
|
@ -521,32 +524,31 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
runtime = Runtime(record =>
|
runtime = Runtime(record =>
|
||||||
{
|
{
|
||||||
if (record.ServerGuid == departingGuid)
|
if (record.ServerGuid == departingGuid)
|
||||||
{
|
runtime.RegisterLiveEntity(Spawn(departingGuid, 2));
|
||||||
LiveEntityRecord replacement = runtime.RegisterLiveEntity(
|
|
||||||
Spawn(departingGuid, 2)).Record!;
|
|
||||||
replacement.FullCellId = 0x02020202u;
|
|
||||||
replacement.WorldEntity = Entity(
|
|
||||||
departingGuid,
|
|
||||||
new Vector3(999f, 999f, 999f),
|
|
||||||
Quaternion.Identity);
|
|
||||||
runtime.InstallPhysicsHost(replacement, Host(departingGuid));
|
|
||||||
}
|
|
||||||
CleanPhysicsHost(record);
|
CleanPhysicsHost(record);
|
||||||
});
|
});
|
||||||
LiveEntityRecord departing = runtime.RegisterLiveEntity(Spawn(departingGuid, 1)).Record!;
|
|
||||||
LiveEntityRecord watcherRecord = runtime.RegisterLiveEntity(Spawn(watcherGuid, 1)).Record!;
|
|
||||||
departing.FullCellId = 0x01010123u;
|
|
||||||
Quaternion departingRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
|
Quaternion departingRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
|
||||||
departing.WorldEntity = Entity(
|
LiveEntityRecord departing = runtime.RegisterAndMaterializeProjection(
|
||||||
departingGuid,
|
Spawn(departingGuid, 1),
|
||||||
new Vector3(12f, 34f, 56f),
|
localId => Entity(
|
||||||
departingRotation);
|
localId,
|
||||||
watcherRecord.WorldEntity = Entity(watcherGuid, Vector3.Zero, Quaternion.Identity);
|
departingGuid,
|
||||||
|
new Vector3(12f, 34f, 56f),
|
||||||
|
departingRotation));
|
||||||
|
departing.FullCellId = 0x01010123u;
|
||||||
|
WorldEntity departingEntity = Assert.IsType<WorldEntity>(departing.WorldEntity);
|
||||||
|
LiveEntityRecord watcherRecord = runtime.RegisterAndMaterializeProjection(
|
||||||
|
Spawn(watcherGuid, 1),
|
||||||
|
localId => Entity(
|
||||||
|
localId,
|
||||||
|
watcherGuid,
|
||||||
|
Vector3.Zero,
|
||||||
|
Quaternion.Identity));
|
||||||
IPhysicsObjHost? Resolve(uint id) =>
|
IPhysicsObjHost? Resolve(uint id) =>
|
||||||
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host) ? host : null;
|
runtime.TryGetPhysicsHost(id, out IPhysicsObjHost host) ? host : null;
|
||||||
var body = new PhysicsBody
|
var body = new PhysicsBody
|
||||||
{
|
{
|
||||||
Position = departing.WorldEntity.Position,
|
Position = departingEntity.Position,
|
||||||
Orientation = departingRotation,
|
Orientation = departingRotation,
|
||||||
};
|
};
|
||||||
var remote = new RemoteMotion(body);
|
var remote = new RemoteMotion(body);
|
||||||
|
|
@ -586,6 +588,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
Assert.True(runtime.UnregisterLiveEntity(
|
Assert.True(runtime.UnregisterLiveEntity(
|
||||||
new DeleteObject.Parsed(departingGuid, InstanceSequence: 1),
|
new DeleteObject.Parsed(departingGuid, InstanceSequence: 1),
|
||||||
isLocalPlayer: false));
|
isLocalPlayer: false));
|
||||||
|
LiveEntityRecord replacement = runtime.RegisterAndMaterializeProjection(
|
||||||
|
Spawn(departingGuid, 2));
|
||||||
|
replacement.FullCellId = 0x02020202u;
|
||||||
|
runtime.InstallPhysicsHost(
|
||||||
|
replacement,
|
||||||
|
Host(
|
||||||
|
departingGuid,
|
||||||
|
position: new Vector3(999f, 999f, 999f)));
|
||||||
|
|
||||||
TargetInfo exit = Assert.Single(delivered);
|
TargetInfo exit = Assert.Single(delivered);
|
||||||
Assert.Equal(TargetStatus.ExitWorld, exit.Status);
|
Assert.Equal(TargetStatus.ExitWorld, exit.Status);
|
||||||
|
|
@ -600,11 +610,11 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
{
|
{
|
||||||
const uint guid = 0x7000004Bu;
|
const uint guid = 0x7000004Bu;
|
||||||
var runtime = Runtime();
|
var runtime = Runtime();
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(Spawn(guid, 1)).Record!;
|
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(Spawn(guid, 1));
|
||||||
EntityPhysicsHost host = EntityPhysicsHost.CreateMinimal(record, _ => null, () => 0);
|
EntityPhysicsHost host = EntityPhysicsHost.CreateMinimal(record, _ => null, () => 0);
|
||||||
runtime.InstallPhysicsHost(record, host);
|
runtime.InstallPhysicsHost(record, host);
|
||||||
|
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.True(runtime.TryGetPhysicsHost(guid, out IPhysicsObjHost resolved));
|
Assert.True(runtime.TryGetPhysicsHost(guid, out IPhysicsObjHost resolved));
|
||||||
Assert.Same(host, resolved);
|
Assert.Same(host, resolved);
|
||||||
}
|
}
|
||||||
|
|
@ -645,12 +655,13 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
||||||
InstanceSequence: instance);
|
InstanceSequence: instance);
|
||||||
|
|
||||||
private static AcDream.Core.World.WorldEntity Entity(
|
private static AcDream.Core.World.WorldEntity Entity(
|
||||||
|
uint localId,
|
||||||
uint guid,
|
uint guid,
|
||||||
Vector3 position,
|
Vector3 position,
|
||||||
Quaternion rotation) =>
|
Quaternion rotation) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = 1_000_000u + (guid & 0xFFFFu),
|
Id = localId,
|
||||||
ServerGuid = guid,
|
ServerGuid = guid,
|
||||||
SourceGfxObjOrSetupId = 0x02000001u,
|
SourceGfxObjOrSetupId = 0x02000001u,
|
||||||
Position = position,
|
Position = position,
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,9 @@ public sealed class LiveEntityPresentationControllerTests
|
||||||
fixture.PresentationOrder);
|
fixture.PresentationOrder);
|
||||||
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
||||||
Assert.True(fixture.Entity.IsDrawVisible);
|
Assert.True(fixture.Entity.IsDrawVisible);
|
||||||
Assert.Same(fixture.Entity, Assert.Single(fixture.Runtime.WorldEntities).Value);
|
Assert.Same(
|
||||||
|
fixture.Entity,
|
||||||
|
Assert.Single(fixture.Runtime.VisibleRecords).WorldEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -289,7 +291,9 @@ public sealed class LiveEntityPresentationControllerTests
|
||||||
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
||||||
|
|
||||||
Assert.True(fixture.Runtime.RebucketLiveEntity(Fixture.Guid, 0x02020001u));
|
Assert.True(fixture.Runtime.RebucketLiveEntity(Fixture.Guid, 0x02020001u));
|
||||||
Assert.False(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid));
|
Assert.False(fixture.Runtime.TryGetInteractionEligibleEntity(
|
||||||
|
Fixture.Guid,
|
||||||
|
out _));
|
||||||
|
|
||||||
Assert.True(fixture.Runtime.TryApplyState(
|
Assert.True(fixture.Runtime.TryApplyState(
|
||||||
new SetState.Parsed(Fixture.Guid, 0u, 1, 3),
|
new SetState.Parsed(Fixture.Guid, 0u, 1, 3),
|
||||||
|
|
@ -303,7 +307,9 @@ public sealed class LiveEntityPresentationControllerTests
|
||||||
new LandBlock(),
|
new LandBlock(),
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
|
|
||||||
Assert.True(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid));
|
Assert.True(fixture.Runtime.TryGetInteractionEligibleEntity(
|
||||||
|
Fixture.Guid,
|
||||||
|
out _));
|
||||||
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,7 +324,9 @@ public sealed class LiveEntityPresentationControllerTests
|
||||||
Fixture.Guid,
|
Fixture.Guid,
|
||||||
0x02020001u));
|
0x02020001u));
|
||||||
|
|
||||||
Assert.False(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid));
|
Assert.False(fixture.Runtime.TryGetInteractionEligibleEntity(
|
||||||
|
Fixture.Guid,
|
||||||
|
out _));
|
||||||
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
||||||
Assert.Equal(1, fixture.Shadows.RetainedRegistrationCount);
|
Assert.Equal(1, fixture.Shadows.RetainedRegistrationCount);
|
||||||
Assert.Equal(1, fixture.Shadows.SuspendedRegistrationCount);
|
Assert.Equal(1, fixture.Shadows.SuspendedRegistrationCount);
|
||||||
|
|
@ -331,7 +339,7 @@ public sealed class LiveEntityPresentationControllerTests
|
||||||
|
|
||||||
Assert.Same(
|
Assert.Same(
|
||||||
fixture.Entity,
|
fixture.Entity,
|
||||||
Assert.Single(fixture.Runtime.WorldEntities).Value);
|
Assert.Single(fixture.Runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
||||||
Assert.Equal(0, fixture.Shadows.SuspendedRegistrationCount);
|
Assert.Equal(0, fixture.Shadows.SuspendedRegistrationCount);
|
||||||
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
|
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
|
||||||
|
|
@ -467,7 +475,7 @@ public sealed class LiveEntityPresentationControllerTests
|
||||||
new LandBlock(),
|
new LandBlock(),
|
||||||
Array.Empty<WorldEntity>()));
|
Array.Empty<WorldEntity>()));
|
||||||
|
|
||||||
Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Equal(1, shadows.TotalRegistered);
|
Assert.Equal(1, shadows.TotalRegistered);
|
||||||
Assert.Equal(0, shadows.SuspendedRegistrationCount);
|
Assert.Equal(0, shadows.SuspendedRegistrationCount);
|
||||||
Assert.False(controller.HasDeferredShadowRestore(Fixture.Guid));
|
Assert.False(controller.HasDeferredShadowRestore(Fixture.Guid));
|
||||||
|
|
|
||||||
|
|
@ -247,10 +247,8 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
|
||||||
MotionState: null,
|
MotionState: null,
|
||||||
MotionTableId: null,
|
MotionTableId: null,
|
||||||
InstanceSequence: instance);
|
InstanceSequence: instance);
|
||||||
LiveEntityRecord record = Live.RegisterLiveEntity(spawn).Record!;
|
LiveEntityRecord record = Live.RegisterAndMaterializeProjection(
|
||||||
WorldEntity entity = Live.MaterializeLiveEntity(
|
spawn,
|
||||||
Guid,
|
|
||||||
Cell,
|
|
||||||
id => new WorldEntity
|
id => new WorldEntity
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -260,7 +258,8 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
|
||||||
Rotation = Quaternion.Identity,
|
Rotation = Quaternion.Identity,
|
||||||
MeshRefs = Array.Empty<MeshRef>(),
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
ParentCellId = Cell,
|
ParentCellId = Cell,
|
||||||
})!;
|
});
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||||
var snapshot = new WorldEntitySnapshot(
|
var snapshot = new WorldEntitySnapshot(
|
||||||
entity.Id,
|
entity.Id,
|
||||||
entity.SourceGfxObjOrSetupId,
|
entity.SourceGfxObjOrSetupId,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ public sealed class LiveEntityRuntimeTeardownControllerTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Retry_ReusesPlanAndDoesNotReplayCompletedOwners()
|
public void Retry_ReusesPlanAndDoesNotReplayCompletedOwners()
|
||||||
{
|
{
|
||||||
LiveEntityRecord record = new(Spawn());
|
LiveEntityRecord record =
|
||||||
|
LiveEntityTestFixture.CreateExactProjectionRecord(Spawn());
|
||||||
int factoryCalls = 0;
|
int factoryCalls = 0;
|
||||||
int completedOwnerCalls = 0;
|
int completedOwnerCalls = 0;
|
||||||
int retryingOwnerCalls = 0;
|
int retryingOwnerCalls = 0;
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Single(spatial.Entities);
|
Assert.Single(spatial.Entities);
|
||||||
Assert.True(runtime.WithdrawLiveEntityProjection(spawn.Guid));
|
Assert.True(runtime.WithdrawLiveEntityProjection(spawn.Guid));
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(0, resources.UnregisterCount);
|
Assert.Equal(0, resources.UnregisterCount);
|
||||||
Assert.True(runtime.TryGetAnimationRuntime(entity!.Id, out _));
|
Assert.True(runtime.TryGetAnimationRuntime(entity!.Id, out _));
|
||||||
|
|
@ -424,6 +424,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
guid,
|
guid,
|
||||||
0x01010001u,
|
0x01010001u,
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||||
var animation = new AnimationRuntime(entity);
|
var animation = new AnimationRuntime(entity);
|
||||||
var remote = new RemoteMotionRuntime();
|
var remote = new RemoteMotionRuntime();
|
||||||
var projectile = new ProjectileRuntime(remote.Body);
|
var projectile = new ProjectileRuntime(remote.Body);
|
||||||
|
|
@ -431,9 +432,9 @@ public sealed class LiveEntityRuntimeTests
|
||||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||||
runtime.SetProjectileRuntime(guid, projectile);
|
runtime.SetProjectileRuntime(guid, projectile);
|
||||||
|
|
||||||
Assert.Same(animation, Assert.Single(runtime.SpatialAnimationRuntimes).Value);
|
Assert.True(runtime.IsCurrentSpatialAnimation(record, animation));
|
||||||
Assert.Same(remote, Assert.Single(runtime.SpatialRemoteMotionRuntimes).Value);
|
Assert.True(runtime.IsCurrentSpatialRemoteMotion(record, remote));
|
||||||
Assert.Same(projectile, Assert.Single(runtime.SpatialProjectileRuntimes).Value);
|
Assert.True(runtime.IsCurrentSpatialProjectile(record, projectile));
|
||||||
|
|
||||||
// Hidden suppresses presentation, not the live CPhysicsObj workset.
|
// Hidden suppresses presentation, not the live CPhysicsObj workset.
|
||||||
Assert.True(runtime.TryApplyState(new SetState.Parsed(
|
Assert.True(runtime.TryApplyState(new SetState.Parsed(
|
||||||
|
|
@ -441,16 +442,16 @@ public sealed class LiveEntityRuntimeTests
|
||||||
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.IgnoreCollisions),
|
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.IgnoreCollisions),
|
||||||
1,
|
1,
|
||||||
2), out _));
|
2), out _));
|
||||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
Assert.Equal(1, runtime.SpatialAnimationRuntimeCount);
|
||||||
Assert.Single(runtime.SpatialRemoteMotionRuntimes);
|
Assert.Equal(1, runtime.SpatialRemoteMotionRuntimeCount);
|
||||||
Assert.Single(runtime.SpatialProjectileRuntimes);
|
Assert.Equal(1, runtime.SpatialProjectileRuntimeCount);
|
||||||
|
|
||||||
// A pending projection retains every logical component but disappears
|
// A pending projection retains every logical component but disappears
|
||||||
// from all per-frame worksets.
|
// from all per-frame worksets.
|
||||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
|
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
|
||||||
Assert.Empty(runtime.SpatialAnimationRuntimes);
|
Assert.Equal(0, runtime.SpatialAnimationRuntimeCount);
|
||||||
Assert.Empty(runtime.SpatialRemoteMotionRuntimes);
|
Assert.Equal(0, runtime.SpatialRemoteMotionRuntimeCount);
|
||||||
Assert.Empty(runtime.SpatialProjectileRuntimes);
|
Assert.Equal(0, runtime.SpatialProjectileRuntimeCount);
|
||||||
Assert.True(runtime.TryGetAnimationRuntime(entity.Id, out var retainedAnimation));
|
Assert.True(runtime.TryGetAnimationRuntime(entity.Id, out var retainedAnimation));
|
||||||
Assert.Same(animation, retainedAnimation);
|
Assert.Same(animation, retainedAnimation);
|
||||||
Assert.True(runtime.TryGetRemoteMotionRuntime(guid, out var retainedRemote));
|
Assert.True(runtime.TryGetRemoteMotionRuntime(guid, out var retainedRemote));
|
||||||
|
|
@ -459,14 +460,14 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Same(projectile, retainedProjectile);
|
Assert.Same(projectile, retainedProjectile);
|
||||||
|
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0202FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0202FFFFu));
|
||||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
Assert.Equal(1, runtime.SpatialAnimationRuntimeCount);
|
||||||
Assert.Single(runtime.SpatialRemoteMotionRuntimes);
|
Assert.Equal(1, runtime.SpatialRemoteMotionRuntimeCount);
|
||||||
Assert.Single(runtime.SpatialProjectileRuntimes);
|
Assert.Equal(1, runtime.SpatialProjectileRuntimeCount);
|
||||||
|
|
||||||
Assert.True(runtime.WithdrawLiveEntityProjection(guid));
|
Assert.True(runtime.WithdrawLiveEntityProjection(guid));
|
||||||
Assert.Empty(runtime.SpatialAnimationRuntimes);
|
Assert.Equal(0, runtime.SpatialAnimationRuntimeCount);
|
||||||
Assert.Empty(runtime.SpatialRemoteMotionRuntimes);
|
Assert.Equal(0, runtime.SpatialRemoteMotionRuntimeCount);
|
||||||
Assert.Empty(runtime.SpatialProjectileRuntimes);
|
Assert.Equal(0, runtime.SpatialProjectileRuntimeCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -484,22 +485,25 @@ public sealed class LiveEntityRuntimeTests
|
||||||
var animation = new AnimationRuntime(entity);
|
var animation = new AnimationRuntime(entity);
|
||||||
runtime.SetAnimationRuntime(guid, animation);
|
runtime.SetAnimationRuntime(guid, animation);
|
||||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(BoundSlot(runtime));
|
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(BoundSlot(runtime));
|
||||||
|
var spatialIds = new HashSet<uint>();
|
||||||
|
|
||||||
Assert.Equal(1, view.Count);
|
Assert.Equal(1, view.Count);
|
||||||
Assert.Equal(entity.Id, Assert.Single(view.Keys));
|
view.CopySpatialIdsTo(spatialIds);
|
||||||
|
Assert.Equal(entity.Id, Assert.Single(spatialIds));
|
||||||
Assert.Same(animation, Assert.Single(view).Value);
|
Assert.Same(animation, Assert.Single(view).Value);
|
||||||
|
|
||||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
|
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
|
||||||
Assert.Equal(0, view.Count);
|
Assert.Equal(0, view.Count);
|
||||||
Assert.Empty(view.Keys);
|
view.CopySpatialIdsTo(spatialIds);
|
||||||
|
Assert.Empty(spatialIds);
|
||||||
Assert.Empty(view);
|
Assert.Empty(view);
|
||||||
Assert.True(view.TryGetValue(entity.Id, out AnimationRuntime retained));
|
Assert.True(view.TryGetValue(entity.Id, out AnimationRuntime retained));
|
||||||
Assert.Same(animation, retained);
|
Assert.Same(animation, retained);
|
||||||
|
|
||||||
Assert.True(view.Remove(entity.Id));
|
Assert.True(view.Remove(entity.Id));
|
||||||
Assert.False(view.TryGetValue(entity.Id, out _));
|
Assert.False(view.TryGetValue(entity.Id, out _));
|
||||||
Assert.Empty(runtime.AnimationRuntimes);
|
Assert.Equal(0, runtime.AnimationRuntimeCount);
|
||||||
Assert.Empty(runtime.SpatialAnimationRuntimes);
|
Assert.Equal(0, runtime.SpatialAnimationRuntimeCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -531,8 +535,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.Single(visited);
|
Assert.Single(visited);
|
||||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
Assert.Equal(1, runtime.SpatialAnimationRuntimeCount);
|
||||||
Assert.Equal(2, runtime.AnimationRuntimes.Count);
|
Assert.Equal(2, runtime.AnimationRuntimeCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -601,10 +605,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.False(runtime.WithdrawLiveEntityProjection(guid));
|
Assert.False(runtime.WithdrawLiveEntityProjection(guid));
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
||||||
Assert.Equal((ushort)2, current.Generation);
|
Assert.Equal((ushort)2, current.Generation);
|
||||||
KeyValuePair<uint, ILiveEntityAnimationRuntime> indexed =
|
Assert.True(runtime.IsCurrentSpatialAnimation(
|
||||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
current,
|
||||||
Assert.Same(current.AnimationRuntime, indexed.Value);
|
current.AnimationRuntime!));
|
||||||
Assert.NotSame(oldAnimation, indexed.Value);
|
Assert.NotSame(oldAnimation, current.AnimationRuntime);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -670,13 +674,18 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
||||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
WorldSession.EntitySpawn spawn = Spawn(guid, 6, 1, 0x01010001u);
|
WorldSession.EntitySpawn spawn = Spawn(guid, 6, 1, 0x01010001u);
|
||||||
runtime.RegisterLiveEntity(spawn);
|
LiveEntityRegistrationResult registration =
|
||||||
|
runtime.RegisterLiveEntity(spawn);
|
||||||
|
RuntimeEntityRecord canonical = Assert.IsType<RuntimeEntityRecord>(
|
||||||
|
registration.Canonical);
|
||||||
var profile = new EffectProfile();
|
var profile = new EffectProfile();
|
||||||
runtime.SetEffectProfile(guid, profile);
|
|
||||||
runtime.MaterializeLiveEntity(
|
runtime.MaterializeLiveEntity(
|
||||||
guid,
|
canonical,
|
||||||
spawn.Position!.Value.LandblockId,
|
spawn.Position!.Value.LandblockId,
|
||||||
id => Entity(id, guid));
|
id => Entity(id, guid),
|
||||||
|
LiveEntityProjectionKind.World,
|
||||||
|
initializeProjection: record => record.EffectProfile = profile,
|
||||||
|
out _);
|
||||||
|
|
||||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x01020001u));
|
Assert.True(runtime.RebucketLiveEntity(guid, 0x01020001u));
|
||||||
Assert.True(runtime.TryGetEffectProfile(guid, out var retained));
|
Assert.True(runtime.TryGetEffectProfile(guid, out var retained));
|
||||||
|
|
@ -865,7 +874,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
LiveEntityProjectionKind.Attached)!;
|
LiveEntityProjectionKind.Attached)!;
|
||||||
|
|
||||||
Assert.Single(spatial.Entities);
|
Assert.Single(spatial.Entities);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
||||||
Assert.True(runtime.TryGetWorldEntity(spawn.Guid, out WorldEntity resolved));
|
Assert.True(runtime.TryGetWorldEntity(spawn.Guid, out WorldEntity resolved));
|
||||||
Assert.Same(attached, resolved);
|
Assert.Same(attached, resolved);
|
||||||
|
|
@ -880,7 +889,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
|
|
||||||
Assert.Same(attached, same);
|
Assert.Same(attached, same);
|
||||||
Assert.True(same.IsAncestorDrawVisible);
|
Assert.True(same.IsAncestorDrawVisible);
|
||||||
Assert.Same(attached, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(attached, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.True(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
Assert.True(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
||||||
Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
|
|
@ -902,12 +911,12 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Equal(1, spatial.PendingLiveEntityCount);
|
Assert.Equal(1, spatial.PendingLiveEntityCount);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
|
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
||||||
Assert.Same(entity, Assert.Single(spatial.Entities));
|
Assert.Same(entity, Assert.Single(spatial.Entities));
|
||||||
Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.True(runtime.RebucketLiveEntity(spawn.Guid, 0x01020001u));
|
Assert.True(runtime.RebucketLiveEntity(spawn.Guid, 0x01020001u));
|
||||||
Assert.Same(entity, Assert.Single(spatial.Entities));
|
Assert.Same(entity, Assert.Single(spatial.Entities));
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
|
|
@ -928,8 +937,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spawn.Position!.Value.LandblockId,
|
spawn.Position!.Value.LandblockId,
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
|
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.True(runtime.TryApplyVector(
|
Assert.True(runtime.TryApplyVector(
|
||||||
new VectorUpdate.Parsed(
|
new VectorUpdate.Parsed(
|
||||||
guid,
|
guid,
|
||||||
|
|
@ -960,8 +969,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
||||||
Assert.True(runtime.RebucketLiveEntity(guid, accepted.Position!.Value.LandblockId));
|
Assert.True(runtime.RebucketLiveEntity(guid, accepted.Position!.Value.LandblockId));
|
||||||
|
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(0, resources.UnregisterCount);
|
Assert.Equal(0, resources.UnregisterCount);
|
||||||
Assert.Equal(1, spatial.PendingLiveEntityCount);
|
Assert.Equal(1, spatial.PendingLiveEntityCount);
|
||||||
|
|
@ -1097,11 +1106,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
||||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(spawn).Record!;
|
LiveEntityRecord record = RegisterProjection(runtime, spawn);
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
guid,
|
|
||||||
spawn.Position!.Value.LandblockId,
|
|
||||||
id => Entity(id, guid));
|
|
||||||
RetailObjectQuantumClock clock = record.ObjectClock;
|
RetailObjectQuantumClock clock = record.ObjectClock;
|
||||||
Assert.Equal(0, clock.Advance(0.02).Count);
|
Assert.Equal(0, clock.Advance(0.02).Count);
|
||||||
|
|
||||||
|
|
@ -1145,15 +1150,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
1,
|
1,
|
||||||
0x01010001u,
|
0x01010001u,
|
||||||
PhysicsStateFlags.Static);
|
PhysicsStateFlags.Static);
|
||||||
LiveEntityRecord record = runtime.RegisterLiveEntity(spawn).Record!;
|
LiveEntityRecord record = RegisterProjection(runtime, spawn);
|
||||||
var remote = new RemoteMotionRuntime();
|
var remote = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||||
|
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
guid,
|
|
||||||
spawn.Position!.Value.LandblockId,
|
|
||||||
id => Entity(id, guid));
|
|
||||||
|
|
||||||
Assert.False(record.ObjectClock.IsActive);
|
Assert.False(record.ObjectClock.IsActive);
|
||||||
Assert.Equal(0.0, record.ObjectClock.PendingSeconds, 8);
|
Assert.Equal(0.0, record.ObjectClock.PendingSeconds, 8);
|
||||||
Assert.False(remote.Body.TransientState.HasFlag(TransientStateFlags.Active));
|
Assert.False(remote.Body.TransientState.HasFlag(TransientStateFlags.Active));
|
||||||
|
|
@ -1168,14 +1168,11 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
|
|
||||||
LiveEntityRecord ordinary = runtime.RegisterLiveEntity(
|
LiveEntityRecord ordinary = RegisterProjection(
|
||||||
Spawn(ordinaryGuid, 1, 1, 0x01010001u)).Record!;
|
runtime,
|
||||||
|
Spawn(ordinaryGuid, 1, 1, 0x01010001u));
|
||||||
var ordinaryRemote = new RemoteMotionRuntime();
|
var ordinaryRemote = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote);
|
runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote);
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
ordinaryGuid,
|
|
||||||
0x01010001u,
|
|
||||||
id => Entity(id, ordinaryGuid));
|
|
||||||
ordinary.ObjectClock.Deactivate();
|
ordinary.ObjectClock.Deactivate();
|
||||||
ordinaryRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
ordinaryRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
||||||
|
|
||||||
|
|
@ -1183,14 +1180,16 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(ordinary.ObjectClock.IsActive);
|
Assert.True(ordinary.ObjectClock.IsActive);
|
||||||
Assert.True(ordinaryRemote.Body.TransientState.HasFlag(TransientStateFlags.Active));
|
Assert.True(ordinaryRemote.Body.TransientState.HasFlag(TransientStateFlags.Active));
|
||||||
|
|
||||||
LiveEntityRecord staticRecord = runtime.RegisterLiveEntity(
|
LiveEntityRecord staticRecord = RegisterProjection(
|
||||||
Spawn(staticGuid, 1, 1, 0x01010001u, PhysicsStateFlags.Static)).Record!;
|
runtime,
|
||||||
|
Spawn(
|
||||||
|
staticGuid,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0x01010001u,
|
||||||
|
PhysicsStateFlags.Static));
|
||||||
var staticRemote = new RemoteMotionRuntime();
|
var staticRemote = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(staticGuid, staticRemote);
|
runtime.SetRemoteMotionRuntime(staticGuid, staticRemote);
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
staticGuid,
|
|
||||||
0x01010001u,
|
|
||||||
id => Entity(id, staticGuid));
|
|
||||||
|
|
||||||
Assert.False(runtime.TryActivateOrdinaryObject(staticGuid, staticRemote));
|
Assert.False(runtime.TryActivateOrdinaryObject(staticGuid, staticRemote));
|
||||||
Assert.False(staticRecord.ObjectClock.IsActive);
|
Assert.False(staticRecord.ObjectClock.IsActive);
|
||||||
|
|
@ -1207,14 +1206,11 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
|
|
||||||
LiveEntityRecord ordinary = runtime.RegisterLiveEntity(
|
LiveEntityRecord ordinary = RegisterProjection(
|
||||||
Spawn(ordinaryGuid, 1, 1, 0x01010001u)).Record!;
|
runtime,
|
||||||
|
Spawn(ordinaryGuid, 1, 1, 0x01010001u));
|
||||||
var ordinaryRemote = new RemoteMotionRuntime();
|
var ordinaryRemote = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote);
|
runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote);
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
ordinaryGuid,
|
|
||||||
0x01010001u,
|
|
||||||
id => Entity(id, ordinaryGuid));
|
|
||||||
ordinary.ObjectClock.Deactivate();
|
ordinary.ObjectClock.Deactivate();
|
||||||
ordinaryRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
ordinaryRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
||||||
ordinaryRemote.Body.LastUpdateTime = 1.0;
|
ordinaryRemote.Body.LastUpdateTime = 1.0;
|
||||||
|
|
@ -1234,14 +1230,11 @@ public sealed class LiveEntityRuntimeTests
|
||||||
// Defensive split state: the retained object clock is already active,
|
// Defensive split state: the retained object clock is already active,
|
||||||
// but a body consumer cleared its legacy Active bit. set_velocity must
|
// but a body consumer cleared its legacy Active bit. set_velocity must
|
||||||
// still rebase that body's compatibility timestamp.
|
// still rebase that body's compatibility timestamp.
|
||||||
LiveEntityRecord defensive = runtime.RegisterLiveEntity(
|
LiveEntityRecord defensive = RegisterProjection(
|
||||||
Spawn(defensiveGuid, 1, 1, 0x01010001u)).Record!;
|
runtime,
|
||||||
|
Spawn(defensiveGuid, 1, 1, 0x01010001u));
|
||||||
var defensiveRemote = new RemoteMotionRuntime();
|
var defensiveRemote = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(defensiveGuid, defensiveRemote);
|
runtime.SetRemoteMotionRuntime(defensiveGuid, defensiveRemote);
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
defensiveGuid,
|
|
||||||
0x01010001u,
|
|
||||||
id => Entity(id, defensiveGuid));
|
|
||||||
Assert.True(defensive.ObjectClock.IsActive);
|
Assert.True(defensive.ObjectClock.IsActive);
|
||||||
defensiveRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
defensiveRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
||||||
defensiveRemote.Body.LastUpdateTime = 2.0;
|
defensiveRemote.Body.LastUpdateTime = 2.0;
|
||||||
|
|
@ -1255,19 +1248,16 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(defensiveRemote.Body.IsActive);
|
Assert.True(defensiveRemote.Body.IsActive);
|
||||||
Assert.Equal(9.0, defensiveRemote.Body.LastUpdateTime);
|
Assert.Equal(9.0, defensiveRemote.Body.LastUpdateTime);
|
||||||
|
|
||||||
LiveEntityRecord staticRecord = runtime.RegisterLiveEntity(
|
LiveEntityRecord staticRecord = RegisterProjection(
|
||||||
|
runtime,
|
||||||
Spawn(
|
Spawn(
|
||||||
staticGuid,
|
staticGuid,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
0x01010001u,
|
0x01010001u,
|
||||||
PhysicsStateFlags.Static)).Record!;
|
PhysicsStateFlags.Static));
|
||||||
var staticRemote = new RemoteMotionRuntime();
|
var staticRemote = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(staticGuid, staticRemote);
|
runtime.SetRemoteMotionRuntime(staticGuid, staticRemote);
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
staticGuid,
|
|
||||||
0x01010001u,
|
|
||||||
id => Entity(id, staticGuid));
|
|
||||||
staticRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
staticRemote.Body.TransientState &= ~TransientStateFlags.Active;
|
||||||
staticRemote.Body.LastUpdateTime = 3.0;
|
staticRemote.Body.LastUpdateTime = 3.0;
|
||||||
|
|
||||||
|
|
@ -1291,8 +1281,9 @@ public sealed class LiveEntityRuntimeTests
|
||||||
const uint childGuid = 0x70000055u;
|
const uint childGuid = 0x70000055u;
|
||||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||||
runtime.RegisterLiveEntity(Spawn(parentGuid, 1, 1, 0x01010001u));
|
runtime.RegisterLiveEntity(Spawn(parentGuid, 1, 1, 0x01010001u));
|
||||||
LiveEntityRecord child = runtime.RegisterLiveEntity(
|
LiveEntityRecord child = RegisterProjection(
|
||||||
Spawn(childGuid, 1, 1, 0x01010001u)).Record!;
|
runtime,
|
||||||
|
Spawn(childGuid, 1, 1, 0x01010001u));
|
||||||
ulong initialPosition = child.PositionAuthorityVersion;
|
ulong initialPosition = child.PositionAuthorityVersion;
|
||||||
ulong initialVelocity = child.VelocityAuthorityVersion;
|
ulong initialVelocity = child.VelocityAuthorityVersion;
|
||||||
|
|
||||||
|
|
@ -1325,11 +1316,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
WorldSession.EntitySpawn firstSpawn = Spawn(guid, 1, 1, 0x01010001u);
|
WorldSession.EntitySpawn firstSpawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||||
LiveEntityRecord first = runtime.RegisterLiveEntity(firstSpawn).Record!;
|
LiveEntityRecord first = RegisterProjection(runtime, firstSpawn);
|
||||||
runtime.MaterializeLiveEntity(
|
|
||||||
guid,
|
|
||||||
firstSpawn.Position!.Value.LandblockId,
|
|
||||||
id => Entity(id, guid));
|
|
||||||
RetailObjectQuantumClock firstClock = first.ObjectClock;
|
RetailObjectQuantumClock firstClock = first.ObjectClock;
|
||||||
Assert.Equal(0, firstClock.Advance(0.02).Count);
|
Assert.Equal(0, firstClock.Advance(0.02).Count);
|
||||||
|
|
||||||
|
|
@ -1341,7 +1328,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
||||||
isLocalPlayer: false));
|
isLocalPlayer: false));
|
||||||
WorldSession.EntitySpawn secondSpawn = Spawn(guid, 2, 2, 0x01010001u);
|
WorldSession.EntitySpawn secondSpawn = Spawn(guid, 2, 2, 0x01010001u);
|
||||||
LiveEntityRecord second = runtime.RegisterLiveEntity(secondSpawn).Record!;
|
LiveEntityRecord second = RegisterProjection(runtime, secondSpawn);
|
||||||
|
|
||||||
Assert.NotSame(first, second);
|
Assert.NotSame(first, second);
|
||||||
Assert.NotSame(firstClock, second.ObjectClock);
|
Assert.NotSame(firstClock, second.ObjectClock);
|
||||||
|
|
@ -1364,9 +1351,17 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(runtime.TryApplyState(
|
Assert.True(runtime.TryApplyState(
|
||||||
new SetState.Parsed(stateBeforeBindGuid, (uint)firstState, 1, 2),
|
new SetState.Parsed(stateBeforeBindGuid, (uint)firstState, 1, 2),
|
||||||
out _));
|
out _));
|
||||||
|
runtime.MaterializeLiveEntity(
|
||||||
|
stateBeforeBindGuid,
|
||||||
|
0x01010001u,
|
||||||
|
id => Entity(id, stateBeforeBindGuid));
|
||||||
var lateBody = new RemoteMotionRuntime();
|
var lateBody = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(stateBeforeBindGuid, lateBody);
|
runtime.SetRemoteMotionRuntime(stateBeforeBindGuid, lateBody);
|
||||||
|
|
||||||
|
runtime.MaterializeLiveEntity(
|
||||||
|
bindBeforeStateGuid,
|
||||||
|
0x01010001u,
|
||||||
|
id => Entity(id, bindBeforeStateGuid));
|
||||||
var earlyBody = new RemoteMotionRuntime();
|
var earlyBody = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(bindBeforeStateGuid, earlyBody);
|
runtime.SetRemoteMotionRuntime(bindBeforeStateGuid, earlyBody);
|
||||||
PhysicsStateFlags secondState = PhysicsStateFlags.Static
|
PhysicsStateFlags secondState = PhysicsStateFlags.Static
|
||||||
|
|
@ -1388,6 +1383,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
const uint guid = 0x70000039u;
|
const uint guid = 0x70000039u;
|
||||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||||
|
runtime.MaterializeLiveEntity(
|
||||||
|
guid,
|
||||||
|
0x01010001u,
|
||||||
|
id => Entity(id, guid));
|
||||||
var remote = new RemoteMotionRuntime();
|
var remote = new RemoteMotionRuntime();
|
||||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||||
|
|
||||||
|
|
@ -1404,9 +1403,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
const uint guid = 0x70000040u;
|
const uint guid = 0x70000040u;
|
||||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||||
|
|
||||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
LiveEntityRecord record = RegisterProjection(
|
||||||
|
runtime,
|
||||||
|
Spawn(guid, 1, 1, 0x01010001u));
|
||||||
|
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
|
||||||
Assert.True(record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition));
|
Assert.True(record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition));
|
||||||
Assert.Equal(RetailHiddenTransition.None, transition.HiddenTransition);
|
Assert.Equal(RetailHiddenTransition.None, transition.HiddenTransition);
|
||||||
Assert.Equal(PhysicsStateFlags.ReportCollisions, record.FinalPhysicsState);
|
Assert.Equal(PhysicsStateFlags.ReportCollisions, record.FinalPhysicsState);
|
||||||
|
|
@ -1431,8 +1431,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
id => Entity(id, guid))!;
|
id => Entity(id, guid))!;
|
||||||
|
|
||||||
Assert.False(entity.IsDrawVisible);
|
Assert.False(entity.IsDrawVisible);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Single(spatial.Entities);
|
Assert.Single(spatial.Entities);
|
||||||
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
|
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||||
|
|
@ -1462,7 +1462,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
2), out _, out RetailPhysicsStateTransition hidden));
|
2), out _, out RetailPhysicsStateTransition hidden));
|
||||||
Assert.Equal(RetailHiddenTransition.BecameHidden, hidden.HiddenTransition);
|
Assert.Equal(RetailHiddenTransition.BecameHidden, hidden.HiddenTransition);
|
||||||
Assert.False(entity.IsDrawVisible);
|
Assert.False(entity.IsDrawVisible);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.False(runtime.TryGetInteractionEligibleEntity(guid, out _));
|
Assert.False(runtime.TryGetInteractionEligibleEntity(guid, out _));
|
||||||
Assert.False(runtime.TryGetInteractionEligibleRecord(guid, entity.Id, out _));
|
Assert.False(runtime.TryGetInteractionEligibleRecord(guid, entity.Id, out _));
|
||||||
|
|
||||||
|
|
@ -1478,7 +1478,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Same(entity, eligibleRecord.WorldEntity);
|
Assert.Same(entity, eligibleRecord.WorldEntity);
|
||||||
Assert.False(runtime.TryGetInteractionEligibleRecord(guid, entity.Id + 1u, out _));
|
Assert.False(runtime.TryGetInteractionEligibleRecord(guid, entity.Id + 1u, out _));
|
||||||
Assert.Same(entity, eligible);
|
Assert.Same(entity, eligible);
|
||||||
Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Same(entity, Assert.Single(spatial.Entities));
|
Assert.Same(entity, Assert.Single(spatial.Entities));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1573,7 +1573,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
|
|
||||||
spatial.RemoveLandblock(0x0101FFFFu);
|
spatial.RemoveLandblock(0x0101FFFFu);
|
||||||
|
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.True(runtime.TryGetRecord(spawn.Guid, out LiveEntityRecord record));
|
Assert.True(runtime.TryGetRecord(spawn.Guid, out LiveEntityRecord record));
|
||||||
Assert.True(record.IsSpatiallyProjected);
|
Assert.True(record.IsSpatiallyProjected);
|
||||||
Assert.False(record.IsSpatiallyVisible);
|
Assert.False(record.IsSpatiallyVisible);
|
||||||
|
|
@ -1582,7 +1582,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0, resources.UnregisterCount);
|
Assert.Equal(0, resources.UnregisterCount);
|
||||||
|
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
}
|
}
|
||||||
|
|
@ -1632,7 +1632,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
runtime.Clear();
|
runtime.Clear();
|
||||||
|
|
||||||
Assert.Equal(0, runtime.Count);
|
Assert.Equal(0, runtime.Count);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
|
|
@ -1654,10 +1654,11 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spawn.Position!.Value.LandblockId,
|
spawn.Position!.Value.LandblockId,
|
||||||
id => Entity(id, spawn.Guid)));
|
id => Entity(id, spawn.Guid)));
|
||||||
|
|
||||||
Assert.True(runtime.TryGetRecord(spawn.Guid, out LiveEntityRecord record));
|
Assert.False(runtime.TryGetRecord(spawn.Guid, out _));
|
||||||
Assert.Null(record.WorldEntity);
|
RuntimeEntityRecord canonical = CurrentCanonical(runtime, spawn.Guid);
|
||||||
|
Assert.Null(canonical.LocalEntityId);
|
||||||
Assert.Equal(0, runtime.MaterializedCount);
|
Assert.Equal(0, runtime.MaterializedCount);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
|
|
@ -1724,7 +1725,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(1, runtime.RetryPendingTeardowns());
|
Assert.Equal(1, runtime.RetryPendingTeardowns());
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
||||||
Assert.Same(replacement, current.WorldEntity);
|
Assert.Same(replacement, current.WorldEntity);
|
||||||
Assert.Same(replacement, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(replacement, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1746,7 +1747,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
LiveEntityRegistrationResult retransmit = runtime.RegisterLiveEntity(spawn);
|
LiveEntityRegistrationResult retransmit = runtime.RegisterLiveEntity(spawn);
|
||||||
|
|
||||||
Assert.False(retransmit.LogicalRegistrationCreated);
|
Assert.False(retransmit.LogicalRegistrationCreated);
|
||||||
Assert.Null(retransmit.Record);
|
Assert.Null(retransmit.Projection);
|
||||||
Assert.Equal(0, runtime.Count);
|
Assert.Equal(0, runtime.Count);
|
||||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||||
|
|
||||||
|
|
@ -1780,7 +1781,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0, runtime.Count);
|
Assert.Equal(0, runtime.Count);
|
||||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||||
Assert.Equal(1, runtime.MaterializedCount);
|
Assert.Equal(1, runtime.MaterializedCount);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1898,9 +1899,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Spawn(guid, 2, 1, 0x01010001u));
|
Spawn(guid, 2, 1, 0x01010001u));
|
||||||
|
|
||||||
Assert.NotNull(replacement.PriorGenerationCleanupFailure);
|
Assert.NotNull(replacement.PriorGenerationCleanupFailure);
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
RuntimeEntityRecord canonical = CurrentCanonical(runtime, guid);
|
||||||
Assert.Equal((ushort)2, record.Generation);
|
Assert.Equal((ushort)2, canonical.Generation);
|
||||||
Assert.Null(record.WorldEntity);
|
Assert.Null(canonical.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
WorldEntity installed = runtime.MaterializeLiveEntity(
|
WorldEntity installed = runtime.MaterializeLiveEntity(
|
||||||
guid,
|
guid,
|
||||||
0x01010001u,
|
0x01010001u,
|
||||||
|
|
@ -1949,7 +1951,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
runtime.RebucketLiveEntity(guid, 0x01020001u);
|
runtime.RebucketLiveEntity(guid, 0x01020001u);
|
||||||
|
|
||||||
Assert.Same(attached, Assert.Single(spatial.Entities));
|
Assert.Same(attached, Assert.Single(spatial.Entities));
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(0, resources.UnregisterCount);
|
Assert.Equal(0, resources.UnregisterCount);
|
||||||
var destination = Assert.Single(
|
var destination = Assert.Single(
|
||||||
|
|
@ -2082,8 +2084,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0x0202FFFFu, record.CanonicalLandblockId);
|
Assert.Equal(0x0202FFFFu, record.CanonicalLandblockId);
|
||||||
Assert.True(record.IsSpatiallyProjected);
|
Assert.True(record.IsSpatiallyProjected);
|
||||||
Assert.False(record.IsSpatiallyVisible);
|
Assert.False(record.IsSpatiallyVisible);
|
||||||
Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(entity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Equal(1, spatial.PendingLiveEntityCount);
|
Assert.Equal(1, spatial.PendingLiveEntityCount);
|
||||||
Assert.Equal(0, spatial.PendingVisibilityTransitionCount);
|
Assert.Equal(0, spatial.PendingVisibilityTransitionCount);
|
||||||
}
|
}
|
||||||
|
|
@ -2112,8 +2114,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||||
Assert.False(record.IsSpatiallyProjected);
|
Assert.False(record.IsSpatiallyProjected);
|
||||||
Assert.False(record.IsSpatiallyVisible);
|
Assert.False(record.IsSpatiallyVisible);
|
||||||
Assert.Empty(runtime.MaterializedWorldEntities);
|
Assert.Single(runtime.MaterializedRecords);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
||||||
Assert.Equal(0, spatial.PendingVisibilityTransitionCount);
|
Assert.Equal(0, spatial.PendingVisibilityTransitionCount);
|
||||||
|
|
@ -2148,8 +2150,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0x01010011u, replacement.FullCellId);
|
Assert.Equal(0x01010011u, replacement.FullCellId);
|
||||||
Assert.True(replacement.IsSpatiallyProjected);
|
Assert.True(replacement.IsSpatiallyProjected);
|
||||||
Assert.True(replacement.IsSpatiallyVisible);
|
Assert.True(replacement.IsSpatiallyVisible);
|
||||||
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Same(replacement.WorldEntity, Assert.Single(spatial.Entities));
|
Assert.Same(replacement.WorldEntity, Assert.Single(spatial.Entities));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2161,11 +2163,14 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||||
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid));
|
WorldEntity original = runtime.MaterializeLiveEntity(
|
||||||
|
guid,
|
||||||
|
0x01010001u,
|
||||||
|
id => Entity(id, guid))!;
|
||||||
bool replaced = false;
|
bool replaced = false;
|
||||||
spatial.LiveProjectionVisibilityChanged += (edgeGuid, visible) =>
|
spatial.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
||||||
{
|
{
|
||||||
if (replaced || visible || edgeGuid != guid)
|
if (replaced || visible || localEntityId != original.Id)
|
||||||
return;
|
return;
|
||||||
replaced = true;
|
replaced = true;
|
||||||
Assert.True(runtime.UnregisterLiveEntity(
|
Assert.True(runtime.UnregisterLiveEntity(
|
||||||
|
|
@ -2183,8 +2188,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0x0101FFFFu, replacement.CanonicalLandblockId);
|
Assert.Equal(0x0101FFFFu, replacement.CanonicalLandblockId);
|
||||||
Assert.True(replacement.IsSpatiallyProjected);
|
Assert.True(replacement.IsSpatiallyProjected);
|
||||||
Assert.True(replacement.IsSpatiallyVisible);
|
Assert.True(replacement.IsSpatiallyVisible);
|
||||||
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(replacement.WorldEntity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Same(replacement.WorldEntity, Assert.Single(spatial.Entities));
|
Assert.Same(replacement.WorldEntity, Assert.Single(spatial.Entities));
|
||||||
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
||||||
}
|
}
|
||||||
|
|
@ -2213,8 +2218,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0x01010022u, record.FullCellId);
|
Assert.Equal(0x01010022u, record.FullCellId);
|
||||||
Assert.True(record.IsSpatiallyProjected);
|
Assert.True(record.IsSpatiallyProjected);
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
Assert.Same(record.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(record.WorldEntity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Same(record.WorldEntity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(record.WorldEntity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Same(record.WorldEntity, Assert.Single(spatial.Entities));
|
Assert.Same(record.WorldEntity, Assert.Single(spatial.Entities));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2260,8 +2265,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(record.IsSpatiallyProjected);
|
Assert.True(record.IsSpatiallyProjected);
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
Assert.Same(pending, record.WorldEntity);
|
Assert.Same(pending, record.WorldEntity);
|
||||||
Assert.Same(pending, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(pending, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Same(pending, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(pending, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Same(pending, Assert.Single(spatial.Entities));
|
Assert.Same(pending, Assert.Single(spatial.Entities));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2300,7 +2305,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
||||||
Assert.Equal((ushort)2, current.Generation);
|
Assert.Equal((ushort)2, current.Generation);
|
||||||
Assert.Same(replacementEntity, current.WorldEntity);
|
Assert.Same(replacementEntity, current.WorldEntity);
|
||||||
Assert.Same(replacementEntity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(replacementEntity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||||
Assert.Equal(1, runtime.RetryPendingTeardowns());
|
Assert.Equal(1, runtime.RetryPendingTeardowns());
|
||||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||||
|
|
@ -2315,11 +2320,14 @@ public sealed class LiveEntityRuntimeTests
|
||||||
spatial.AddLandblock(EmptyLandblock(0x0303FFFFu));
|
spatial.AddLandblock(EmptyLandblock(0x0303FFFFu));
|
||||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||||
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid));
|
WorldEntity original = runtime.MaterializeLiveEntity(
|
||||||
|
guid,
|
||||||
|
0x01010001u,
|
||||||
|
id => Entity(id, guid))!;
|
||||||
bool rebucketed = false;
|
bool rebucketed = false;
|
||||||
spatial.LiveProjectionVisibilityChanged += (edgeGuid, visible) =>
|
spatial.LiveProjectionVisibilityChanged += (localEntityId, visible) =>
|
||||||
{
|
{
|
||||||
if (rebucketed || visible || edgeGuid != guid)
|
if (rebucketed || visible || localEntityId != original.Id)
|
||||||
return;
|
return;
|
||||||
rebucketed = true;
|
rebucketed = true;
|
||||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x03030033u));
|
Assert.True(runtime.RebucketLiveEntity(guid, 0x03030033u));
|
||||||
|
|
@ -2332,8 +2340,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0x0303FFFFu, record.CanonicalLandblockId);
|
Assert.Equal(0x0303FFFFu, record.CanonicalLandblockId);
|
||||||
Assert.True(record.IsSpatiallyProjected);
|
Assert.True(record.IsSpatiallyProjected);
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
Assert.Same(record.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
Assert.Same(record.WorldEntity, Assert.Single(runtime.MaterializedRecords).WorldEntity);
|
||||||
Assert.Same(record.WorldEntity, Assert.Single(runtime.WorldEntities).Value);
|
Assert.Same(record.WorldEntity, Assert.Single(runtime.VisibleRecords).WorldEntity);
|
||||||
Assert.Same(record.WorldEntity, Assert.Single(spatial.Entities));
|
Assert.Same(record.WorldEntity, Assert.Single(spatial.Entities));
|
||||||
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
||||||
}
|
}
|
||||||
|
|
@ -2374,7 +2382,8 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement));
|
||||||
Assert.Equal((ushort)2, replacement.Generation);
|
Assert.Equal((ushort)2, replacement.Generation);
|
||||||
Assert.True(replacement.IsSpatiallyVisible);
|
Assert.True(replacement.IsSpatiallyVisible);
|
||||||
Assert.True(spatial.IsLiveEntityVisible(guid));
|
Assert.True(spatial.IsLiveEntityVisible(
|
||||||
|
replacement.WorldEntity!.Id));
|
||||||
Assert.Equal(2, resources.RegisterCount);
|
Assert.Equal(2, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
}
|
}
|
||||||
|
|
@ -2448,11 +2457,14 @@ public sealed class LiveEntityRuntimeTests
|
||||||
error.Flatten().InnerExceptions,
|
error.Flatten().InnerExceptions,
|
||||||
exception => exception is InvalidOperationException
|
exception => exception is InvalidOperationException
|
||||||
&& exception.Message.Contains("active logical-lifetime transition", StringComparison.Ordinal));
|
&& exception.Message.Contains("active logical-lifetime transition", StringComparison.Ordinal));
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
RuntimeEntityRecord current = CurrentCanonical(runtime, guid);
|
||||||
Assert.Equal((ushort)2, current.Generation);
|
Assert.Equal((ushort)2, current.Generation);
|
||||||
Assert.Null(current.WorldEntity);
|
Assert.Null(current.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.DoesNotContain(guid, runtime.WorldEntities.Keys);
|
Assert.DoesNotContain(
|
||||||
|
runtime.VisibleRecords,
|
||||||
|
record => record.ServerGuid == guid);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
Assert.False(runtime.TryGetServerGuid(old.Id, out _));
|
Assert.False(runtime.TryGetServerGuid(old.Id, out _));
|
||||||
|
|
@ -2479,9 +2491,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
|
|
||||||
Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition);
|
Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition);
|
||||||
Assert.False(outer.LogicalRegistrationCreated);
|
Assert.False(outer.LogicalRegistrationCreated);
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
RuntimeEntityRecord current = CurrentCanonical(runtime, guid);
|
||||||
Assert.Equal((ushort)3, current.Generation);
|
Assert.Equal((ushort)3, current.Generation);
|
||||||
Assert.Null(current.WorldEntity);
|
Assert.Null(current.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
|
|
@ -2509,9 +2522,12 @@ public sealed class LiveEntityRuntimeTests
|
||||||
|
|
||||||
Assert.Equal(CreateObjectTimestampDisposition.NewGeneration, outer.Inbound.Disposition);
|
Assert.Equal(CreateObjectTimestampDisposition.NewGeneration, outer.Inbound.Disposition);
|
||||||
Assert.True(outer.LogicalRegistrationCreated);
|
Assert.True(outer.LogicalRegistrationCreated);
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement));
|
RuntimeEntityRecord replacement = CurrentCanonical(runtime, guid);
|
||||||
Assert.Equal((ushort)2, replacement.Generation);
|
Assert.Equal((ushort)2, replacement.Generation);
|
||||||
Assert.True(runtime.TryGetRecord(unrelatedGuid, out _));
|
Assert.Null(replacement.LocalEntityId);
|
||||||
|
Assert.Null(CurrentCanonical(runtime, unrelatedGuid).LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
|
Assert.False(runtime.TryGetRecord(unrelatedGuid, out _));
|
||||||
Assert.Equal(2, runtime.Count);
|
Assert.Equal(2, runtime.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2569,9 +2585,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
|
|
||||||
Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition);
|
Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition);
|
||||||
Assert.False(outer.LogicalRegistrationCreated);
|
Assert.False(outer.LogicalRegistrationCreated);
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
RuntimeEntityRecord current = CurrentCanonical(runtime, guid);
|
||||||
Assert.Equal((ushort)3, current.Generation);
|
Assert.Equal((ushort)3, current.Generation);
|
||||||
Assert.Null(current.WorldEntity);
|
Assert.Null(current.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
}
|
}
|
||||||
|
|
@ -2600,9 +2617,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
error.Flatten().InnerExceptions,
|
error.Flatten().InnerExceptions,
|
||||||
exception => exception is InvalidOperationException
|
exception => exception is InvalidOperationException
|
||||||
&& exception.Message == "old incarnation cleanup failed");
|
&& exception.Message == "old incarnation cleanup failed");
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
RuntimeEntityRecord current = CurrentCanonical(runtime, guid);
|
||||||
Assert.Equal((ushort)3, current.Generation);
|
Assert.Equal((ushort)3, current.Generation);
|
||||||
Assert.Null(current.WorldEntity);
|
Assert.Null(current.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
}
|
}
|
||||||
|
|
@ -2651,9 +2669,10 @@ public sealed class LiveEntityRuntimeTests
|
||||||
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)));
|
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)));
|
||||||
|
|
||||||
Assert.Contains("atomic resource registration", error.Message, StringComparison.Ordinal);
|
Assert.Contains("atomic resource registration", error.Message, StringComparison.Ordinal);
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
RuntimeEntityRecord current = CurrentCanonical(runtime, guid);
|
||||||
Assert.Equal((ushort)1, current.Generation);
|
Assert.Equal((ushort)1, current.Generation);
|
||||||
Assert.Null(current.WorldEntity);
|
Assert.Null(current.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
Assert.Equal(0, runtime.MaterializedCount);
|
Assert.Equal(0, runtime.MaterializedCount);
|
||||||
|
|
@ -2675,8 +2694,9 @@ public sealed class LiveEntityRuntimeTests
|
||||||
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)));
|
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)));
|
||||||
|
|
||||||
Assert.Contains("atomic resource registration", error.Message, StringComparison.Ordinal);
|
Assert.Contains("atomic resource registration", error.Message, StringComparison.Ordinal);
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
RuntimeEntityRecord current = CurrentCanonical(runtime, guid);
|
||||||
Assert.Null(current.WorldEntity);
|
Assert.Null(current.LocalEntityId);
|
||||||
|
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
Assert.Equal(0, runtime.MaterializedCount);
|
Assert.Equal(0, runtime.MaterializedCount);
|
||||||
|
|
@ -2746,7 +2766,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(0, runtime.Count);
|
Assert.Equal(0, runtime.Count);
|
||||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||||
Assert.Equal(1, runtime.MaterializedCount);
|
Assert.Equal(1, runtime.MaterializedCount);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
||||||
Assert.Equal(1, resources.RegisterCount);
|
Assert.Equal(1, resources.RegisterCount);
|
||||||
|
|
@ -2755,7 +2775,7 @@ public sealed class LiveEntityRuntimeTests
|
||||||
runtime.Clear();
|
runtime.Clear();
|
||||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||||
Assert.Equal(0, runtime.MaterializedCount);
|
Assert.Equal(0, runtime.MaterializedCount);
|
||||||
Assert.Empty(runtime.MaterializedWorldEntities);
|
Assert.Empty(runtime.MaterializedRecords);
|
||||||
Assert.Equal(2, resources.UnregisterCount);
|
Assert.Equal(2, resources.UnregisterCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2779,57 +2799,11 @@ public sealed class LiveEntityRuntimeTests
|
||||||
beforeTeardown: () => throw new InvalidOperationException("fixture callback failure")));
|
beforeTeardown: () => throw new InvalidOperationException("fixture callback failure")));
|
||||||
|
|
||||||
Assert.Equal(0, runtime.Count);
|
Assert.Equal(0, runtime.Count);
|
||||||
Assert.Empty(runtime.WorldEntities);
|
Assert.Empty(runtime.VisibleRecords);
|
||||||
Assert.Empty(spatial.Entities);
|
Assert.Empty(spatial.Entities);
|
||||||
Assert.Equal(1, resources.UnregisterCount);
|
Assert.Equal(1, resources.UnregisterCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IncarnationCleanup_OldTombstoneCannotMutateReplacementGuidState()
|
|
||||||
{
|
|
||||||
const uint guid = 0x70000035u;
|
|
||||||
var oldRecord = new LiveEntityRecord(Spawn(guid, 1, 1, 0x01010001u));
|
|
||||||
var replacement = new LiveEntityRecord(Spawn(guid, 2, 1, 0x01010001u));
|
|
||||||
LiveEntityRecord? current = replacement;
|
|
||||||
var cleanup = new LiveEntityIncarnationCleanup(oldRecord, _ => current);
|
|
||||||
object oldHost = new();
|
|
||||||
object newHost = new();
|
|
||||||
var hosts = new Dictionary<uint, object> { [guid] = newHost };
|
|
||||||
bool guidStateCleared = false;
|
|
||||||
|
|
||||||
var plan = new LiveEntityTeardownPlan(
|
|
||||||
[
|
|
||||||
() => cleanup.RunIfNoReplacement(() => guidStateCleared = true),
|
|
||||||
() => cleanup.RemoveCaptured(hosts, oldHost),
|
|
||||||
]);
|
|
||||||
plan.Advance();
|
|
||||||
|
|
||||||
Assert.False(guidStateCleared);
|
|
||||||
Assert.Same(newHost, hosts[guid]);
|
|
||||||
Assert.True(plan.IsComplete);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IncarnationCleanup_RemovesOnlyCapturedOwnerAndResumesAfterReplacementEnds()
|
|
||||||
{
|
|
||||||
const uint guid = 0x70000036u;
|
|
||||||
var oldRecord = new LiveEntityRecord(Spawn(guid, 1, 1, 0x01010001u));
|
|
||||||
var replacement = new LiveEntityRecord(Spawn(guid, 2, 1, 0x01010001u));
|
|
||||||
LiveEntityRecord? current = replacement;
|
|
||||||
var cleanup = new LiveEntityIncarnationCleanup(oldRecord, _ => current);
|
|
||||||
object oldHost = new();
|
|
||||||
var hosts = new Dictionary<uint, object> { [guid] = oldHost };
|
|
||||||
int guidCleanupCount = 0;
|
|
||||||
|
|
||||||
cleanup.RemoveCaptured(hosts, oldHost);
|
|
||||||
cleanup.RunIfNoReplacement(() => guidCleanupCount++);
|
|
||||||
current = null;
|
|
||||||
cleanup.RunIfNoReplacement(() => guidCleanupCount++);
|
|
||||||
|
|
||||||
Assert.Empty(hosts);
|
|
||||||
Assert.Equal(1, guidCleanupCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
||||||
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
||||||
|
|
||||||
|
|
@ -2849,6 +2823,34 @@ public sealed class LiveEntityRuntimeTests
|
||||||
: null,
|
: null,
|
||||||
update => runtime.TryApplyParent(update, out _));
|
update => runtime.TryApplyParent(update, out _));
|
||||||
|
|
||||||
|
private static LiveEntityRecord RegisterProjection(
|
||||||
|
LiveEntityRuntime runtime,
|
||||||
|
WorldSession.EntitySpawn spawn)
|
||||||
|
{
|
||||||
|
LiveEntityRegistrationResult registration =
|
||||||
|
runtime.RegisterLiveEntity(spawn);
|
||||||
|
RuntimeEntityRecord canonical = Assert.IsType<RuntimeEntityRecord>(
|
||||||
|
registration.Canonical);
|
||||||
|
WorldEntity entity = Assert.IsType<WorldEntity>(
|
||||||
|
runtime.MaterializeLiveEntity(
|
||||||
|
canonical,
|
||||||
|
spawn.Position?.LandblockId ?? 0u,
|
||||||
|
id => Entity(id, spawn.Guid),
|
||||||
|
LiveEntityProjectionKind.World,
|
||||||
|
initializeProjection: null,
|
||||||
|
out LiveEntityRecord? record));
|
||||||
|
Assert.Equal(entity.Id, canonical.LocalEntityId);
|
||||||
|
return Assert.IsType<LiveEntityRecord>(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeEntityRecord CurrentCanonical(
|
||||||
|
LiveEntityRuntime runtime,
|
||||||
|
uint serverGuid)
|
||||||
|
{
|
||||||
|
Assert.True(runtime.TryGetCanonical(serverGuid, out RuntimeEntityRecord canonical));
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
|
||||||
private static WorldEntity Entity(uint id, uint guid) => new()
|
private static WorldEntity Entity(uint id, uint guid) => new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
|
||||||
87
tests/AcDream.App.Tests/World/LiveEntityTestFixture.cs
Normal file
87
tests/AcDream.App.Tests/World/LiveEntityTestFixture.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test-only construction seam for exact Runtime identities and App
|
||||||
|
/// projections. Component fixtures must not manufacture detached
|
||||||
|
/// <see cref="LiveEntityRecord"/> instances because that bypasses the same
|
||||||
|
/// local-ID/incarnation key used by production.
|
||||||
|
/// </summary>
|
||||||
|
internal static class LiveEntityTestFixture
|
||||||
|
{
|
||||||
|
public static LiveEntityRecord CreateExactProjectionRecord(
|
||||||
|
WorldSession.EntitySpawn spawn)
|
||||||
|
{
|
||||||
|
var directory = new RuntimeEntityDirectory();
|
||||||
|
RuntimeEntityRecord canonical = directory.AddActive(spawn);
|
||||||
|
directory.ClaimLocalId(canonical);
|
||||||
|
var projections = new LiveEntityProjectionStore(directory);
|
||||||
|
return projections.AddMaterializing(canonical);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LiveEntityRecord RegisterAndMaterializeProjection(
|
||||||
|
this LiveEntityRuntime runtime,
|
||||||
|
WorldSession.EntitySpawn spawn,
|
||||||
|
Func<uint, WorldEntity>? factory = null,
|
||||||
|
LiveEntityProjectionKind projectionKind = LiveEntityProjectionKind.World,
|
||||||
|
Action<LiveEntityRecord>? initializeProjection = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
|
||||||
|
LiveEntityRegistrationResult registration =
|
||||||
|
runtime.RegisterLiveEntity(spawn);
|
||||||
|
RuntimeEntityRecord canonical = registration.Canonical
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"The fixture cannot materialize a stale CreateObject.");
|
||||||
|
if (registration.Projection is { } existing)
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
WorldEntity? entity = runtime.MaterializeLiveEntity(
|
||||||
|
canonical,
|
||||||
|
spawn.Position?.LandblockId ?? canonical.FullCellId,
|
||||||
|
factory ?? (id => CreateWorldEntity(id, spawn)),
|
||||||
|
projectionKind,
|
||||||
|
initializeProjection,
|
||||||
|
out LiveEntityRecord? record);
|
||||||
|
if (entity is null || record is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The fixture failed to materialize the exact Runtime incarnation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldEntity CreateWorldEntity(
|
||||||
|
uint localEntityId,
|
||||||
|
WorldSession.EntitySpawn spawn)
|
||||||
|
{
|
||||||
|
var position = spawn.Position;
|
||||||
|
return new WorldEntity
|
||||||
|
{
|
||||||
|
Id = localEntityId,
|
||||||
|
ServerGuid = spawn.Guid,
|
||||||
|
SourceGfxObjOrSetupId = spawn.SetupTableId ?? 0u,
|
||||||
|
Position = position is { } placed
|
||||||
|
? new Vector3(
|
||||||
|
placed.PositionX,
|
||||||
|
placed.PositionY,
|
||||||
|
placed.PositionZ)
|
||||||
|
: Vector3.Zero,
|
||||||
|
Rotation = position is { } oriented
|
||||||
|
? new Quaternion(
|
||||||
|
oriented.RotationX,
|
||||||
|
oriented.RotationY,
|
||||||
|
oriented.RotationZ,
|
||||||
|
oriented.RotationW)
|
||||||
|
: Quaternion.Identity,
|
||||||
|
MeshRefs = Array.Empty<MeshRef>(),
|
||||||
|
ParentCellId = position?.LandblockId,
|
||||||
|
EffectCellId = position?.LandblockId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
|
|
@ -14,28 +19,118 @@ public sealed class RuntimeEntityOwnershipTests
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic)
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
||||||
?? throw new InvalidOperationException("Missing canonical entity directory.");
|
?? throw new InvalidOperationException("Missing canonical entity directory.");
|
||||||
FieldInfo sidecars = typeof(LiveEntityRuntime).GetField(
|
FieldInfo sidecars = typeof(LiveEntityRuntime).GetField(
|
||||||
"_activeRecords",
|
"_projections",
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic)
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
||||||
?? throw new InvalidOperationException("Missing App projection sidecar store.");
|
?? throw new InvalidOperationException("Missing App projection sidecar store.");
|
||||||
|
|
||||||
Assert.Equal(typeof(RuntimeEntityDirectory), directory.FieldType);
|
Assert.Equal(typeof(RuntimeEntityDirectory), directory.FieldType);
|
||||||
Assert.Equal(typeof(RuntimeEntityRecord), sidecars.FieldType.GetGenericArguments()[0]);
|
Assert.Equal(typeof(LiveEntityProjectionStore), sidecars.FieldType);
|
||||||
Assert.Equal(typeof(LiveEntityRecord), sidecars.FieldType.GetGenericArguments()[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppCompatibilityLookup_IsNotASecondGuidDictionary()
|
public void AppProjectionOwners_UseExactRuntimeKeysAndNoGuidDictionary()
|
||||||
{
|
{
|
||||||
FieldInfo compatibilityView = typeof(LiveEntityRuntime).GetField(
|
FieldInfo[] runtimeFields = typeof(LiveEntityRuntime).GetFields(
|
||||||
"_recordsByGuid",
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic)
|
Assert.DoesNotContain(runtimeFields, IsGuidDictionary);
|
||||||
?? throw new InvalidOperationException("Missing migration compatibility view.");
|
|
||||||
|
|
||||||
Assert.False(compatibilityView.FieldType.IsGenericType);
|
FieldInfo[] storeFields = typeof(LiveEntityProjectionStore).GetFields(
|
||||||
Assert.DoesNotContain(
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
compatibilityView.FieldType.GetInterfaces(),
|
FieldInfo[] exactStores = storeFields
|
||||||
type => type.IsGenericType
|
.Where(field => IsDictionaryWithKey(field, typeof(RuntimeEntityKey)))
|
||||||
&& type.GetGenericTypeDefinition() == typeof(IDictionary<,>));
|
.ToArray();
|
||||||
|
Assert.NotEmpty(exactStores);
|
||||||
|
Assert.All(
|
||||||
|
exactStores,
|
||||||
|
field => Assert.Equal(
|
||||||
|
typeof(LiveEntityRecord),
|
||||||
|
field.FieldType.GetGenericArguments()[1]));
|
||||||
|
Assert.DoesNotContain(storeFields, IsGuidDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MaterializedPresentationWorksets_AreExactKeyed()
|
||||||
|
{
|
||||||
|
AssertExactKeyFields(
|
||||||
|
typeof(LiveEntityPresentationController),
|
||||||
|
"_readyOwners",
|
||||||
|
"_suspendedShadowOwners",
|
||||||
|
"_activePlacementOwners");
|
||||||
|
AssertExactKeyFields(typeof(RemoteTeleportController), "_pending");
|
||||||
|
AssertExactKeyFields(typeof(LiveRenderProjectionJournal), "_byKey");
|
||||||
|
AssertExactKeyFields(
|
||||||
|
typeof(EntityEffectController),
|
||||||
|
"_liveProfiles",
|
||||||
|
"_readyLiveOwners");
|
||||||
|
AssertExactKeyFields(
|
||||||
|
typeof(LiveEntityLightController),
|
||||||
|
"_trackedOwners",
|
||||||
|
"_presentOwners");
|
||||||
|
AssertExactKeyFields(
|
||||||
|
typeof(EquippedChildRenderController),
|
||||||
|
"_attachedByChild",
|
||||||
|
"_pendingUnparentByChild",
|
||||||
|
"_pendingOrdinaryRemovalByRoot",
|
||||||
|
"_pendingDetachedRemovalByChild",
|
||||||
|
"_pendingReparentRemovalByChild",
|
||||||
|
"_pendingPoseLossRemovalByChild",
|
||||||
|
"_pendingOrphanRemovalByChild");
|
||||||
|
AssertExactKeyFields(typeof(LiveEntityAnimationScheduler), "_schedules");
|
||||||
|
AssertExactKeyFields(typeof(EntitySpawnAdapter), "_ownersByKey");
|
||||||
|
AssertExactKeyFields(
|
||||||
|
typeof(LiveEntityLivenessTracker),
|
||||||
|
"_deadlines",
|
||||||
|
"_present");
|
||||||
|
AssertExactKeyFields(
|
||||||
|
typeof(RemoteMovementObservationTracker),
|
||||||
|
"_lastMove");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppAssembly_HasNoGuidKeyedLiveEntityRecordDictionary()
|
||||||
|
{
|
||||||
|
FieldInfo[] forbidden = typeof(LiveEntityRuntime).Assembly
|
||||||
|
.GetTypes()
|
||||||
|
.SelectMany(type => type.GetFields(
|
||||||
|
BindingFlags.Instance
|
||||||
|
| BindingFlags.Static
|
||||||
|
| BindingFlags.Public
|
||||||
|
| BindingFlags.NonPublic))
|
||||||
|
.Where(field =>
|
||||||
|
IsDictionaryWithKey(field, typeof(uint))
|
||||||
|
&& field.FieldType.GetGenericArguments()[1]
|
||||||
|
== typeof(LiveEntityRecord))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Empty(forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExactOwners_HaveNoRetainedGuidIndex()
|
||||||
|
{
|
||||||
|
Type[] exactOwnerTypes =
|
||||||
|
[
|
||||||
|
typeof(LiveEntityProjectionStore),
|
||||||
|
typeof(LiveEntityPresentationController),
|
||||||
|
typeof(RemoteTeleportController),
|
||||||
|
typeof(LiveRenderProjectionJournal),
|
||||||
|
typeof(LiveEntityLightController),
|
||||||
|
typeof(EquippedChildRenderController),
|
||||||
|
typeof(LiveEntityAnimationScheduler),
|
||||||
|
typeof(EntitySpawnAdapter),
|
||||||
|
typeof(LiveEntityLivenessTracker),
|
||||||
|
typeof(RemoteMovementObservationTracker),
|
||||||
|
];
|
||||||
|
|
||||||
|
FieldInfo[] forbidden = exactOwnerTypes
|
||||||
|
.SelectMany(type => type.GetFields(
|
||||||
|
BindingFlags.Instance
|
||||||
|
| BindingFlags.NonPublic))
|
||||||
|
.Where(field =>
|
||||||
|
field.Name.Contains("Guid", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Empty(forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -73,4 +168,42 @@ public sealed class RuntimeEntityOwnershipTests
|
||||||
|| ns?.StartsWith("Silk.NET", StringComparison.Ordinal) == true
|
|| ns?.StartsWith("Silk.NET", StringComparison.Ordinal) == true
|
||||||
|| ns?.StartsWith("ImGuiNET", StringComparison.Ordinal) == true;
|
|| ns?.StartsWith("ImGuiNET", StringComparison.Ordinal) == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsGuidDictionary(FieldInfo field) =>
|
||||||
|
IsDictionaryWithKey(field, typeof(uint));
|
||||||
|
|
||||||
|
private static void AssertExactKeyFields(
|
||||||
|
Type owner,
|
||||||
|
params string[] fieldNames)
|
||||||
|
{
|
||||||
|
foreach (string fieldName in fieldNames)
|
||||||
|
{
|
||||||
|
FieldInfo field = owner.GetField(
|
||||||
|
fieldName,
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"{owner.Name}.{fieldName} is missing.");
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
IsCollectionWithKey(field, typeof(RuntimeEntityKey)),
|
||||||
|
$"{owner.Name}.{fieldName} must be keyed by RuntimeEntityKey, "
|
||||||
|
+ $"but was {field.FieldType}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCollectionWithKey(FieldInfo field, Type keyType)
|
||||||
|
{
|
||||||
|
if (!field.FieldType.IsGenericType)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Type generic = field.FieldType.GetGenericTypeDefinition();
|
||||||
|
return (generic == typeof(Dictionary<,>)
|
||||||
|
|| generic == typeof(HashSet<>))
|
||||||
|
&& field.FieldType.GetGenericArguments()[0] == keyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDictionaryWithKey(FieldInfo field, Type keyType) =>
|
||||||
|
field.FieldType.IsGenericType
|
||||||
|
&& field.FieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>)
|
||||||
|
&& field.FieldType.GetGenericArguments()[0] == keyType;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue