feat(app): project canonical runtime placements
This commit is contained in:
parent
ef43667872
commit
74c9b155bd
5 changed files with 1395 additions and 0 deletions
|
|
@ -5,6 +5,7 @@ using AcDream.Core.Physics;
|
|||
using AcDream.Core.World;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
|
|
@ -386,6 +387,15 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
private readonly Dictionary<RuntimeEntityKey, ILiveEntityAnimationRuntime> _spatialAnimations = new();
|
||||
private readonly List<RuntimeEntityRecord> _spatialRootCanonicalScratch = new();
|
||||
private readonly List<RuntimeEntityRecord> _spatialRemoteCanonicalScratch = new();
|
||||
// Runtime placement receipts are presentation observations of an already
|
||||
// committed canonical SetPosition transaction. GpuWorldState still emits
|
||||
// its ordinary visibility callback while that observation rebuckets or
|
||||
// withdraws a sidecar, so pin the exact incarnation while the callback is
|
||||
// in flight. A depth (rather than a bool) preserves nested/re-entrant
|
||||
// projection mutations without ever turning graphical visibility into a
|
||||
// Runtime physics/workset mutation.
|
||||
private readonly Dictionary<RuntimeEntityKey, int>
|
||||
_presentationOnlySpatialMutationDepth = new();
|
||||
private bool _isClearing;
|
||||
private bool _sessionClearPendingFinalization;
|
||||
private bool _isRegisteringResources;
|
||||
|
|
@ -446,6 +456,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
_projections.VisibleRecords;
|
||||
internal IReadOnlyCollection<RuntimeEntityRecord> CanonicalRecords =>
|
||||
_directory.ActiveRecords;
|
||||
internal ulong SessionLifetimeVersion => _directory.SessionLifetimeVersion;
|
||||
internal RuntimePhysicsState Physics => _physics;
|
||||
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _directory.Snapshots;
|
||||
internal int AnimationRuntimeCount
|
||||
|
|
@ -863,6 +874,269 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies one canonical Runtime placement receipt to the graphical
|
||||
/// sidecar only. Runtime has already committed identity, position,
|
||||
/// collision residence, object-clock state, and simulation worksets.
|
||||
/// </summary>
|
||||
internal bool TryApplyRuntimePlacementProjection(
|
||||
in RuntimePlacementProjectionSnapshot projection)
|
||||
{
|
||||
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
|
||||
{
|
||||
// Discard cancels only an unacknowledged observation. If its Place
|
||||
// was already projected, retail keeps that last committed frame
|
||||
// visible until a later canonical Place or Withdraw supersedes it.
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimePlacementProjectionToken token = projection.Token;
|
||||
if (!TryGetRuntimePlacementProjectionRecord(
|
||||
token,
|
||||
requirePlacementVersions:
|
||||
projection.Kind is RuntimePlacementProjectionKind.Place,
|
||||
out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (projection.Kind is RuntimePlacementProjectionKind.Place
|
||||
&& !_spatial.IsLoaded(
|
||||
(token.ExactCellId & 0xFFFF0000u) | 0xFFFFu))
|
||||
{
|
||||
// A canonical SetPosition receipt is not permission to create a
|
||||
// pending graphical bucket. Keep the FIFO head unacknowledged
|
||||
// until the destination backend exists, otherwise GpuWorldState's
|
||||
// later pending-drain edge escapes this exact receipt transaction.
|
||||
return false;
|
||||
}
|
||||
|
||||
return projection.Kind switch
|
||||
{
|
||||
RuntimePlacementProjectionKind.Place =>
|
||||
TryApplyRuntimePlacementPlace(in projection, record, entity),
|
||||
RuntimePlacementProjectionKind.Withdraw =>
|
||||
TryApplyRuntimePlacementWithdrawal(token, record, entity),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private bool TryApplyRuntimePlacementPlace(
|
||||
in RuntimePlacementProjectionSnapshot projection,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity)
|
||||
{
|
||||
RuntimePlacementProjectionToken token = projection.Token;
|
||||
RuntimeEntityKey key = token.Entity;
|
||||
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
||||
|
||||
entity.SetPosition(projection.WorldPosition);
|
||||
entity.Rotation = projection.Orientation;
|
||||
entity.ParentCellId = token.ExactCellId;
|
||||
entity.EffectCellId = token.ExactCellId;
|
||||
record.IsSpatiallyProjected = true;
|
||||
|
||||
Exception? spatialNotificationFailure = null;
|
||||
uint priorRebucketingGuid = _rebucketingGuid;
|
||||
_rebucketingGuid = record.ServerGuid;
|
||||
BeginPresentationOnlySpatialMutation(key);
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_spatial.RebucketLiveEntity(key, entity, token.ExactCellId);
|
||||
}
|
||||
catch (AggregateException error)
|
||||
{
|
||||
spatialNotificationFailure = error;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndPresentationOnlySpatialMutation(key);
|
||||
_rebucketingGuid = priorRebucketingGuid;
|
||||
}
|
||||
|
||||
if (!IsCurrentProjectionOperation(
|
||||
record.ServerGuid,
|
||||
record,
|
||||
projectionOperation)
|
||||
|| !TryGetRuntimePlacementProjectionRecord(
|
||||
token,
|
||||
requirePlacementVersions: true,
|
||||
out LiveEntityRecord? current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|| !ReferenceEquals(current.WorldEntity, entity))
|
||||
{
|
||||
ThrowAfterCommittedProjectionChange(
|
||||
record.ServerGuid,
|
||||
spatialNotificationFailure,
|
||||
runtimeNotificationFailure: null);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool visible = _spatial.IsLiveEntityProjectionResident(key);
|
||||
record.IsSpatiallyVisible = visible;
|
||||
RefreshSpatialPresentationIndexes(record);
|
||||
RefreshPresentation(record);
|
||||
|
||||
if (!IsCurrentProjectionOperation(
|
||||
record.ServerGuid,
|
||||
record,
|
||||
projectionOperation))
|
||||
{
|
||||
ThrowAfterCommittedProjectionChange(
|
||||
record.ServerGuid,
|
||||
spatialNotificationFailure,
|
||||
runtimeNotificationFailure: null);
|
||||
return false;
|
||||
}
|
||||
|
||||
ThrowAfterCommittedProjectionChange(
|
||||
record.ServerGuid,
|
||||
spatialNotificationFailure,
|
||||
runtimeNotificationFailure: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryApplyRuntimePlacementWithdrawal(
|
||||
in RuntimePlacementProjectionToken token,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity)
|
||||
{
|
||||
RuntimeEntityKey key = token.Entity;
|
||||
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
||||
record.IsSpatiallyProjected = false;
|
||||
|
||||
Exception? spatialNotificationFailure = null;
|
||||
uint priorRebucketingGuid = _rebucketingGuid;
|
||||
_rebucketingGuid = record.ServerGuid;
|
||||
BeginPresentationOnlySpatialMutation(key);
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_spatial.RemoveLiveEntityProjection(entity);
|
||||
}
|
||||
catch (AggregateException error)
|
||||
{
|
||||
spatialNotificationFailure = error;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndPresentationOnlySpatialMutation(key);
|
||||
_rebucketingGuid = priorRebucketingGuid;
|
||||
}
|
||||
|
||||
if (!IsCurrentProjectionOperation(
|
||||
record.ServerGuid,
|
||||
record,
|
||||
projectionOperation)
|
||||
|| !TryGetRuntimePlacementProjectionRecord(
|
||||
token,
|
||||
requirePlacementVersions: false,
|
||||
out LiveEntityRecord? current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|| !ReferenceEquals(current.WorldEntity, entity))
|
||||
{
|
||||
ThrowAfterCommittedProjectionChange(
|
||||
record.ServerGuid,
|
||||
spatialNotificationFailure,
|
||||
runtimeNotificationFailure: null);
|
||||
return false;
|
||||
}
|
||||
|
||||
record.IsSpatiallyVisible = false;
|
||||
RefreshSpatialPresentationIndexes(record);
|
||||
RefreshPresentation(record);
|
||||
|
||||
if (!IsCurrentProjectionOperation(
|
||||
record.ServerGuid,
|
||||
record,
|
||||
projectionOperation))
|
||||
{
|
||||
ThrowAfterCommittedProjectionChange(
|
||||
record.ServerGuid,
|
||||
spatialNotificationFailure,
|
||||
runtimeNotificationFailure: null);
|
||||
return false;
|
||||
}
|
||||
|
||||
ThrowAfterCommittedProjectionChange(
|
||||
record.ServerGuid,
|
||||
spatialNotificationFailure,
|
||||
runtimeNotificationFailure: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetRuntimePlacementProjectionRecord(
|
||||
in RuntimePlacementProjectionToken token,
|
||||
bool requirePlacementVersions,
|
||||
out LiveEntityRecord record)
|
||||
{
|
||||
if (!token.IsValid
|
||||
|| token.SessionLifetimeVersion != _directory.SessionLifetimeVersion
|
||||
|| !_projections.TryGet(token.Entity, out record!)
|
||||
|| !_directory.IsCurrent(record.Canonical)
|
||||
|| record.Canonical.Key != token.Entity
|
||||
|| RequireProjectionKey(record) != token.Entity
|
||||
|| !IsValidPortalPlacementAuthority(token))
|
||||
{
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requirePlacementVersions
|
||||
&& (record.Canonical.PositionAuthorityVersion
|
||||
!= token.PositionAuthorityVersion
|
||||
|| record.Canonical.SpatialAuthorityVersion
|
||||
!= token.SpatialAuthorityVersion
|
||||
|| record.Canonical.PlacementCommitVersion
|
||||
!= token.PlacementCommitVersion
|
||||
|| record.Canonical.FullCellId != token.ExactCellId))
|
||||
{
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsValidPortalPlacementAuthority(
|
||||
in RuntimePlacementProjectionToken token)
|
||||
{
|
||||
RuntimePortalPlacementAuthority portal = token.Portal;
|
||||
if (!portal.Present)
|
||||
return portal.IsEmpty;
|
||||
|
||||
return portal.IsValid
|
||||
&& portal.Projection.DestinationCell == token.ExactCellId;
|
||||
}
|
||||
|
||||
private void BeginPresentationOnlySpatialMutation(RuntimeEntityKey key)
|
||||
{
|
||||
_presentationOnlySpatialMutationDepth.TryGetValue(key, out int depth);
|
||||
_presentationOnlySpatialMutationDepth[key] = checked(depth + 1);
|
||||
}
|
||||
|
||||
private void EndPresentationOnlySpatialMutation(RuntimeEntityKey key)
|
||||
{
|
||||
if (!_presentationOnlySpatialMutationDepth.TryGetValue(
|
||||
key,
|
||||
out int depth)
|
||||
|| depth <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Presentation-only spatial mutation depth was not balanced.");
|
||||
}
|
||||
|
||||
if (depth == 1)
|
||||
_presentationOnlySpatialMutationDepth.Remove(key);
|
||||
else
|
||||
_presentationOnlySpatialMutationDepth[key] = depth - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes only the render-bucket reference. The logical record and every
|
||||
/// create-time resource remain alive for later projection/rebucketing.
|
||||
|
|
@ -2588,6 +2862,33 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
|
||||
_physics.AcknowledgeSpatialProjection(record.Canonical, spatial);
|
||||
|
||||
RefreshSpatialPresentationIndexes(record, current, spatial, key);
|
||||
}
|
||||
|
||||
private void RefreshSpatialPresentationIndexes(LiveEntityRecord record)
|
||||
{
|
||||
bool current = IsCurrentRecord(record);
|
||||
bool spatial = current && HasSpatialRuntimeProjection(record);
|
||||
if (record.ProjectionKey is not { } key)
|
||||
{
|
||||
if (record.WorldEntity is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Materialized live entity 0x{record.ServerGuid:X8}/{record.Generation} has no exact projection key.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshSpatialPresentationIndexes(record, current, spatial, key);
|
||||
}
|
||||
|
||||
private void RefreshSpatialPresentationIndexes(
|
||||
LiveEntityRecord record,
|
||||
bool current,
|
||||
bool spatial,
|
||||
RuntimeEntityKey key)
|
||||
{
|
||||
|
||||
if (record.WorldEntity is not null)
|
||||
{
|
||||
if (spatial && record.AnimationRuntime is { } animation)
|
||||
|
|
@ -2641,6 +2942,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
bool wasVisible = record.IsSpatiallyVisible;
|
||||
if (RequireProjectionKey(record) != key)
|
||||
return;
|
||||
if (_presentationOnlySpatialMutationDepth.ContainsKey(key))
|
||||
{
|
||||
record.IsSpatiallyVisible = visible;
|
||||
RefreshSpatialPresentationIndexes(record);
|
||||
RefreshPresentation(record);
|
||||
return;
|
||||
}
|
||||
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
|
||||
record.IsSpatiallyVisible = visible;
|
||||
bool isOrdinaryRoot = record.ProjectionKind is LiveEntityProjectionKind.World
|
||||
|
|
|
|||
175
src/AcDream.App/World/RuntimePlacementPresentationSink.cs
Normal file
175
src/AcDream.App/World/RuntimePlacementPresentationSink.cs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.World;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Graphical projection sink for canonical Runtime SetPosition receipts. It
|
||||
/// owns only App-facing world, effect-pose, cached-local-shadow, selection,
|
||||
/// and renderer/VFX visibility projections. Runtime physics, shadows, body
|
||||
/// state, clocks, and worksets were committed before this sink is invoked.
|
||||
///
|
||||
/// This adapter deliberately owns no subscription: production composition
|
||||
/// activates the shared observer only after both graphical and no-window hosts
|
||||
/// implement the same presentation-only contract.
|
||||
/// </summary>
|
||||
internal sealed class RuntimePlacementPresentationSink
|
||||
: IRuntimePlacementProjectionSink
|
||||
{
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly RuntimeWorldTransitState _transit;
|
||||
private readonly WorldGameState _worldState;
|
||||
private readonly WorldEvents _worldEvents;
|
||||
private readonly EntityEffectPoseRegistry _effectPoses;
|
||||
private readonly LocalPlayerShadowState _localPlayerShadow;
|
||||
private readonly Func<uint> _localPlayerGuid;
|
||||
private readonly Action<uint> _clearSelectionForUnavailableEntity;
|
||||
private readonly Action<LiveEntityRecord, bool>[] _visibilitySinks;
|
||||
|
||||
public RuntimePlacementPresentationSink(
|
||||
LiveEntityRuntime liveEntities,
|
||||
RuntimeWorldTransitState transit,
|
||||
WorldGameState worldState,
|
||||
WorldEvents worldEvents,
|
||||
EntityEffectPoseRegistry effectPoses,
|
||||
LocalPlayerShadowState localPlayerShadow,
|
||||
Func<uint> localPlayerGuid,
|
||||
Action<uint> clearSelectionForUnavailableEntity,
|
||||
IEnumerable<Action<LiveEntityRecord, bool>>? visibilitySinks = null)
|
||||
{
|
||||
_liveEntities = liveEntities
|
||||
?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
|
||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||
_worldEvents = worldEvents ?? throw new ArgumentNullException(nameof(worldEvents));
|
||||
_effectPoses = effectPoses
|
||||
?? throw new ArgumentNullException(nameof(effectPoses));
|
||||
_localPlayerShadow = localPlayerShadow
|
||||
?? throw new ArgumentNullException(nameof(localPlayerShadow));
|
||||
_localPlayerGuid = localPlayerGuid
|
||||
?? throw new ArgumentNullException(nameof(localPlayerGuid));
|
||||
_clearSelectionForUnavailableEntity = clearSelectionForUnavailableEntity
|
||||
?? throw new ArgumentNullException(
|
||||
nameof(clearSelectionForUnavailableEntity));
|
||||
_visibilitySinks = visibilitySinks?.ToArray()
|
||||
?? Array.Empty<Action<LiveEntityRecord, bool>>();
|
||||
if (_visibilitySinks.Any(static sink => sink is null))
|
||||
throw new ArgumentException(
|
||||
"Presentation visibility sinks cannot contain null.",
|
||||
nameof(visibilitySinks));
|
||||
}
|
||||
|
||||
public bool TryApply(in RuntimePlacementProjectionSnapshot projection)
|
||||
{
|
||||
if (projection.Kind is RuntimePlacementProjectionKind.Place
|
||||
&& !_transit.IsCurrentPlacementAuthority(
|
||||
projection.Token.Portal,
|
||||
projection.Token.ExactCellId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_liveEntities.TryApplyRuntimePlacementProjection(in projection))
|
||||
return false;
|
||||
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
|
||||
return true;
|
||||
if (!_liveEntities.TryGetRecord(
|
||||
projection.Token.Entity,
|
||||
out LiveEntityRecord record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return projection.Kind switch
|
||||
{
|
||||
RuntimePlacementProjectionKind.Place =>
|
||||
TryPublishPlace(record, entity),
|
||||
RuntimePlacementProjectionKind.Withdraw =>
|
||||
TryPublishWithdrawal(record, entity),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private bool TryPublishPlace(LiveEntityRecord record, WorldEntity entity)
|
||||
{
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
|
||||
WorldEntitySnapshot snapshot = Snapshot(entity);
|
||||
_worldState.Add(snapshot);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
_worldEvents.UpsertCurrent(snapshot);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
_effectPoses.PublishMeshRefs(entity);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
|
||||
if (record.ServerGuid == _localPlayerGuid())
|
||||
{
|
||||
_localPlayerShadow.Set(
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
record.FullCellId);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _visibilitySinks.Length; i++)
|
||||
{
|
||||
_visibilitySinks[i](record, true);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryPublishWithdrawal(
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity)
|
||||
{
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < _visibilitySinks.Length; i++)
|
||||
{
|
||||
_visibilitySinks[i](record, false);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
}
|
||||
|
||||
_worldState.RemoveById(entity.Id);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
_worldEvents.ForgetEntity(entity.Id);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
_effectPoses.Remove(entity.Id);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
if (record.ServerGuid == _localPlayerGuid())
|
||||
_localPlayerShadow.Clear();
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
_clearSelectionForUnavailableEntity(record.ServerGuid);
|
||||
return IsCurrent(record, entity);
|
||||
}
|
||||
|
||||
private bool IsCurrent(LiveEntityRecord record, WorldEntity entity) =>
|
||||
_liveEntities.TryGetRecord(
|
||||
record.ProjectionKey!.Value,
|
||||
out LiveEntityRecord current)
|
||||
&& ReferenceEquals(current, record)
|
||||
&& ReferenceEquals(current.WorldEntity, entity);
|
||||
|
||||
private static WorldEntitySnapshot Snapshot(WorldEntity entity) => new(
|
||||
entity.Id,
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
entity.Position,
|
||||
entity.Rotation);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Globalization;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
namespace AcDream.Runtime.World;
|
||||
|
||||
|
|
@ -247,6 +248,32 @@ public sealed class RuntimeWorldTransitState
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the portal suffix carried by a canonical SetPosition
|
||||
/// projection receipt against this instance's live transit ownership.
|
||||
/// Shape equality alone is insufficient: a superseded, cancelled, or
|
||||
/// completed host token can remain structurally valid after it has lost
|
||||
/// authority to reveal the destination.
|
||||
/// </summary>
|
||||
public bool IsCurrentPlacementAuthority(
|
||||
in RuntimePortalPlacementAuthority authority,
|
||||
uint exactCellId)
|
||||
{
|
||||
if (!authority.Present)
|
||||
return authority.IsEmpty;
|
||||
|
||||
return authority.IsValid
|
||||
&& authority.Projection.DestinationCell == exactCellId
|
||||
&& IsCurrentPortalDestination(
|
||||
authority.RevealGeneration,
|
||||
authority.TeleportSequence,
|
||||
exactCellId)
|
||||
&& TryGetHostProjection(
|
||||
authority.Projection,
|
||||
out RuntimeWorldHostProjectionSnapshot host)
|
||||
&& !host.IsSuperseding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the graphical reservation-release callback as due before the
|
||||
/// host executes it. A thrown callback therefore leaves an exact pending
|
||||
|
|
|
|||
|
|
@ -0,0 +1,813 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
public sealed class RuntimePlacementPresentationSinkTests
|
||||
{
|
||||
private const uint SourceCell = 0x01010001u;
|
||||
private const uint DestinationCell = 0x01020001u;
|
||||
private const uint Guid = 0x7000A101u;
|
||||
|
||||
[Fact]
|
||||
public void Place_ReframesAndRebucketsExactSidecarWithoutMutatingRuntimePhysics()
|
||||
{
|
||||
Fixture fixture = Fixture.Create(twoLandblocks: true);
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
|
||||
|
||||
// Model the already-committed canonical SetPosition edge whose receipt
|
||||
// the dormant graphical observer is now projecting.
|
||||
record.FullCellId = DestinationCell;
|
||||
record.CanonicalLandblockId =
|
||||
(DestinationCell & 0xFFFF0000u) | 0xFFFFu;
|
||||
record.Canonical.AdvancePlacementCommit();
|
||||
RuntimePlacementProjectionSnapshot place = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(44f, 55f, 66f),
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f));
|
||||
|
||||
RuntimeOwnershipSnapshot before = RuntimeOwnershipSnapshot.Capture(
|
||||
fixture.Runtime,
|
||||
record);
|
||||
int genericVisibilityCount = 0;
|
||||
fixture.Runtime.ProjectionVisibilityChanged += (_, _) =>
|
||||
genericVisibilityCount++;
|
||||
|
||||
Assert.True(fixture.Sink.TryApply(in place));
|
||||
|
||||
Assert.Equal(place.WorldPosition, entity.Position);
|
||||
Assert.Equal(place.Orientation, entity.Rotation);
|
||||
Assert.Equal(DestinationCell, entity.ParentCellId);
|
||||
Assert.True(record.IsSpatiallyProjected);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
Assert.Contains(record, fixture.Runtime.VisibleRecords);
|
||||
Assert.True(fixture.Spatial.IsLiveEntityProjectionResident(
|
||||
record.ProjectionKey!.Value));
|
||||
Assert.Equal(before, RuntimeOwnershipSnapshot.Capture(
|
||||
fixture.Runtime,
|
||||
record));
|
||||
Assert.Equal(0, genericVisibilityCount);
|
||||
Assert.Equal(
|
||||
place.WorldPosition,
|
||||
Assert.Single(fixture.WorldState.Entities).Position);
|
||||
Assert.Equal((record, true), Assert.Single(fixture.Visibility));
|
||||
Assert.Equal(
|
||||
new LocalPlayerShadowState.Snapshot(
|
||||
place.WorldPosition,
|
||||
place.Orientation,
|
||||
DestinationCell),
|
||||
fixture.LocalShadow.Current);
|
||||
|
||||
fixture.Spatial.RemoveLandblock(SourceCell | 0xFFFFu);
|
||||
Assert.True(fixture.Spatial.IsLiveEntityProjectionResident(
|
||||
record.ProjectionKey.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdraw_RemovesOnlyPresentationAndRetainsLogicalRuntimeOwnership()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
RuntimePlacementProjectionSnapshot withdraw = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Withdraw,
|
||||
record.WorldEntity!.Position,
|
||||
record.WorldEntity.Rotation);
|
||||
RuntimeOwnershipSnapshot before = RuntimeOwnershipSnapshot.Capture(
|
||||
fixture.Runtime,
|
||||
record);
|
||||
int genericVisibilityCount = 0;
|
||||
fixture.Runtime.ProjectionVisibilityChanged += (_, _) =>
|
||||
genericVisibilityCount++;
|
||||
|
||||
Assert.True(fixture.Sink.TryApply(in withdraw));
|
||||
|
||||
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord current));
|
||||
Assert.Same(record, current);
|
||||
Assert.NotNull(record.WorldEntity);
|
||||
Assert.True(record.ResourcesRegistered);
|
||||
Assert.False(record.IsSpatiallyProjected);
|
||||
Assert.False(record.IsSpatiallyVisible);
|
||||
Assert.DoesNotContain(record, fixture.Runtime.VisibleRecords);
|
||||
Assert.False(fixture.Spatial.IsLiveEntityProjectionResident(
|
||||
record.ProjectionKey!.Value));
|
||||
Assert.Equal(before, RuntimeOwnershipSnapshot.Capture(
|
||||
fixture.Runtime,
|
||||
record));
|
||||
Assert.Equal(0, genericVisibilityCount);
|
||||
Assert.Empty(fixture.WorldState.Entities);
|
||||
Assert.Equal(0, fixture.EffectPoses.Count);
|
||||
Assert.Null(fixture.LocalShadow.Current);
|
||||
Assert.Equal((record, false), Assert.Single(fixture.Visibility));
|
||||
Assert.Equal(Guid, Assert.Single(fixture.ClearedSelection));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Discard_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
RuntimePlacementProjectionSnapshot discard = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Discard,
|
||||
new Vector3(900f),
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1f)) with
|
||||
{
|
||||
Token = Placement(fixture, record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
Vector3.Zero,
|
||||
Quaternion.Identity).Token with
|
||||
{
|
||||
SessionLifetimeVersion = ulong.MaxValue,
|
||||
PositionAuthorityVersion = ulong.MaxValue,
|
||||
ExactCellId = 0xDEAD0001u,
|
||||
},
|
||||
};
|
||||
Vector3 priorPosition = entity.Position;
|
||||
Quaternion priorRotation = entity.Rotation;
|
||||
bool priorVisible = record.IsSpatiallyVisible;
|
||||
|
||||
Assert.True(fixture.Sink.TryApply(in discard));
|
||||
|
||||
Assert.Equal(priorPosition, entity.Position);
|
||||
Assert.Equal(priorRotation, entity.Rotation);
|
||||
Assert.Equal(priorVisible, record.IsSpatiallyVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Place_RejectsStaleCanonicalVersionsWithoutChangingSidecar()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
RuntimePlacementProjectionSnapshot stale = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(90f, 91f, 92f),
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitY, 1f));
|
||||
record.Canonical.AdvancePlacementCommit();
|
||||
Vector3 priorPosition = entity.Position;
|
||||
Quaternion priorRotation = entity.Rotation;
|
||||
|
||||
Assert.False(fixture.Sink.TryApply(in stale));
|
||||
|
||||
Assert.Equal(priorPosition, entity.Position);
|
||||
Assert.Equal(priorRotation, entity.Rotation);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Place_WithoutMaterializedSidecarRemainsPendingForRetry()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRegistrationResult registration = fixture.Runtime.RegisterLiveEntity(
|
||||
Spawn(Guid, 1, SourceCell));
|
||||
RuntimeEntityRecord canonical = Assert.IsType<RuntimeEntityRecord>(
|
||||
registration.Canonical);
|
||||
RuntimeEntityKey missingKey = new(0x60000042u, canonical.Incarnation);
|
||||
var token = new RuntimePlacementProjectionToken(
|
||||
Sequence: 1,
|
||||
Revision: 1,
|
||||
Entity: missingKey,
|
||||
PositionAuthorityVersion: canonical.PositionAuthorityVersion,
|
||||
SpatialAuthorityVersion: canonical.SpatialAuthorityVersion,
|
||||
PlacementCommitVersion: canonical.PlacementCommitVersion,
|
||||
SessionLifetimeVersion: fixture.Runtime.SessionLifetimeVersion,
|
||||
ExactCellId: canonical.FullCellId,
|
||||
CollisionGeneration: 1,
|
||||
Portal: default);
|
||||
var place = new RuntimePlacementProjectionSnapshot(
|
||||
token,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(1f, 2f, 3f),
|
||||
Quaternion.Identity,
|
||||
Vector3.Zero,
|
||||
InContact: false,
|
||||
OnWalkable: false);
|
||||
|
||||
Assert.False(fixture.Sink.TryApply(in place));
|
||||
Assert.Empty(fixture.Spatial.Entities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Place_WithoutLoadedDestinationBackendRemainsPendingAtPriorProjection()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
Vector3 priorPosition = entity.Position;
|
||||
record.FullCellId = DestinationCell;
|
||||
record.CanonicalLandblockId =
|
||||
(DestinationCell & 0xFFFF0000u) | 0xFFFFu;
|
||||
record.Canonical.AdvancePlacementCommit();
|
||||
RuntimePlacementProjectionSnapshot place = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(70f, 71f, 72f),
|
||||
Quaternion.Identity);
|
||||
|
||||
Assert.False(fixture.Sink.TryApply(in place));
|
||||
|
||||
Assert.Equal(priorPosition, entity.Position);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
Assert.True(fixture.Spatial.IsLiveEntityProjectionResident(
|
||||
record.ProjectionKey!.Value));
|
||||
Assert.Empty(fixture.Visibility);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Place_UsesExactIncarnationAndCannotMutateSameGuidReplacement()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord old = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
RuntimePlacementProjectionSnapshot stale = Placement(
|
||||
fixture,
|
||||
old,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(88f, 77f, 66f),
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 1.25f));
|
||||
|
||||
LiveEntityRecord replacement = fixture.Materialize(
|
||||
Spawn(Guid, 2, SourceCell));
|
||||
WorldEntity replacementEntity = replacement.WorldEntity!;
|
||||
Vector3 priorPosition = replacementEntity.Position;
|
||||
Quaternion priorRotation = replacementEntity.Rotation;
|
||||
|
||||
Assert.False(fixture.Sink.TryApply(in stale));
|
||||
|
||||
Assert.Equal(priorPosition, replacementEntity.Position);
|
||||
Assert.Equal(priorRotation, replacementEntity.Rotation);
|
||||
Assert.True(replacement.IsSpatiallyVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PortalPlace_RequiresExactCurrentTransitHostAndSequence()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
RuntimePlacementProjectionSnapshot ordinary = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(21f, 22f, 23f),
|
||||
Quaternion.Identity);
|
||||
RuntimePortalPlacementAuthority authority = fixture.BeginPortal(
|
||||
SourceCell,
|
||||
teleportSequence: 9);
|
||||
RuntimePlacementProjectionSnapshot current = ordinary with
|
||||
{
|
||||
Token = ordinary.Token with { Portal = authority },
|
||||
};
|
||||
|
||||
Assert.True(fixture.Sink.TryApply(in current));
|
||||
Assert.Equal(current.WorldPosition, record.WorldEntity!.Position);
|
||||
|
||||
Assert.True(fixture.Transit.BeginHostProjectionSupersession(
|
||||
authority.Projection));
|
||||
RuntimePlacementProjectionSnapshot superseded = current with
|
||||
{
|
||||
WorldPosition = new Vector3(80f, 81f, 82f),
|
||||
};
|
||||
Assert.False(fixture.Sink.TryApply(in superseded));
|
||||
Assert.Equal(current.WorldPosition, record.WorldEntity.Position);
|
||||
|
||||
RuntimePlacementProjectionSnapshot wrongSequence = current with
|
||||
{
|
||||
Token = current.Token with
|
||||
{
|
||||
Portal = authority with { TeleportSequence = 10 },
|
||||
},
|
||||
WorldPosition = new Vector3(90f, 91f, 92f),
|
||||
};
|
||||
Assert.False(fixture.Sink.TryApply(in wrongSequence));
|
||||
Assert.Equal(current.WorldPosition, record.WorldEntity.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlaceAndWithdraw_DoNotMutateRemoteBodyOrRuntimeOwnership()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
RemoteMotion remote = fixture.Runtime.GetOrCreateRemoteMotionRuntime(Guid);
|
||||
remote.Body.Position = new Vector3(4f, 5f, 6f);
|
||||
remote.Body.Orientation = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitY,
|
||||
0.4f);
|
||||
remote.Body.State = PhysicsStateFlags.Gravity
|
||||
| PhysicsStateFlags.ReportCollisions;
|
||||
remote.Body.TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact;
|
||||
remote.Body.InWorld = true;
|
||||
remote.Body.LastUpdateTime = 42.5;
|
||||
PhysicsBodySnapshot bodyBefore = PhysicsBodySnapshot.Capture(remote.Body);
|
||||
RuntimePhysicsOwnershipSnapshot runtimeBefore =
|
||||
fixture.Runtime.Physics.CaptureOwnership();
|
||||
RuntimePlacementProjectionSnapshot place = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Place,
|
||||
new Vector3(30f, 31f, 32f),
|
||||
Quaternion.Identity);
|
||||
|
||||
Assert.True(fixture.Sink.TryApply(in place));
|
||||
Assert.Equal(bodyBefore, PhysicsBodySnapshot.Capture(remote.Body));
|
||||
Assert.Equal(runtimeBefore, fixture.Runtime.Physics.CaptureOwnership());
|
||||
|
||||
RuntimePlacementProjectionSnapshot withdraw = place with
|
||||
{
|
||||
Kind = RuntimePlacementProjectionKind.Withdraw,
|
||||
};
|
||||
Assert.True(fixture.Sink.TryApply(in withdraw));
|
||||
Assert.Equal(bodyBefore, PhysicsBodySnapshot.Capture(remote.Body));
|
||||
Assert.Equal(runtimeBefore, fixture.Runtime.Physics.CaptureOwnership());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdraw_DoesNotMutateProjectileBodyShadowOrWorksetOwnership()
|
||||
{
|
||||
Fixture fixture = Fixture.Create();
|
||||
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
|
||||
PhysicsBody body = fixture.Runtime.GetOrCreatePhysicsBody(
|
||||
Guid,
|
||||
_ => new PhysicsBody());
|
||||
body.Position = new Vector3(7f, 8f, 9f);
|
||||
body.Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitX, 0.6f);
|
||||
body.State = PhysicsStateFlags.Missile
|
||||
| PhysicsStateFlags.ReportCollisions;
|
||||
body.TransientState = TransientStateFlags.Active;
|
||||
body.InWorld = true;
|
||||
body.LastUpdateTime = 99.25;
|
||||
fixture.Runtime.BindProjectileRuntime(
|
||||
Guid,
|
||||
body,
|
||||
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||
PhysicsBodySnapshot bodyBefore = PhysicsBodySnapshot.Capture(body);
|
||||
RuntimePhysicsOwnershipSnapshot runtimeBefore =
|
||||
fixture.Runtime.Physics.CaptureOwnership();
|
||||
RuntimePlacementProjectionSnapshot withdraw = Placement(
|
||||
fixture,
|
||||
record,
|
||||
RuntimePlacementProjectionKind.Withdraw,
|
||||
record.WorldEntity!.Position,
|
||||
record.WorldEntity.Rotation);
|
||||
|
||||
Assert.True(fixture.Sink.TryApply(in withdraw));
|
||||
|
||||
Assert.Equal(bodyBefore, PhysicsBodySnapshot.Capture(body));
|
||||
Assert.Equal(runtimeBefore, fixture.Runtime.Physics.CaptureOwnership());
|
||||
Assert.Same(body, record.ProjectileRuntime!.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedPresentationTail_RetriesSameReceiptIdempotently()
|
||||
{
|
||||
using var fixture = SubscriptionFixture.Create();
|
||||
fixture.VisibilityFailuresRemaining = 1;
|
||||
using var subscription = new RuntimePlacementProjectionSubscription(
|
||||
fixture.Lifetime.Placements,
|
||||
() => fixture.Generation,
|
||||
fixture.Sink);
|
||||
|
||||
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
|
||||
fixture.Record.Canonical,
|
||||
publishWithdrawal: true));
|
||||
|
||||
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
|
||||
Assert.Equal(1, fixture.Lifetime.Events.DispatchFailureCount);
|
||||
Assert.IsType<InvalidOperationException>(
|
||||
fixture.Lifetime.Events.LastDispatchFailure);
|
||||
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
|
||||
|
||||
Assert.True(subscription.RetryPending());
|
||||
|
||||
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
|
||||
Assert.False(fixture.Record.IsSpatiallyProjected);
|
||||
Assert.Empty(fixture.WorldState.Entities);
|
||||
Assert.Equal(0, fixture.EffectPoses.Count);
|
||||
Assert.Equal(2, fixture.Visibility.Count);
|
||||
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
|
||||
}
|
||||
|
||||
private static RuntimePlacementProjectionSnapshot Placement(
|
||||
Fixture fixture,
|
||||
LiveEntityRecord record,
|
||||
RuntimePlacementProjectionKind kind,
|
||||
Vector3 position,
|
||||
Quaternion orientation)
|
||||
{
|
||||
RuntimeEntityRecord canonical = record.Canonical;
|
||||
var token = new RuntimePlacementProjectionToken(
|
||||
Sequence: 1,
|
||||
Revision: 1,
|
||||
Entity: record.ProjectionKey!.Value,
|
||||
PositionAuthorityVersion: canonical.PositionAuthorityVersion,
|
||||
SpatialAuthorityVersion: canonical.SpatialAuthorityVersion,
|
||||
PlacementCommitVersion: canonical.PlacementCommitVersion,
|
||||
SessionLifetimeVersion: fixture.Runtime.SessionLifetimeVersion,
|
||||
ExactCellId: canonical.FullCellId,
|
||||
CollisionGeneration: 1,
|
||||
Portal: default);
|
||||
return new RuntimePlacementProjectionSnapshot(
|
||||
token,
|
||||
kind,
|
||||
position,
|
||||
orientation,
|
||||
CellLocalPosition: position,
|
||||
InContact: false,
|
||||
OnWalkable: false);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(
|
||||
uint guid,
|
||||
ushort instance,
|
||||
uint cell)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
Position: 1,
|
||||
Movement: 1,
|
||||
State: 1,
|
||||
Vector: 1,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 1,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 1,
|
||||
Instance: instance);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: 0x09000001u,
|
||||
SoundTableId: null,
|
||||
PhysicsScriptTableId: null,
|
||||
Parent: null,
|
||||
Children: null,
|
||||
Scale: null,
|
||||
Friction: null,
|
||||
Elasticity: null,
|
||||
Translucency: null,
|
||||
Velocity: null,
|
||||
Acceleration: null,
|
||||
AngularVelocity: null,
|
||||
DefaultScriptType: null,
|
||||
DefaultScriptIntensity: null,
|
||||
Timestamps: timestamps);
|
||||
return new WorldSession.EntitySpawn(
|
||||
guid,
|
||||
position,
|
||||
0x02000001u,
|
||||
Array.Empty<CreateObject.AnimPartChange>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
null,
|
||||
null,
|
||||
"fixture",
|
||||
null,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
InstanceSequence: instance,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private readonly record struct RuntimeOwnershipSnapshot(
|
||||
ulong PositionAuthorityVersion,
|
||||
ulong SpatialAuthorityVersion,
|
||||
ulong PlacementCommitVersion,
|
||||
ulong ObjectClockEpoch,
|
||||
bool ObjectClockIsActive,
|
||||
int SpatialRootCount,
|
||||
int SpatialRemoteCount,
|
||||
int SpatialProjectileCount,
|
||||
RuntimePhysicsOwnershipSnapshot Physics)
|
||||
{
|
||||
internal static RuntimeOwnershipSnapshot Capture(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record) => new(
|
||||
record.Canonical.PositionAuthorityVersion,
|
||||
record.Canonical.SpatialAuthorityVersion,
|
||||
record.Canonical.PlacementCommitVersion,
|
||||
record.ObjectClockEpoch,
|
||||
record.ObjectClock.IsActive,
|
||||
runtime.SpatialRootObjectCount,
|
||||
runtime.SpatialRemoteMotionRuntimeCount,
|
||||
runtime.SpatialProjectileRuntimeCount,
|
||||
runtime.Physics.CaptureOwnership());
|
||||
}
|
||||
|
||||
private readonly record struct PhysicsBodySnapshot(
|
||||
Vector3 Position,
|
||||
Quaternion Orientation,
|
||||
PhysicsStateFlags State,
|
||||
TransientStateFlags TransientState,
|
||||
bool InWorld,
|
||||
double LastUpdateTime)
|
||||
{
|
||||
internal static PhysicsBodySnapshot Capture(PhysicsBody body) => new(
|
||||
body.Position,
|
||||
body.Orientation,
|
||||
body.State,
|
||||
body.TransientState,
|
||||
body.InWorld,
|
||||
body.LastUpdateTime);
|
||||
}
|
||||
|
||||
private sealed class Fixture
|
||||
{
|
||||
private Fixture(
|
||||
GpuWorldState spatial,
|
||||
LiveEntityRuntime runtime,
|
||||
RuntimeWorldTransitState transit,
|
||||
WorldGameState worldState,
|
||||
WorldEvents worldEvents,
|
||||
EntityEffectPoseRegistry effectPoses,
|
||||
LocalPlayerShadowState localShadow)
|
||||
{
|
||||
Spatial = spatial;
|
||||
Runtime = runtime;
|
||||
Transit = transit;
|
||||
WorldState = worldState;
|
||||
WorldEvents = worldEvents;
|
||||
EffectPoses = effectPoses;
|
||||
LocalShadow = localShadow;
|
||||
Sink = new RuntimePlacementPresentationSink(
|
||||
runtime,
|
||||
transit,
|
||||
worldState,
|
||||
worldEvents,
|
||||
effectPoses,
|
||||
localShadow,
|
||||
() => Guid,
|
||||
ClearedSelection.Add,
|
||||
[
|
||||
(record, visible) =>
|
||||
{
|
||||
Visibility.Add((record, visible));
|
||||
if (VisibilityFailuresRemaining > 0)
|
||||
{
|
||||
VisibilityFailuresRemaining--;
|
||||
throw new InvalidOperationException(
|
||||
"fixture presentation failure");
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
internal GpuWorldState Spatial { get; }
|
||||
internal LiveEntityRuntime Runtime { get; }
|
||||
internal RuntimeWorldTransitState Transit { get; }
|
||||
internal WorldGameState WorldState { get; }
|
||||
internal WorldEvents WorldEvents { get; }
|
||||
internal EntityEffectPoseRegistry EffectPoses { get; }
|
||||
internal LocalPlayerShadowState LocalShadow { get; }
|
||||
internal List<(LiveEntityRecord Record, bool Visible)> Visibility { get; } = [];
|
||||
internal List<uint> ClearedSelection { get; } = [];
|
||||
internal int VisibilityFailuresRemaining { get; set; }
|
||||
internal RuntimePlacementPresentationSink Sink { get; }
|
||||
|
||||
internal static Fixture Create(bool twoLandblocks = false)
|
||||
{
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(SourceCell | 0xFFFFu));
|
||||
if (twoLandblocks)
|
||||
spatial.AddLandblock(EmptyLandblock(DestinationCell | 0xFFFFu));
|
||||
var resources = new RecordingResources();
|
||||
LiveEntityRuntime runtime = LiveEntityRuntimeFixture.Create(
|
||||
spatial,
|
||||
resources);
|
||||
return new Fixture(
|
||||
spatial,
|
||||
runtime,
|
||||
new RuntimeWorldTransitState(),
|
||||
new WorldGameState(),
|
||||
new WorldEvents(),
|
||||
new EntityEffectPoseRegistry(),
|
||||
new LocalPlayerShadowState());
|
||||
}
|
||||
|
||||
internal LiveEntityRecord Materialize(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(spawn);
|
||||
Assert.True(record.ResourcesRegistered);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
|
||||
entity.Id,
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
entity.Position,
|
||||
entity.Rotation);
|
||||
WorldState.Add(snapshot);
|
||||
WorldEvents.UpsertCurrent(snapshot);
|
||||
EffectPoses.PublishMeshRefs(entity);
|
||||
if (record.ServerGuid == Guid)
|
||||
{
|
||||
LocalShadow.Set(
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
record.FullCellId);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
internal RuntimePortalPlacementAuthority BeginPortal(
|
||||
uint cell,
|
||||
ushort teleportSequence)
|
||||
{
|
||||
Assert.True(Transit.TryQueueTeleportStart(teleportSequence));
|
||||
Assert.True(Transit.ActivateQueuedTeleport());
|
||||
Assert.True(Transit.OfferTeleportDestination(
|
||||
new RuntimeTeleportDestination(
|
||||
Guid,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 1,
|
||||
TeleportSequence: teleportSequence,
|
||||
ForcePositionSequence: 1,
|
||||
new Position(
|
||||
cell,
|
||||
new Vector3(1f, 2f, 3f),
|
||||
Quaternion.Identity)),
|
||||
teleportTimestampAdvanced: true));
|
||||
Assert.True(Transit.TryBeginPortalReveal(
|
||||
teleportSequence,
|
||||
cell,
|
||||
out long generation));
|
||||
Assert.True(Transit.TryRegisterHostProjection(
|
||||
generation,
|
||||
cell,
|
||||
out RuntimeWorldHostProjectionToken host));
|
||||
return new RuntimePortalPlacementAuthority(
|
||||
true,
|
||||
generation,
|
||||
teleportSequence,
|
||||
host);
|
||||
}
|
||||
|
||||
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
||||
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
||||
}
|
||||
|
||||
private sealed class SubscriptionFixture : IDisposable
|
||||
{
|
||||
private SubscriptionFixture(
|
||||
RuntimeEntityObjectLifetime lifetime,
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
RuntimePlacementPresentationSink sink,
|
||||
WorldGameState worldState,
|
||||
EntityEffectPoseRegistry effectPoses,
|
||||
List<(LiveEntityRecord Record, bool Visible)> visibility)
|
||||
{
|
||||
Lifetime = lifetime;
|
||||
Runtime = runtime;
|
||||
Record = record;
|
||||
Sink = sink;
|
||||
WorldState = worldState;
|
||||
EffectPoses = effectPoses;
|
||||
Visibility = visibility;
|
||||
}
|
||||
|
||||
internal RuntimeGenerationToken Generation { get; } = new(7UL);
|
||||
internal RuntimeEntityObjectLifetime Lifetime { get; }
|
||||
internal LiveEntityRuntime Runtime { get; }
|
||||
internal LiveEntityRecord Record { get; }
|
||||
internal RuntimePlacementPresentationSink Sink { get; }
|
||||
internal WorldGameState WorldState { get; }
|
||||
internal EntityEffectPoseRegistry EffectPoses { get; }
|
||||
internal List<(LiveEntityRecord Record, bool Visible)> Visibility { get; }
|
||||
internal int VisibilityFailuresRemaining { get; set; }
|
||||
|
||||
internal static SubscriptionFixture Create()
|
||||
{
|
||||
PhysicsEngine engine = FlatEngine();
|
||||
var lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||
RuntimeGenerationToken generation = new(7UL);
|
||||
lifetime.BindEventContext(() => generation, static () => 11UL);
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
SourceCell | 0xFFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
var runtime = new LiveEntityRuntime(
|
||||
spatial,
|
||||
new RecordingResources(),
|
||||
lifetime);
|
||||
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(
|
||||
Spawn(Guid, 1, SourceCell));
|
||||
PhysicsBody body = runtime.GetOrCreatePhysicsBody(
|
||||
Guid,
|
||||
_ => new PhysicsBody
|
||||
{
|
||||
Position = new Vector3(10f, 10f, 5f),
|
||||
Orientation = Quaternion.Identity,
|
||||
LastUpdateTime = 1d,
|
||||
State = PhysicsStateFlags.ReportCollisions,
|
||||
TransientState = TransientStateFlags.Active,
|
||||
});
|
||||
body.SnapToCell(
|
||||
SourceCell,
|
||||
body.Position,
|
||||
body.Position);
|
||||
|
||||
var worldState = new WorldGameState();
|
||||
var worldEvents = new WorldEvents();
|
||||
var effectPoses = new EntityEffectPoseRegistry();
|
||||
var localShadow = new LocalPlayerShadowState();
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
|
||||
entity.Id,
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
entity.Position,
|
||||
entity.Rotation);
|
||||
worldState.Add(snapshot);
|
||||
worldEvents.UpsertCurrent(snapshot);
|
||||
effectPoses.PublishMeshRefs(entity);
|
||||
localShadow.Set(entity.Position, entity.Rotation, record.FullCellId);
|
||||
var visibility = new List<(LiveEntityRecord Record, bool Visible)>();
|
||||
SubscriptionFixture? fixture = null;
|
||||
var sink = new RuntimePlacementPresentationSink(
|
||||
runtime,
|
||||
new RuntimeWorldTransitState(),
|
||||
worldState,
|
||||
worldEvents,
|
||||
effectPoses,
|
||||
localShadow,
|
||||
() => Guid,
|
||||
_ => { },
|
||||
[
|
||||
(candidate, visible) =>
|
||||
{
|
||||
visibility.Add((candidate, visible));
|
||||
if (fixture!.VisibilityFailuresRemaining > 0)
|
||||
{
|
||||
fixture.VisibilityFailuresRemaining--;
|
||||
throw new InvalidOperationException(
|
||||
"fixture presentation failure");
|
||||
}
|
||||
},
|
||||
]);
|
||||
fixture = new SubscriptionFixture(
|
||||
lifetime,
|
||||
runtime,
|
||||
record,
|
||||
sink,
|
||||
worldState,
|
||||
effectPoses,
|
||||
visibility);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Runtime.Clear();
|
||||
Lifetime.Dispose();
|
||||
}
|
||||
|
||||
private static PhysicsEngine FlatEngine()
|
||||
{
|
||||
var engine = new PhysicsEngine
|
||||
{
|
||||
DataCache = new PhysicsDataCache(),
|
||||
};
|
||||
engine.AddLandblock(
|
||||
SourceCell & 0xFFFF0000u,
|
||||
new TerrainSurface(new byte[81], new float[256]),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public void Register(WorldEntity entity) { }
|
||||
public void Unregister(WorldEntity entity) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime.Tests.World;
|
||||
|
|
@ -9,6 +10,77 @@ public sealed class RuntimeWorldTransitStateTests
|
|||
private const uint OutdoorCell = 0x11340021u;
|
||||
private const uint OtherCell = 0x3032001Cu;
|
||||
|
||||
[Fact]
|
||||
public void PlacementAuthority_RequiresCurrentPortalHostSequenceAndReveal()
|
||||
{
|
||||
var state = new RuntimeWorldTransitState();
|
||||
long generation = BeginPortal(state, OutdoorCell, sequence: 7);
|
||||
RuntimeWorldHostProjectionToken host =
|
||||
RegisterHost(state, generation, OutdoorCell);
|
||||
RuntimePortalPlacementAuthority authority = new(
|
||||
Present: true,
|
||||
RevealGeneration: generation,
|
||||
TeleportSequence: 7,
|
||||
Projection: host);
|
||||
|
||||
Assert.True(state.IsCurrentPlacementAuthority(
|
||||
authority,
|
||||
OutdoorCell));
|
||||
Assert.True(state.IsCurrentPlacementAuthority(default, OutdoorCell));
|
||||
Assert.False(state.IsCurrentPlacementAuthority(
|
||||
authority with { TeleportSequence = 8 },
|
||||
OutdoorCell));
|
||||
Assert.False(state.IsCurrentPlacementAuthority(
|
||||
authority,
|
||||
OtherCell));
|
||||
|
||||
Assert.True(state.BeginHostProjectionSupersession(host));
|
||||
Assert.False(state.IsCurrentPlacementAuthority(
|
||||
authority,
|
||||
OutdoorCell));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlacementAuthority_RejectsCancelledAndCompletedReveals()
|
||||
{
|
||||
var cancelled = new RuntimeWorldTransitState();
|
||||
long cancelledGeneration = BeginPortal(cancelled, OutdoorCell);
|
||||
RuntimeWorldHostProjectionToken cancelledHost =
|
||||
RegisterHost(cancelled, cancelledGeneration, OutdoorCell);
|
||||
RuntimePortalPlacementAuthority cancelledAuthority = new(
|
||||
true,
|
||||
cancelledGeneration,
|
||||
1,
|
||||
cancelledHost);
|
||||
Assert.True(cancelled.Cancel(cancelledGeneration));
|
||||
Assert.False(cancelled.IsCurrentPlacementAuthority(
|
||||
cancelledAuthority,
|
||||
OutdoorCell));
|
||||
|
||||
var completed = new RuntimeWorldTransitState();
|
||||
long completedGeneration = BeginPortal(completed, OutdoorCell);
|
||||
RuntimeWorldHostProjectionToken completedHost =
|
||||
RegisterHost(completed, completedGeneration, OutdoorCell);
|
||||
RuntimePortalPlacementAuthority completedAuthority = new(
|
||||
true,
|
||||
completedGeneration,
|
||||
1,
|
||||
completedHost);
|
||||
Assert.True(completed.AcknowledgeDestinationReadiness(
|
||||
Ready(completedGeneration, OutdoorCell)));
|
||||
Assert.True(completed.AcknowledgePortalMaterialized(
|
||||
completedGeneration,
|
||||
1,
|
||||
OutdoorCell));
|
||||
Assert.True(completed.AcknowledgeWorldViewportVisible(
|
||||
completedGeneration));
|
||||
Assert.True(completed.Complete(completedGeneration));
|
||||
|
||||
Assert.False(completed.IsCurrentPlacementAuthority(
|
||||
completedAuthority,
|
||||
OutdoorCell));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeginReveal_OwnsGenerationDestinationAndSimulationGate()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue