feat(runtime): seal dormant SetPosition evaluations

This commit is contained in:
Erik 2026-08-01 11:31:58 +02:00
parent 22651c823d
commit 99f867f053
16 changed files with 2298 additions and 62 deletions

View file

@ -179,6 +179,17 @@ public sealed class PropertyBundle
/// </summary>
public sealed class ClientObject
{
private uint? _houseOwnerId;
private uint? _monarchId;
private HouseRestrictionRecord? _restrictions;
/// <summary>
/// Synchronous table-internal notification for the qualities consulted by
/// retail's house-entry restriction gate. Multiple tables may retain the
/// same object during isolated evaluation fixtures; each receives the edge.
/// </summary>
internal event Action<ClientObject>? RestrictionAuthorityChanged;
public uint ObjectId { get; init; }
public uint WeenieClassId { get; set; } // "blueprint"
public string Name { get; set; } = "";
@ -273,7 +284,16 @@ public sealed class ClientObject
/// object (wire <c>WeenieHeaderFlag.Owner</c>, 0x02000000). Zero or a
/// match against the mover's own id admits regardless of the guest list.
/// </summary>
public uint? HouseOwnerId { get; set; }
public uint? HouseOwnerId
{
get => _houseOwnerId;
set
{
if (_houseOwnerId == value) return;
_houseOwnerId = value;
RestrictionAuthorityChanged?.Invoke(this);
}
}
/// <summary>
/// AP-129 (Campaign P Slice P4 review fix): retail <c>PublicWeenieDesc
/// ._monarch_iid</c> (wire <c>WeenieHeaderFlag.Monarch</c>, 0x40) — this
@ -281,7 +301,16 @@ public sealed class ClientObject
/// objects; a player's own value is what <c>RestrictionDB::IsAllowedIn</c>
/// compares against a house's <see cref="HouseRestrictionRecord.AllegianceMonarchId"/>.
/// </summary>
public uint? MonarchId { get; set; }
public uint? MonarchId
{
get => _monarchId;
set
{
if (_monarchId == value) return;
_monarchId = value;
RestrictionAuthorityChanged?.Invoke(this);
}
}
/// <summary>
/// AP-129 (Campaign P Slice P4 review fix): retail <c>PublicWeenieDesc
/// ._db</c> (<c>RestrictionDB*</c>) — the house's own guest/ban list.
@ -291,7 +320,16 @@ public sealed class ClientObject
/// object — acdream cannot distinguish the two, and both resolve to
/// the same retail-faithful "allow" default.
/// </summary>
public HouseRestrictionRecord? Restrictions { get; set; }
public HouseRestrictionRecord? Restrictions
{
get => _restrictions;
set
{
if (ReferenceEquals(_restrictions, value)) return;
_restrictions = value;
RestrictionAuthorityChanged?.Invoke(this);
}
}
public PropertyBundle Properties { get; } = new();
/// <summary>

View file

@ -116,11 +116,71 @@ public sealed class ClientObjectTable
private readonly ConcurrentDictionary<uint, Container> _containers = new();
private readonly Dictionary<uint, List<uint>> _containerIndex = new();
private readonly Dictionary<uint, List<uint>> _equipmentIndex = new();
private readonly HashSet<ClientObject> _restrictionObservedObjects =
new(ReferenceEqualityComparer.Instance);
// B-Drag: pre-move snapshots for optimistic inventory moves. itemId → (container, slot, equip) BEFORE
// the optimistic MoveItem; restored by RollbackMove on InventoryServerSaveFailed (0x00A0),
// cleared by ConfirmMove on the InventoryPutObjInContainer (0x0022) echo.
private readonly Dictionary<uint, (ClientObjectPlacement placement, int outstanding)> _pendingMoves = new();
private ulong _mutationRevision;
public ClientObjectTable()
{
// Keep one conservative authority over every object mutation that can
// affect physics entry restrictions. These handlers are registered
// before any consumer can subscribe, so re-entrant observers see the
// advanced revision before they can evaluate or publish a receipt.
ObjectAdded += _ => AdvanceMutationRevision();
ObjectMoved += _ => AdvanceMutationRevision();
ObjectRemoved += _ => AdvanceMutationRevision();
ObjectUpdated += _ => AdvanceMutationRevision();
Cleared += AdvanceMutationRevision;
}
/// <summary>
/// Monotonic authority for live object-table qualities used by physics,
/// including house ownership/restrictions and the mover's monarch.
/// </summary>
internal ulong MutationRevision => _mutationRevision;
private void AdvanceMutationRevision() =>
_mutationRevision = checked(_mutationRevision + 1UL);
private void RetainObject(ClientObject item)
{
if (_objects.TryGetValue(item.ObjectId, out ClientObject? prior)
&& !ReferenceEquals(prior, item))
{
UnbindRestrictionAuthority(prior);
}
_objects[item.ObjectId] = item;
BindRestrictionAuthority(item);
}
private void BindRestrictionAuthority(ClientObject item)
{
if (!_restrictionObservedObjects.Add(item)) return;
item.RestrictionAuthorityChanged += OnRestrictionAuthorityChanged;
}
private void UnbindRestrictionAuthority(ClientObject item)
{
if (!_restrictionObservedObjects.Remove(item)) return;
item.RestrictionAuthorityChanged -= OnRestrictionAuthorityChanged;
}
private void OnRestrictionAuthorityChanged(ClientObject item)
{
// Unbinding is synchronous, but keep the exact-reference check as a
// defensive lifetime gate against a stale/replaced object callback.
if (_objects.TryGetValue(item.ObjectId, out ClientObject? retained)
&& ReferenceEquals(retained, item))
{
AdvanceMutationRevision();
}
}
/// <summary>Fires when an object is first added to the session.</summary>
public event Action<ClientObject>? ObjectAdded;
@ -270,7 +330,7 @@ public sealed class ClientObjectTable
ClientObjectPlacement previous = prior is null
? default
: ClientObjectPlacement.From(prior);
_objects[item.ObjectId] = item;
RetainObject(item);
UpdateEquipmentIndex(item.ObjectId, previous, ClientObjectPlacement.From(item));
if (!existed) ObjectAdded?.Invoke(item);
else ObjectUpdated?.Invoke(item);
@ -615,6 +675,7 @@ public sealed class ClientObjectTable
bool notifyObjectRemoved)
{
if (!_objects.TryRemove(itemId, out var item)) return false;
UnbindRestrictionAuthority(item);
List<uint>? changedContainers = RemoveFromOtherContainerIndexes(
itemId,
exceptContainerId: 0u);
@ -690,7 +751,7 @@ public sealed class ClientObjectTable
if (!existed || item is null)
{
item = new ClientObject { ObjectId = guid };
_objects[guid] = item;
RetainObject(item);
}
foreach (var kv in incoming.Ints) item.Properties.Ints[kv.Key] = kv.Value;
foreach (var kv in incoming.Int64s) item.Properties.Int64s[kv.Key] = kv.Value;
@ -800,7 +861,7 @@ public sealed class ClientObjectTable
if (!existed || obj is null) // keep: satisfies nullable flow analysis
{
obj = new ClientObject { ObjectId = d.Guid };
_objects[d.Guid] = obj;
RetainObject(obj);
}
uint oldContainer = obj.ContainerId;
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
@ -876,7 +937,7 @@ public sealed class ClientObjectTable
if (!existed || obj is null) // keep: satisfies nullable flow analysis
{
obj = new ClientObject { ObjectId = guid };
_objects[guid] = obj;
RetainObject(obj);
}
uint oldContainer = obj.ContainerId;
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
@ -955,7 +1016,7 @@ public sealed class ClientObjectTable
if (!existed || obj is null)
{
obj = new ClientObject { ObjectId = entry.Guid };
_objects[entry.Guid] = obj;
RetainObject(obj);
}
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
@ -1249,7 +1310,7 @@ public sealed class ClientObjectTable
if (!existed || obj is null)
{
obj = new ClientObject { ObjectId = entry.Guid };
_objects[entry.Guid] = obj;
RetainObject(obj);
}
obj.ContainerTypeHint = entry.ContainerType;
if (!existed) added.Add(obj);
@ -1361,7 +1422,7 @@ public sealed class ClientObjectTable
if (!existed || obj is null)
{
obj = new ClientObject { ObjectId = entry.Guid };
_objects[entry.Guid] = obj;
RetainObject(obj);
}
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
@ -1426,6 +1487,8 @@ public sealed class ClientObjectTable
/// </summary>
public void Clear()
{
foreach (ClientObject item in _restrictionObservedObjects.ToArray())
UnbindRestrictionAuthority(item);
_objects.Clear();
_containers.Clear();
_containerIndex.Clear();

View file

@ -1,3 +1,4 @@
using System.Collections.Frozen;
using System.Collections.Generic;
namespace AcDream.Core.Items;
@ -22,11 +23,29 @@ namespace AcDream.Core.Items;
/// (0 = dwelling access only, 1 = storage access also). Retail's
/// <c>IsAllowedIn</c> only consults key membership for entry; the permission
/// value is preserved for wire fidelity but not consulted here.</param>
public sealed record HouseRestrictionRecord(
bool OpenToPublic,
uint AllegianceMonarchId,
IReadOnlyDictionary<uint, uint> Guests)
public sealed record HouseRestrictionRecord
{
public HouseRestrictionRecord(
bool OpenToPublic,
uint AllegianceMonarchId,
IReadOnlyDictionary<uint, uint> Guests)
{
ArgumentNullException.ThrowIfNull(Guests);
this.OpenToPublic = OpenToPublic;
this.AllegianceMonarchId = AllegianceMonarchId;
this.Guests = Guests.ToFrozenDictionary();
}
public bool OpenToPublic { get; }
public uint AllegianceMonarchId { get; }
/// <summary>
/// Immutable snapshot of the wire permission table. The parser's mutable
/// dictionary must never remain an untracked mutation path into live
/// collision-entry authority.
/// </summary>
public IReadOnlyDictionary<uint, uint> Guests { get; }
/// <summary>
/// Verbatim port of retail <c>RestrictionDB::IsAllowedIn</c>
/// (named-retail pc:444493-444516, 0x005ae8f0):

View file

@ -25,6 +25,14 @@ public sealed class CellArray : ICollection<uint>, IReadOnlyCollection<uint>
private readonly List<uint> _order = new();
private readonly HashSet<uint> _seen = new();
/// <summary>
/// Optional append-only union target used by one retained SetPosition
/// transaction. Clearing this CELLARRAY must not clear the target: retail
/// can rebuild the working array repeatedly while every probed cell still
/// contributes to the transaction's collision-world authority footprint.
/// </summary>
internal CellArray? UnionTarget { get; set; }
public int Count => _order.Count;
public bool IsReadOnly => false;
@ -34,6 +42,11 @@ public sealed class CellArray : ICollection<uint>, IReadOnlyCollection<uint>
/// <summary>Append <paramref name="id"/> iff not already present (retail add_cell dedup).</summary>
public void Add(uint id)
{
if (UnionTarget is { } target
&& !ReferenceEquals(target, this))
{
target.Add(id);
}
if (_seen.Add(id))
_order.Add(id);
}

View file

@ -169,6 +169,7 @@ public static class CellTransit
// Retail CEnvCell::find_transit_cells first asks the loaded
// neighbour cell whether the sphere intersects its CellBSP.
// The portal-plane side test is only the unloaded-cell load hint.
RecordUnionOnlyProbe(candidates, otherId);
var otherCell = cache.GetCellStruct(otherId);
if (otherCell is not null &&
CollisionTraversal.HasCellContainment(cache, otherCell))
@ -446,6 +447,7 @@ public static class CellTransit
if (portal.OtherPortalId < 0)
continue;
RecordUnionOnlyProbe(candidates, portal.OtherCellId);
var otherCell = cache.GetCellStruct(portal.OtherCellId);
if (otherCell is null ||
!CollisionTraversal.HasCellContainment(cache, otherCell))
@ -679,8 +681,13 @@ public static class CellTransit
/// </para>
/// </summary>
public static uint FindVisibleChildCell(
PhysicsDataCache cache, uint startCellId, Vector3 worldPoint, bool useStabList)
PhysicsDataCache cache,
uint startCellId,
Vector3 worldPoint,
bool useStabList,
ICollection<uint>? probedCells = null)
{
probedCells?.Add(startCellId);
var start = cache.GetCellStruct(startCellId);
if (start is null) return 0u;
@ -691,12 +698,17 @@ public static class CellTransit
{
// arg3 != 0 → iterate stab_list, GetVisible + point_in_cell (:311444-311465)
foreach (uint id in start.VisibleCellIds)
{
probedCells?.Add(id);
if (PointInCell(cache, cache.GetCellStruct(id), worldPoint)) return id;
}
}
else
{
// arg3 == 0 → iterate direct portals, GetOtherCell + point_in_cell (:311411-311434)
foreach (var portal in start.Portals)
{
probedCells?.Add(portal.OtherCellId);
if (PointInCell(
cache,
cache.GetCellStruct(portal.OtherCellId),
@ -704,6 +716,7 @@ public static class CellTransit
{
return portal.OtherCellId;
}
}
}
return 0u;
@ -1068,7 +1081,11 @@ public static class CellTransit
sphereRadius))
{
uint recovered = FindVisibleChildCell(
cache, currentCellId, worldSphereCenter, useStabList: true);
cache,
currentCellId,
worldSphereCenter,
useStabList: true,
(candidates as CellArray)?.UnionTarget);
if (recovered != 0u && recovered != currentCellId)
return recovered;
}
@ -1078,6 +1095,14 @@ public static class CellTransit
return currentCellId;
}
private static void RecordUnionOnlyProbe(
ICollection<uint> candidates,
uint cellId)
{
if (candidates is CellArray { UnionTarget: { } queryFootprint })
queryFootprint.Add(cellId);
}
private static int EffectiveSphereCount(IReadOnlyList<Sphere> worldSpheres, int numSpheres)
{
if (numSpheres <= 0 || worldSpheres.Count == 0) return 0;

View file

@ -224,7 +224,22 @@ public sealed class PhysicsEngine
/// when the restriction weenie can't be resolved — so production MUST
/// wire this to the live table for the fix to actually admit anyone.
/// </summary>
public ClientObjectTable? Objects { get; set; }
private ClientObjectTable? _objects;
private ulong _objectsBindingRevision;
public ClientObjectTable? Objects
{
get => _objects;
set
{
if (ReferenceEquals(_objects, value))
return;
_objects = value;
_objectsBindingRevision = checked(_objectsBindingRevision + 1UL);
}
}
internal ulong ObjectsBindingRevision => _objectsBindingRevision;
internal sealed record LandblockPhysics(
TerrainSurface Terrain,
@ -1553,8 +1568,10 @@ public sealed class PhysicsEngine
private AdjustedSetPosition AdjustSetPosition(
uint seedCellId,
Vector3 cellLocalPosition,
Vector3 firstWorldSphereCenter)
Vector3 firstWorldSphereCenter,
CellArray queryFootprint)
{
queryFootprint.Add(seedCellId);
uint low = seedCellId & 0xFFFFu;
bool lowInRange = low is (>= 1u and <= 0x40u)
or (>= 0x0100u and <= 0xFFFDu)
@ -1582,7 +1599,8 @@ public sealed class PhysicsEngine
cache,
seedCellId,
firstWorldSphereCenter,
useStabList: true);
useStabList: true,
queryFootprint);
if (child != 0u)
{
return new AdjustedSetPosition(
@ -1606,6 +1624,7 @@ public sealed class PhysicsEngine
bool adjusted = LandDefs.AdjustToOutside(
ref adjustedCell,
ref adjustedLocal);
queryFootprint.Add(adjustedCell);
bool resident = adjusted
&& IsLandblockTerrainResident(adjustedCell);
return new AdjustedSetPosition(
@ -1635,33 +1654,54 @@ public sealed class PhysicsEngine
}
Transition transition = RentTransition();
CellArray queryFootprint =
transition.SpherePath.SetPositionQueryFootprint;
try
{
queryFootprint.Clear();
transition.SpherePath.CellCandidates.UnionTarget = queryFootprint;
InitializeSetPositionTransition(transition, request);
bool randomOnly = request.Flags.HasFlag(
PhysicsSetPositionFlags.RandomScatter);
PhysicsSetPositionResult result;
if (randomOnly)
{
return SetScatterPositionInternal(
result = SetScatterPositionInternal(
transition,
request,
handleCollisions);
handleCollisions,
queryFootprint);
}
else
{
result = SetPositionInternal(
transition,
request,
handleCollisions,
queryFootprint);
if (result.Error != PhysicsSetPositionError.Ok
&& request.Flags.HasFlag(PhysicsSetPositionFlags.Scatter))
{
result = SetScatterPositionInternal(
transition,
request,
handleCollisions,
queryFootprint);
}
}
PhysicsSetPositionResult result =
SetPositionInternal(transition, request, handleCollisions);
if (result.Error != PhysicsSetPositionError.Ok
&& request.Flags.HasFlag(PhysicsSetPositionFlags.Scatter))
// Scatter retains one append-only query union across every inner
// attempt. Materialize it exactly once at the public transaction
// boundary; copying it per attempt is quadratic at retail's
// maximum retry count.
return result with
{
return SetScatterPositionInternal(
transition,
request,
handleCollisions);
}
return result;
QueriedCellIds = queryFootprint.OrderedIds.ToImmutableArray(),
};
}
finally
{
transition.SpherePath.CellCandidates.UnionTarget = null;
ReturnTransition(transition);
}
}
@ -1686,7 +1726,8 @@ public sealed class PhysicsEngine
private PhysicsSetPositionResult SetScatterPositionInternal(
Transition transition,
in PhysicsSetPositionRequest request,
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions,
CellArray queryFootprint)
{
PhysicsSetPositionResult result = ErrorResult(
request,
@ -1706,7 +1747,8 @@ public sealed class PhysicsEngine
result = SetPositionInternal(
transition,
scattered,
handleCollisions);
handleCollisions,
queryFootprint);
if (result.Error == PhysicsSetPositionError.Ok)
break;
}
@ -1716,7 +1758,8 @@ public sealed class PhysicsEngine
private PhysicsSetPositionResult SetPositionInternal(
Transition transition,
in PhysicsSetPositionRequest request,
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions,
CellArray queryFootprint)
{
transition.SpherePath.CellCandidates.Clear();
transition.SpherePath.ClearWalkable();
@ -1731,7 +1774,8 @@ public sealed class PhysicsEngine
AdjustedSetPosition adjusted = AdjustSetPosition(
request.CellId,
request.CellLocalPosition,
firstWorldCenter);
firstWorldCenter,
queryFootprint);
if (!adjusted.Resident)
{
return new PhysicsSetPositionResult(
@ -1849,7 +1893,9 @@ public sealed class PhysicsEngine
}
if (spherePath.CurCellId == 0u)
{
return ErrorResult(request, PhysicsSetPositionError.NoCell);
return ErrorResult(
request,
PhysicsSetPositionError.NoCell);
}
bool inContact = collision.ContactPlaneValid;

View file

@ -145,7 +145,8 @@ internal readonly record struct PhysicsSetPositionResult(
bool CellChanged = false,
PhysicsShadowCommitAction ShadowAction = PhysicsShadowCommitAction.None,
ImmutableArray<uint> CrossCellIds = default,
ImmutableArray<uint> CollidedObjectIds = default)
ImmutableArray<uint> CollidedObjectIds = default,
ImmutableArray<uint> QueriedCellIds = default)
{
internal bool IsSuccessful => Error == PhysicsSetPositionError.Ok;
internal bool IsCommitted =>

View file

@ -78,6 +78,7 @@ public sealed class ShadowObjectRegistry
_collisionWorld.Current.ShadowOwnerFreeSlots;
private readonly HashSet<uint> _prefixScratch = new();
private readonly List<uint> _removedPrefixScratch = new();
private ulong _mutationRevision;
internal event Action<uint, ulong>? OwnerMutated;
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
@ -101,6 +102,7 @@ public sealed class ShadowObjectRegistry
"A populated shadow registry cannot change collision roots.");
}
_collisionWorld = collisionWorld;
AdvanceMutationRevision();
}
internal sealed record RegistrationRecord(
@ -123,8 +125,20 @@ public sealed class ShadowObjectRegistry
? version
: 0UL;
/// <summary>
/// Monotonic authority for every logical mutation of the active shadow
/// collision world. SetPosition evaluation receipts seal this value so a
/// later owner insert, removal, move, state change, suspension, reflood,
/// or cell-row replacement cannot be committed against a different world.
/// </summary>
internal ulong MutationRevision => _mutationRevision;
private void AdvanceMutationRevision() =>
_mutationRevision = checked(_mutationRevision + 1UL);
private void BumpOwnerVersion(uint entityId)
{
AdvanceMutationRevision();
ulong version = checked(GetOwnerVersion(entityId) + 1UL);
_ownerVersions[entityId] = version;
RefreshOwnerPrefixIndex(entityId);
@ -1308,6 +1322,7 @@ public sealed class ShadowObjectRegistry
DeregisterCore(entityId, publishMutation: false);
RemoveOwnerPrefixMembership(entityId);
_ownerVersions.Remove(entityId);
AdvanceMutationRevision();
return;
}
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
@ -1877,6 +1892,13 @@ public sealed class ShadowObjectRegistry
/// </summary>
public void Clear()
{
bool mutated = _cells.Count != 0
|| _entityToCells.Count != 0
|| _entityReg.Count != 0
|| _suspendedEntities.Count != 0
|| _suspendedEntityCells.Count != 0;
if (mutated)
AdvanceMutationRevision();
_cells.Clear();
_entityToCells.Clear();
_suspendedEntities.Clear();

View file

@ -719,6 +719,7 @@ public sealed class SpherePath
private Vector3[]? _walkableVertexStorage;
private Vector3[]? _lastWalkableVertexStorage;
internal readonly CellArray CellCandidates = new();
internal readonly CellArray SetPositionQueryFootprint = new();
internal readonly CellOrderScratchArena OrderedCellScratch = new();
internal Vector3[]? RetainedWalkableVertexStorage => _walkableVertexStorage;
@ -938,6 +939,8 @@ public sealed class SpherePath
if (_lastWalkableVertexStorage is not null)
System.Array.Clear(_lastWalkableVertexStorage);
CellCandidates.Clear();
CellCandidates.UnionTarget = null;
SetPositionQueryFootprint.Clear();
OrderedCellScratch.ResetForReuse();
}

View file

@ -704,6 +704,9 @@ public sealed class PlayerMovementController
internal bool IsSealedPublicationCandidate => _publicationLifecycle
is PlayerMovementControllerPublicationLifecycle.CandidateSealed;
internal bool IsRuntimeOwnedDormant => _publicationLifecycle
is PlayerMovementControllerPublicationLifecycle.RuntimeOwnedDormant;
internal void SealPublicationCandidate()
{
if (_publicationLifecycle

View file

@ -14,6 +14,15 @@ internal enum RuntimeLocalPlayerPhysicsPublicationStatus
Discarded,
}
internal enum RuntimeLocalPlayerPhysicsActivationStatus
{
Evaluated,
DeferredCell,
RejectedPlacement,
RejectedAuthority,
RejectedToken,
}
internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
RuntimeEntityKey Entity,
RuntimeEntityPlacementToken Placement,
@ -31,6 +40,33 @@ internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
&& Entity == Placement.Entity;
}
internal readonly record struct RuntimeLocalPlayerPhysicsActivationToken(
RuntimeEntityKey Entity,
RuntimeEntityPlacementToken Placement,
ulong ActivationId,
uint LocalPlayerServerGuid,
long LocalPlayerIdentityRevision,
ulong PhysicsOwnershipEpoch,
ulong ObjectClockEpoch,
ulong ControllerOwnershipEpoch,
ulong SessionGenerationAuthority)
{
internal bool IsValid => ActivationId != 0UL
&& LocalPlayerServerGuid != 0u
&& Placement.IsValid
&& Entity == Placement.Entity;
}
internal readonly record struct RuntimeLocalPlayerPhysicsActivationReceipt(
RuntimeLocalPlayerPhysicsActivationToken Token,
ulong EvaluationId,
RuntimeDormantSetPositionEvaluation Placement)
{
internal bool IsValid => Token.IsValid
&& EvaluationId != 0UL
&& Placement.Placement == Token.Placement;
}
internal readonly record struct RuntimeLocalPlayerPhysicsCandidateSnapshot(
Vector3 Position,
Quaternion Orientation,
@ -45,10 +81,13 @@ public readonly record struct
bool IsBound,
bool IsDisposed,
int CandidateCount,
int PendingActivationCount,
ulong LastPublicationId)
{
internal bool IsConverged => !IsBound
|| (IsDisposed && CandidateCount == 0);
|| (IsDisposed
&& CandidateCount == 0
&& PendingActivationCount == 0);
}
/// <summary>
@ -74,12 +113,28 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
internal required PhysicsBody Body { get; init; }
}
private sealed class Activation
{
internal required RuntimeLocalPlayerPhysicsActivationToken Token
{ get; init; }
internal required RuntimeEntityRecord Record { get; init; }
internal required RuntimeSetPositionCommand PlacementCommand
{ get; init; }
internal required PlayerMovementController Controller { get; init; }
internal required PhysicsBody Body { get; init; }
internal RuntimeLocalPlayerPhysicsActivationReceipt Receipt
{ get; set; }
}
private readonly RuntimeEntityDirectory _entities;
private readonly RuntimePhysicsState _physics;
private readonly RuntimeLocalPlayerMovementState _movement;
private readonly RuntimeLocalPlayerIdentityState _identity;
private Candidate? _candidate;
private Activation? _activation;
private ulong _nextPublicationId;
private ulong _nextActivationId;
private ulong _nextEvaluationId;
private bool _disposed;
internal RuntimeLocalPlayerPhysicsPublicationState(
@ -161,9 +216,15 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
}
internal RuntimeLocalPlayerPhysicsPublicationStatus Commit(
in RuntimeLocalPlayerPhysicsPublicationToken token)
in RuntimeLocalPlayerPhysicsPublicationToken token) =>
Commit(token, out _);
internal RuntimeLocalPlayerPhysicsPublicationStatus Commit(
in RuntimeLocalPlayerPhysicsPublicationToken token,
out RuntimeLocalPlayerPhysicsActivationToken activationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
activationToken = default;
if (!token.IsValid
|| _candidate is not { } candidate
|| candidate.Token != token)
@ -184,10 +245,104 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
candidate.Record.ObjectClock);
candidate.Record.SetPhysicsBody(candidate.Body);
_movement.CommitRuntimeOwnedController(candidate.Controller);
activationToken = new RuntimeLocalPlayerPhysicsActivationToken(
candidate.Token.Entity,
candidate.Token.Placement,
checked(++_nextActivationId),
candidate.Token.LocalPlayerServerGuid,
candidate.Token.LocalPlayerIdentityRevision,
candidate.Record.PhysicsOwnershipEpoch,
candidate.Record.ObjectClockEpoch,
_movement.ControllerOwnershipEpoch,
_entities.SessionLifetimeVersion);
_activation = new Activation
{
Token = activationToken,
Record = candidate.Record,
PlacementCommand = candidate.PlacementCommand,
Controller = candidate.Controller,
Body = candidate.Body,
};
_candidate = null;
return RuntimeLocalPlayerPhysicsPublicationStatus.Committed;
}
internal RuntimeLocalPlayerPhysicsActivationStatus EvaluateActivation(
in RuntimeLocalPlayerPhysicsActivationToken token,
out RuntimeLocalPlayerPhysicsActivationReceipt receipt)
{
ObjectDisposedException.ThrowIf(_disposed, this);
receipt = default;
if (!token.IsValid
|| _activation is not { } activation
|| activation.Token != token)
{
return RuntimeLocalPlayerPhysicsActivationStatus.RejectedToken;
}
if (!IsActivationCurrent(activation))
{
DiscardActivation();
return RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority;
}
if (!_physics.SetPosition.TryEvaluateDormantLocalActivation(
activation.Record,
activation.Body,
token.Placement,
activation.PlacementCommand,
out RuntimeDormantSetPositionEvaluation placement))
{
if (ReferenceEquals(_activation, activation)
&& !IsActivationCurrent(activation))
{
DiscardActivation();
}
return RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority;
}
receipt = new RuntimeLocalPlayerPhysicsActivationReceipt(
token,
checked(++_nextEvaluationId),
placement);
activation.Receipt = receipt;
if (placement.Result.IsDeferred)
return RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell;
return placement.Result.IsCommitted
? RuntimeLocalPlayerPhysicsActivationStatus.Evaluated
: RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement;
}
internal bool IsEvaluationCurrent(
in RuntimeLocalPlayerPhysicsActivationReceipt receipt)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!receipt.IsValid
|| _activation is not { } activation
|| activation.Token != receipt.Token
|| activation.Receipt != receipt)
{
return false;
}
return IsActivationCurrent(activation)
&& _physics.SetPosition.IsDormantLocalEvaluationCurrent(
activation.Record,
activation.Body,
receipt.Placement);
}
internal bool DiscardActivation(
in RuntimeLocalPlayerPhysicsActivationToken token)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!token.IsValid
|| _activation is not { } activation
|| activation.Token != token)
{
return false;
}
DiscardActivation();
return true;
}
internal RuntimeLocalPlayerPhysicsPublicationStatus Discard(
in RuntimeLocalPlayerPhysicsPublicationToken token)
{
@ -231,12 +386,14 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
IsBound: true,
_disposed,
_candidate is null ? 0 : 1,
_activation is null ? 0 : 1,
_nextPublicationId);
internal void ResetSession()
{
ObjectDisposedException.ThrowIf(_disposed, this);
DiscardCurrent();
DiscardActivation();
}
public void Dispose()
@ -244,6 +401,7 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
if (_disposed)
return;
DiscardCurrent();
DiscardActivation();
_disposed = true;
}
@ -251,7 +409,8 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken placement,
in RuntimeSetPositionCommand command) =>
record.Key is { } key
_activation is null
&& record.Key is { } key
&& key == placement.Entity
&& _entities.IsCurrent(record)
&& !record.DeleteAcceptedForTeardown
@ -276,7 +435,8 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
command);
private bool IsCurrent(Candidate candidate) =>
candidate.Controller.IsSealedPublicationCandidate
_activation is null
&& candidate.Controller.IsSealedPublicationCandidate
&& candidate.Controller.OwnsPhysicsBody(candidate.Body)
&& _entities.SessionLifetimeVersion
== candidate.Token.SessionGenerationAuthority
@ -313,4 +473,55 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
_candidate = null;
candidate?.Controller.DiscardRuntimeCandidate();
}
private bool IsActivationCurrent(Activation activation) =>
activation.Controller.IsRuntimeOwnedDormant
&& activation.Controller.OwnsPhysicsBody(activation.Body)
&& _entities.SessionLifetimeVersion
== activation.Token.SessionGenerationAuthority
&& _entities.IsCurrent(activation.Record)
&& activation.Record.Key == activation.Token.Entity
&& !_identity.IsDisposed
&& _identity.ServerGuid == activation.Token.LocalPlayerServerGuid
&& _identity.ServerGuid == activation.Record.ServerGuid
&& _identity.Revision == activation.Token.LocalPlayerIdentityRevision
&& activation.Record.PhysicsOwnershipEpoch
== activation.Token.PhysicsOwnershipEpoch
&& activation.Record.ObjectClockEpoch
== activation.Token.ObjectClockEpoch
&& _movement.CanCommitRuntimeOwnedController(
activation.Token.ControllerOwnershipEpoch,
activation.Controller)
&& ReferenceEquals(activation.Record.PhysicsBody, activation.Body)
&& activation.Record.PhysicsHost is null
&& activation.Record.RemoteMotion is null
&& activation.Record.Projectile is null
&& !activation.Record.PhysicsBodyAcquisitionInProgress
&& !activation.Record.RemoteMotionBindingInProgress
&& !activation.Record.ProjectileBindingInProgress
&& !activation.Record.RequiresRemotePlacementRuntime
&& !activation.Record.DeleteAcceptedForTeardown
&& _physics.SetPosition.IsExactPreparedPlacementCurrent(
activation.Record,
activation.Token.Placement,
activation.PlacementCommand);
private void DiscardActivation()
{
Activation? activation = _activation;
_activation = null;
if (activation is not null
&& _entities.IsCurrent(activation.Record)
&& ReferenceEquals(
activation.Record.PhysicsBody,
activation.Body))
{
_entities.SetPhysicsBody(activation.Record, null);
}
if (activation is not null
&& ReferenceEquals(_movement.Controller, activation.Controller))
{
_movement.Controller = null;
}
}
}

View file

@ -1,3 +1,5 @@
using System.Collections.Immutable;
using AcDream.Core.Items;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
@ -1042,6 +1044,7 @@ public sealed class RuntimePhysicsState : IDisposable
private bool _suppressCollisionOwnerJournal;
private long _nextCollisionPreparationSequence;
private long _latestCollisionPreparationStartSequence;
private ulong _collisionWorldAuthority = 1UL;
private readonly List<Action<RuntimeCollisionGenerationCommitted>>
_collisionGenerationCommittedObservers = new();
private bool _disposed;
@ -1898,6 +1901,7 @@ public sealed class RuntimePhysicsState : IDisposable
EnsureNotDisposed();
EnsureCollisionMutationThread();
uint canonical = CanonicalLandblock(landblockId);
AdvanceCollisionWorldAuthority();
if (_collisionAdmissions.Remove(
canonical,
out RuntimeCollisionAdmission? superseded))
@ -2013,6 +2017,7 @@ public sealed class RuntimePhysicsState : IDisposable
admission.Generation);
_collisionGenerations[admission.LandblockId] = checked(
admission.Generation + 1UL);
AdvanceCollisionWorldAuthority();
}
TrimCollisionOwnerJournal();
}
@ -2226,6 +2231,7 @@ public sealed class RuntimePhysicsState : IDisposable
try
{
Engine.CommitLandblockReplacement(replacement);
AdvanceCollisionWorldAuthority();
}
finally
{
@ -2496,6 +2502,7 @@ public sealed class RuntimePhysicsState : IDisposable
private void InvalidateCollisionAdmission(uint landblockId)
{
AdvanceCollisionWorldAuthority();
ulong currentGeneration = _collisionGenerations.TryGetValue(
landblockId,
out ulong current)
@ -2540,6 +2547,130 @@ public sealed class RuntimePhysicsState : IDisposable
: 1UL;
}
/// <summary>
/// Exact collision-prefix generation authority used by private
/// SetPosition evaluations. Beginning a replacement generation advances
/// this authority immediately, so a receipt cannot survive a prepared
/// world replacement and later observe different collision rows.
/// </summary>
internal ulong CollisionGenerationAuthority(uint exactCellId)
{
uint landblockId = CanonicalLandblock(exactCellId);
return landblockId != 0u
&& _collisionGenerations.TryGetValue(
landblockId,
out ulong generation)
? generation
: 0UL;
}
internal ulong CollisionWorldAuthority => _collisionWorldAuthority;
internal ulong ShadowWorldAuthority =>
Engine.ShadowObjects.MutationRevision;
internal ClientObjectTable? ObjectTable => Engine.Objects;
internal ulong ObjectTableBindingAuthority =>
Engine.ObjectsBindingRevision;
internal ulong ObjectTableAuthority =>
ObjectTable?.MutationRevision ?? 0UL;
private void AdvanceCollisionWorldAuthority() =>
_collisionWorldAuthority = checked(_collisionWorldAuthority + 1UL);
internal bool TrySealCollisionEvaluationAuthority(
in PhysicsSetPositionResult result,
ulong expectedCollisionWorldAuthority,
ulong expectedShadowWorldAuthority,
ClientObjectTable? expectedObjectTable,
ulong expectedObjectTableBindingAuthority,
ulong expectedObjectTableAuthority,
out RuntimeCollisionEvaluationAuthority authority)
{
authority = default;
if (_collisionWorldAuthority != expectedCollisionWorldAuthority
|| ShadowWorldAuthority != expectedShadowWorldAuthority
|| !ReferenceEquals(ObjectTable, expectedObjectTable)
|| ObjectTableBindingAuthority
!= expectedObjectTableBindingAuthority
|| ObjectTableAuthority != expectedObjectTableAuthority)
{
return false;
}
var prefixes = new HashSet<uint>();
var authorities = ImmutableArray.CreateBuilder<
RuntimeCollisionGenerationAuthority>();
void Add(uint cellId)
{
uint landblock = CanonicalLandblock(cellId);
if (landblock == 0u || !prefixes.Add(landblock))
return;
authorities.Add(new RuntimeCollisionGenerationAuthority(
landblock,
CollisionGenerationAuthority(landblock)));
}
Add(result.CellId);
if (!result.QueriedCellIds.IsDefaultOrEmpty)
{
foreach (uint cellId in result.QueriedCellIds)
Add(cellId);
}
foreach (uint prefix in prefixes)
{
if (_collisionAdmissions.ContainsKey(prefix))
return false;
}
if (_collisionWorldAuthority != expectedCollisionWorldAuthority
|| ShadowWorldAuthority != expectedShadowWorldAuthority
|| !ReferenceEquals(ObjectTable, expectedObjectTable)
|| ObjectTableBindingAuthority
!= expectedObjectTableBindingAuthority
|| ObjectTableAuthority != expectedObjectTableAuthority)
{
return false;
}
authority = new RuntimeCollisionEvaluationAuthority(
expectedCollisionWorldAuthority,
expectedShadowWorldAuthority,
expectedObjectTable,
expectedObjectTableBindingAuthority,
expectedObjectTableAuthority,
authorities.ToImmutable());
return true;
}
internal bool IsCollisionEvaluationAuthorityCurrent(
in RuntimeCollisionEvaluationAuthority authority)
{
if (!authority.IsValid
|| _collisionWorldAuthority != authority.CollisionWorldAuthority
|| ShadowWorldAuthority != authority.ShadowWorldAuthority
|| !ReferenceEquals(ObjectTable, authority.ObjectTable)
|| ObjectTableBindingAuthority
!= authority.ObjectTableBindingAuthority
|| ObjectTableAuthority != authority.ObjectTableAuthority)
{
return false;
}
foreach (RuntimeCollisionGenerationAuthority generation
in authority.Generations)
{
if (generation.LandblockId == 0u
|| _collisionAdmissions.ContainsKey(generation.LandblockId)
|| CollisionGenerationAuthority(generation.LandblockId)
!= generation.Generation)
{
return false;
}
}
return true;
}
internal bool HandleSetPositionCollisions(
RuntimeEntityRecord record,
ulong positionAuthorityVersion,

View file

@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
@ -82,6 +83,32 @@ internal readonly record struct RuntimeSetPositionCommand(
float ShadowWorldOffsetY = 0f,
RuntimePortalPlacementAuthority Portal = default);
internal readonly record struct RuntimeCollisionGenerationAuthority(
uint LandblockId,
ulong Generation);
internal readonly record struct RuntimeCollisionEvaluationAuthority(
ulong CollisionWorldAuthority,
ulong ShadowWorldAuthority,
ClientObjectTable? ObjectTable,
ulong ObjectTableBindingAuthority,
ulong ObjectTableAuthority,
ImmutableArray<RuntimeCollisionGenerationAuthority> Generations)
{
internal bool IsValid => CollisionWorldAuthority != 0UL
&& !Generations.IsDefault;
}
internal readonly record struct RuntimeDormantSetPositionEvaluation(
RuntimeEntityPlacementToken Placement,
RuntimeSetPositionCommand Command,
PhysicsSetPositionResult Result,
RuntimeCollisionEvaluationAuthority CollisionAuthority)
{
internal bool IsValid => Placement.IsValid
&& CollisionAuthority.IsValid;
}
public readonly record struct RuntimePlacementProjectionToken(
ulong Sequence,
ulong Revision,
@ -597,6 +624,98 @@ internal sealed class RuntimeSetPositionState : IDisposable
&& IsPreparationAuthorityCurrent(operation, authority);
}
/// <summary>
/// Evaluates the exact authored local-player placement without mutating
/// the canonical dormant body or any Runtime ownership index. Core's
/// SetPosition transaction is pure; collision reporting is deliberately
/// omitted until the later atomic activation commit.
/// </summary>
internal bool TryEvaluateDormantLocalActivation(
RuntimeEntityRecord record,
PhysicsBody body,
in RuntimeEntityPlacementToken token,
in RuntimeSetPositionCommand command,
out RuntimeDormantSetPositionEvaluation evaluation)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(body);
evaluation = default;
if (!IsExactDormantLocalActivationCurrent(
record,
body,
token,
command,
out Operation? operation))
{
return false;
}
PhysicsSetPositionRequest canonicalRequest = command.Physics with
{
MoverPhysicsState = record.FinalPhysicsState,
MovingEntityId = token.Entity.LocalEntityId,
CurrentCellId = null,
};
if (!IsStructurallyValid(canonicalRequest))
return false;
var canonicalCommand = command with { Physics = canonicalRequest };
ulong collisionWorldAuthority = _physics.CollisionWorldAuthority;
ulong shadowWorldAuthority = _physics.ShadowWorldAuthority;
ClientObjectTable? objectTable = _physics.ObjectTable;
ulong objectTableBindingAuthority =
_physics.ObjectTableBindingAuthority;
ulong objectTableAuthority = objectTable?.MutationRevision ?? 0UL;
PhysicsSetPositionResult result = _physics.Engine.SetPosition(
canonicalRequest,
handleCollisions: null);
if (!IsExactDormantLocalActivationCurrent(
record,
body,
token,
command,
out Operation? current)
|| !ReferenceEquals(current, operation))
{
return false;
}
if (!_physics.TrySealCollisionEvaluationAuthority(
result,
collisionWorldAuthority,
shadowWorldAuthority,
objectTable,
objectTableBindingAuthority,
objectTableAuthority,
out RuntimeCollisionEvaluationAuthority collisionAuthority))
{
return false;
}
evaluation = new RuntimeDormantSetPositionEvaluation(
token,
canonicalCommand,
result,
collisionAuthority);
return true;
}
internal bool IsDormantLocalEvaluationCurrent(
RuntimeEntityRecord record,
PhysicsBody body,
in RuntimeDormantSetPositionEvaluation evaluation) =>
evaluation.IsValid
&& IsExactDormantLocalActivationCurrent(
record,
body,
evaluation.Placement,
evaluation.Command,
out _,
allowCanonicalCommand: true)
&& _physics.IsCollisionEvaluationAuthorityCurrent(
evaluation.CollisionAuthority);
internal RuntimeSetPositionOutcome SubmitPreparedPlacement(
in RuntimeEntityPlacementToken token,
in RuntimeSetPositionCommand command) =>
@ -1881,6 +2000,69 @@ internal sealed class RuntimeSetPositionState : IDisposable
&& operation.Record.PlacementCommitVersion
== operation.PlacementCommitVersion;
private bool IsExactDormantLocalActivationCurrent(
RuntimeEntityRecord record,
PhysicsBody body,
in RuntimeEntityPlacementToken token,
in RuntimeSetPositionCommand command,
out Operation? operation,
bool allowCanonicalCommand = false)
{
operation = null;
if (!token.IsValid
|| token.Entity != record.Key
|| token.PreparationKind
is not RuntimeEntityPlacementPreparationKind.AuthoredMover
|| command.Kind is not (RuntimeSetPositionOperationKind.InitialLogin
or RuntimeSetPositionOperationKind.LocalAuthoritative)
|| command.Portal != default
&& !command.Portal.IsValid
|| !_operations.TryGetValue(token.Entity, out operation)
|| operation.Token != token
|| operation.Stage
is not RuntimeEntityPlacementStage.AwaitingPreparation
|| !ReferenceEquals(operation.Record, record)
|| !IsCurrent(operation)
|| !ReferenceEquals(record.PhysicsBody, body)
|| body.InWorld
|| (body.TransientState & TransientStateFlags.Active) != 0
|| record.PhysicsHost is not null
|| record.RemoteMotion is not null
|| record.Projectile is not null
|| record.PhysicsBodyAcquisitionInProgress
|| record.RemoteMotionBindingInProgress
|| record.ProjectileBindingInProgress
|| record.RequiresRemotePlacementRuntime
|| record.DeleteAcceptedForTeardown
|| _physics.IsSpatialRoot(record)
|| !_moverPreparationAuthorities.TryGetValue(
token.Entity,
out MoverPreparationAuthority authority)
|| authority.OperationId != token.OperationId
|| !authority.Prepared
|| !IsPreparationAuthorityCurrent(operation, authority))
{
operation = null;
return false;
}
if (authority.PreparedCommand == command)
return true;
if (!allowCanonicalCommand)
return false;
RuntimeSetPositionCommand authored = authority.PreparedCommand;
return authored with
{
Physics = authored.Physics with
{
MoverPhysicsState = record.FinalPhysicsState,
MovingEntityId = token.Entity.LocalEntityId,
CurrentCellId = null,
},
} == command;
}
private bool IsVelocityCurrent(Operation operation) =>
operation.SourceVelocityAuthorityVersion == 0UL
|| operation.Record.VelocityAuthorityVersion