feat(runtime): own deferred set-position residence
This commit is contained in:
parent
e84a388e6f
commit
4c02ac4259
18 changed files with 5557 additions and 34 deletions
|
|
@ -23,6 +23,9 @@
|
|||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>AcDream.Runtime</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>AcDream.Runtime.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ internal sealed class CollisionWorldState
|
|||
internal Dictionary<uint, List<ShadowEntry>> ShadowCells { get; } = new();
|
||||
internal Dictionary<uint, List<uint>> ShadowEntityCells { get; } = new();
|
||||
internal HashSet<uint> SuspendedShadowEntities { get; } = new();
|
||||
internal Dictionary<uint, List<uint>> SuspendedShadowEntityCells { get; } = new();
|
||||
internal Dictionary<uint, HashSet<uint>> WithdrawnPrefixesByOwner { get; } = new();
|
||||
internal Dictionary<uint, IReadOnlyList<ShadowShape>> ShadowEntityShapes { get; } = new();
|
||||
internal Dictionary<uint, ShadowObjectRegistry.RegistrationRecord>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ public sealed class ShadowObjectRegistry
|
|||
_collisionWorld.Current.ShadowEntityCells; // for deregistration
|
||||
private HashSet<uint> _suspendedEntities =>
|
||||
_collisionWorld.Current.SuspendedShadowEntities;
|
||||
private Dictionary<uint, List<uint>> _suspendedEntityCells =>
|
||||
_collisionWorld.Current.SuspendedShadowEntityCells;
|
||||
// Rows withdrawn because a touched landblock streamed out. The owner may
|
||||
// be seeded in an adjacent still-resident landblock, so its remaining rows
|
||||
// cannot by themselves tell RefloodLandblock that this prefix needs repair.
|
||||
|
|
@ -691,6 +693,218 @@ public sealed class ShadowObjectRegistry
|
|||
reg.State, reg.Flags, seedCellId, reg.IsStatic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the shadow-list suffix of one canonical retail
|
||||
/// <c>CPhysicsObj::SetPositionInternal</c> commit. The transition has
|
||||
/// already selected whether retail recalculates, replaces, or preserves
|
||||
/// the owner's cross-cell list; this method consumes that decision
|
||||
/// without running a second placement/flood oracle.
|
||||
/// </summary>
|
||||
internal void CommitSetPosition(
|
||||
uint entityId,
|
||||
Vector3 worldPosition,
|
||||
Quaternion worldRotation,
|
||||
uint seedCellId,
|
||||
float worldOffsetX,
|
||||
float worldOffsetY,
|
||||
PhysicsShadowCommitAction action,
|
||||
System.Collections.Immutable.ImmutableArray<uint> crossCellIds)
|
||||
{
|
||||
if (!_entityReg.TryGetValue(
|
||||
entityId,
|
||||
out RegistrationRecord? registration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case PhysicsShadowCommitAction.None:
|
||||
RefreshPositionRows(
|
||||
entityId,
|
||||
registration,
|
||||
worldPosition,
|
||||
worldRotation,
|
||||
seedCellId);
|
||||
return;
|
||||
case PhysicsShadowCommitAction.Recalculate:
|
||||
UpdatePosition(
|
||||
entityId,
|
||||
worldPosition,
|
||||
worldRotation,
|
||||
worldOffsetX,
|
||||
worldOffsetY,
|
||||
landblockId: seedCellId & 0xFFFF0000u,
|
||||
seedCellId);
|
||||
return;
|
||||
case PhysicsShadowCommitAction.Replace:
|
||||
if (crossCellIds.IsDefaultOrEmpty)
|
||||
{
|
||||
RefreshPositionRows(
|
||||
entityId,
|
||||
registration,
|
||||
worldPosition,
|
||||
worldRotation,
|
||||
seedCellId);
|
||||
return;
|
||||
}
|
||||
ReplacePositionRows(
|
||||
entityId,
|
||||
registration,
|
||||
worldPosition,
|
||||
worldRotation,
|
||||
seedCellId,
|
||||
crossCellIds);
|
||||
return;
|
||||
case PhysicsShadowCommitAction.Preserve:
|
||||
RefreshPositionRows(
|
||||
entityId,
|
||||
registration,
|
||||
worldPosition,
|
||||
worldRotation,
|
||||
seedCellId);
|
||||
return;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(action));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshPositionRows(
|
||||
uint entityId,
|
||||
RegistrationRecord registration,
|
||||
Vector3 worldPosition,
|
||||
Quaternion worldRotation,
|
||||
uint seedCellId)
|
||||
{
|
||||
if ((_entityToCells.TryGetValue(
|
||||
entityId,
|
||||
out List<uint>? retainedCells)
|
||||
|| _suspendedEntityCells.TryGetValue(
|
||||
entityId,
|
||||
out retainedCells))
|
||||
&& retainedCells.Count != 0)
|
||||
{
|
||||
ReplacePositionRows(
|
||||
entityId,
|
||||
registration,
|
||||
worldPosition,
|
||||
worldRotation,
|
||||
seedCellId,
|
||||
retainedCells);
|
||||
return;
|
||||
}
|
||||
|
||||
_entityReg[entityId] = registration with
|
||||
{
|
||||
SeedCellId = seedCellId,
|
||||
EntityWorldPos = worldPosition,
|
||||
EntityWorldRot = worldRotation,
|
||||
};
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
private void ReplacePositionRows(
|
||||
uint entityId,
|
||||
RegistrationRecord registration,
|
||||
Vector3 worldPosition,
|
||||
Quaternion worldRotation,
|
||||
uint seedCellId,
|
||||
IReadOnlyList<uint> cellIds)
|
||||
{
|
||||
if (_entityToCells.TryGetValue(
|
||||
entityId,
|
||||
out List<uint>? previousCells))
|
||||
{
|
||||
for (int index = 0; index < previousCells.Count; index++)
|
||||
{
|
||||
if (_cells.TryGetValue(
|
||||
previousCells[index],
|
||||
out List<ShadowEntry>? entries))
|
||||
{
|
||||
RemoveOwnerRows(entries, entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_suspendedEntities.Remove(entityId);
|
||||
_suspendedEntityCells.Remove(entityId);
|
||||
_entityReg[entityId] = registration with
|
||||
{
|
||||
SeedCellId = seedCellId,
|
||||
EntityWorldPos = worldPosition,
|
||||
EntityWorldRot = worldRotation,
|
||||
};
|
||||
|
||||
var exactCells = new List<uint>(cellIds.Count);
|
||||
for (int index = 0; index < cellIds.Count; index++)
|
||||
{
|
||||
uint cellId = cellIds[index];
|
||||
if (cellId == 0u || exactCells.Contains(cellId))
|
||||
continue;
|
||||
exactCells.Add(cellId);
|
||||
}
|
||||
|
||||
if (registration.IsMultiPart
|
||||
&& _entityShapes.TryGetValue(
|
||||
entityId,
|
||||
out IReadOnlyList<ShadowShape>? shapes))
|
||||
{
|
||||
foreach (ShadowShape shape in shapes)
|
||||
{
|
||||
Vector3 partWorldPosition = worldPosition
|
||||
+ Vector3.Transform(shape.LocalPosition, worldRotation);
|
||||
Quaternion partWorldRotation = worldRotation
|
||||
* shape.LocalRotation;
|
||||
var entry = new ShadowEntry(
|
||||
entityId,
|
||||
shape.GfxObjId,
|
||||
partWorldPosition,
|
||||
partWorldRotation,
|
||||
shape.Radius,
|
||||
shape.CollisionType,
|
||||
shape.CylHeight,
|
||||
shape.Scale,
|
||||
registration.State,
|
||||
registration.Flags,
|
||||
shape.LocalPosition,
|
||||
shape.LocalRotation);
|
||||
for (int index = 0; index < exactCells.Count; index++)
|
||||
AddEntryToCell(entry, exactCells[index]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var entry = new ShadowEntry(
|
||||
entityId,
|
||||
registration.GfxObjId,
|
||||
worldPosition,
|
||||
worldRotation,
|
||||
registration.Radius,
|
||||
registration.CollisionType,
|
||||
registration.CylHeight,
|
||||
registration.Scale,
|
||||
registration.State,
|
||||
registration.Flags);
|
||||
for (int index = 0; index < exactCells.Count; index++)
|
||||
AddEntryToCell(entry, exactCells[index]);
|
||||
}
|
||||
|
||||
if (exactCells.Count == 0)
|
||||
_entityToCells.Remove(entityId);
|
||||
else
|
||||
_entityToCells[entityId] = exactCells;
|
||||
if (_withdrawnPrefixesByOwner.TryGetValue(
|
||||
entityId,
|
||||
out HashSet<uint>? withdrawn))
|
||||
{
|
||||
for (int index = 0; index < exactCells.Count; index++)
|
||||
withdrawn.Remove(exactCells[index] & 0xFFFF0000u);
|
||||
if (withdrawn.Count == 0)
|
||||
_withdrawnPrefixesByOwner.Remove(entityId);
|
||||
}
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an entity from every cell collision list while retaining the
|
||||
/// exact registration and shape payload needed to restore it later.
|
||||
|
|
@ -706,6 +920,7 @@ public sealed class ShadowObjectRegistry
|
|||
|
||||
if (_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
{
|
||||
_suspendedEntityCells[entityId] = new List<uint>(cellIds);
|
||||
foreach (uint cellId in cellIds)
|
||||
{
|
||||
if (_cells.TryGetValue(cellId, out var list))
|
||||
|
|
@ -924,7 +1139,8 @@ public sealed class ShadowObjectRegistry
|
|||
bool existed = _entityReg.ContainsKey(entityId)
|
||||
|| _entityToCells.ContainsKey(entityId)
|
||||
|| _entityShapes.ContainsKey(entityId)
|
||||
|| _suspendedEntities.Contains(entityId);
|
||||
|| _suspendedEntities.Contains(entityId)
|
||||
|| _suspendedEntityCells.ContainsKey(entityId);
|
||||
if (_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
{
|
||||
foreach (var cellId in cellIds)
|
||||
|
|
@ -937,6 +1153,7 @@ public sealed class ShadowObjectRegistry
|
|||
_entityShapes.Remove(entityId);
|
||||
_entityReg.Remove(entityId);
|
||||
_suspendedEntities.Remove(entityId);
|
||||
_suspendedEntityCells.Remove(entityId);
|
||||
_withdrawnPrefixesByOwner.Remove(entityId);
|
||||
if (existed && publishMutation)
|
||||
{
|
||||
|
|
@ -1065,6 +1282,7 @@ public sealed class ShadowObjectRegistry
|
|||
_entityShapes.Remove(eid);
|
||||
_entityReg.Remove(eid);
|
||||
_suspendedEntities.Remove(eid);
|
||||
_suspendedEntityCells.Remove(eid);
|
||||
_withdrawnPrefixesByOwner.Remove(eid);
|
||||
}
|
||||
}
|
||||
|
|
@ -1367,6 +1585,9 @@ public sealed class ShadowObjectRegistry
|
|||
return false;
|
||||
}
|
||||
_entityToCells.TryGetValue(entityId, out List<uint>? cells);
|
||||
_suspendedEntityCells.TryGetValue(
|
||||
entityId,
|
||||
out List<uint>? suspendedCells);
|
||||
_entityShapes.TryGetValue(
|
||||
entityId,
|
||||
out IReadOnlyList<ShadowShape>? shapes);
|
||||
|
|
@ -1394,6 +1615,7 @@ public sealed class ShadowObjectRegistry
|
|||
cells is null ? null : new List<uint>(cells),
|
||||
rows,
|
||||
_suspendedEntities.Contains(entityId),
|
||||
suspendedCells is null ? null : new List<uint>(suspendedCells),
|
||||
withdrawn is null ? null : new HashSet<uint>(withdrawn));
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1405,6 +1627,8 @@ public sealed class ShadowObjectRegistry
|
|||
_entityShapes[state.EntityId] = state.Shapes;
|
||||
if (state.Suspended)
|
||||
_suspendedEntities.Add(state.EntityId);
|
||||
if (state.SuspendedCellIds is not null)
|
||||
_suspendedEntityCells[state.EntityId] = state.SuspendedCellIds;
|
||||
if (state.WithdrawnPrefixes is not null)
|
||||
{
|
||||
_withdrawnPrefixesByOwner[state.EntityId] = state.WithdrawnPrefixes;
|
||||
|
|
@ -1614,6 +1838,7 @@ public sealed class ShadowObjectRegistry
|
|||
List<uint>? CellIds,
|
||||
IReadOnlyList<PreparedShadowCellRows> Rows,
|
||||
bool Suspended,
|
||||
List<uint>? SuspendedCellIds,
|
||||
HashSet<uint>? WithdrawnPrefixes);
|
||||
|
||||
internal sealed record PreparedShadowCellRows(
|
||||
|
|
@ -1629,6 +1854,7 @@ public sealed class ShadowObjectRegistry
|
|||
_cells.Clear();
|
||||
_entityToCells.Clear();
|
||||
_suspendedEntities.Clear();
|
||||
_suspendedEntityCells.Clear();
|
||||
_withdrawnPrefixesByOwner.Clear();
|
||||
_entityShapes.Clear();
|
||||
_entityReg.Clear();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public sealed class ParentAttachmentState
|
|||
private readonly Dictionary<uint, ParentAttachmentRelation> _stagedByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
||||
private readonly Dictionary<ParentIncarnation, List<uint>> _committedChildrenByParent = new();
|
||||
|
||||
public int UnresolvedRelationCount =>
|
||||
_unresolvedByChild.Values.Sum(queue => queue.Count);
|
||||
|
|
@ -148,6 +149,9 @@ public sealed class ParentAttachmentState
|
|||
out ParentAttachmentRelation committed)
|
||||
&& committed == relation;
|
||||
|
||||
public bool HasCommittedParent(uint childGuid) =>
|
||||
_lastAcceptedByChild.ContainsKey(childGuid);
|
||||
|
||||
public bool IsPending(
|
||||
ParentAttachmentRelation relation,
|
||||
ParentProjectionCandidateKind kind) =>
|
||||
|
|
@ -184,7 +188,20 @@ public sealed class ParentAttachmentState
|
|||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RemoveCommittedChild(relation.ChildGuid);
|
||||
_lastAcceptedByChild[relation.ChildGuid] = relation;
|
||||
var parent = new ParentIncarnation(
|
||||
relation.ParentGuid,
|
||||
relation.ParentInstanceSequence);
|
||||
if (!_committedChildrenByParent.TryGetValue(
|
||||
parent,
|
||||
out List<uint>? children))
|
||||
{
|
||||
children = [];
|
||||
_committedChildrenByParent.Add(parent, children);
|
||||
}
|
||||
children.Add(relation.ChildGuid);
|
||||
_stagedByChild.Remove(relation.ChildGuid);
|
||||
_recoveryByChild[relation.ChildGuid] = relation;
|
||||
return true;
|
||||
|
|
@ -233,16 +250,34 @@ public sealed class ParentAttachmentState
|
|||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns only the exact direct children currently committed to one
|
||||
/// parent incarnation. Lost-cell destruction follows retail's live
|
||||
/// CHILDLIST and must not capture staged, unresolved, or future-generation
|
||||
/// relations that merely reuse the same parent GUID.
|
||||
/// </summary>
|
||||
public IReadOnlyList<uint> ChildrenAttachedToParent(
|
||||
uint parentGuid,
|
||||
ushort parentInstanceSequence)
|
||||
{
|
||||
var parent = new ParentIncarnation(parentGuid, parentInstanceSequence);
|
||||
return _committedChildrenByParent.TryGetValue(
|
||||
parent,
|
||||
out List<uint>? children)
|
||||
? children
|
||||
: Array.Empty<uint>();
|
||||
}
|
||||
|
||||
public void RemoveObject(uint guid)
|
||||
{
|
||||
_stagedByChild.Remove(guid);
|
||||
_recoveryByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
RemoveCommittedChild(guid);
|
||||
_unresolvedByChild.Remove(guid);
|
||||
|
||||
RemoveParentReferences(_stagedByChild, guid);
|
||||
RemoveParentReferences(_recoveryByChild, guid);
|
||||
RemoveParentReferences(_lastAcceptedByChild, guid);
|
||||
RemoveCommittedParentReferences(guid);
|
||||
|
||||
uint[] children = _unresolvedByChild.Keys.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
|
|
@ -268,10 +303,10 @@ public sealed class ParentAttachmentState
|
|||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||
_stagedByChild.Remove(guid);
|
||||
_recoveryByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
RemoveCommittedChild(guid);
|
||||
RemoveParentReferences(_stagedByChild, guid);
|
||||
RemoveParentReferences(_recoveryByChild, guid);
|
||||
RemoveParentReferences(_lastAcceptedByChild, guid);
|
||||
RemoveCommittedParentReferences(guid);
|
||||
FilterParentCandidates(
|
||||
guid,
|
||||
relation => relation.ParentInstanceSequence == replacementGeneration
|
||||
|
|
@ -292,10 +327,10 @@ public sealed class ParentAttachmentState
|
|||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||
_stagedByChild.Remove(guid);
|
||||
_recoveryByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
RemoveCommittedChild(guid);
|
||||
RemoveParentReferences(_stagedByChild, guid);
|
||||
RemoveParentReferences(_recoveryByChild, guid);
|
||||
RemoveParentReferences(_lastAcceptedByChild, guid);
|
||||
RemoveCommittedParentReferences(guid);
|
||||
FilterParentCandidates(
|
||||
guid,
|
||||
relation => PhysicsTimestampGate.IsNewer(
|
||||
|
|
@ -311,14 +346,14 @@ public sealed class ParentAttachmentState
|
|||
{
|
||||
_stagedByChild.Remove(childGuid);
|
||||
_recoveryByChild.Remove(childGuid);
|
||||
_lastAcceptedByChild.Remove(childGuid);
|
||||
RemoveCommittedChild(childGuid);
|
||||
}
|
||||
|
||||
public void RemoveChild(uint childGuid)
|
||||
{
|
||||
_stagedByChild.Remove(childGuid);
|
||||
_recoveryByChild.Remove(childGuid);
|
||||
_lastAcceptedByChild.Remove(childGuid);
|
||||
RemoveCommittedChild(childGuid);
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +363,50 @@ public sealed class ParentAttachmentState
|
|||
_stagedByChild.Clear();
|
||||
_recoveryByChild.Clear();
|
||||
_lastAcceptedByChild.Clear();
|
||||
foreach (List<uint> children in _committedChildrenByParent.Values)
|
||||
children.Clear();
|
||||
_committedChildrenByParent.Clear();
|
||||
}
|
||||
|
||||
private void RemoveCommittedChild(uint childGuid)
|
||||
{
|
||||
if (!_lastAcceptedByChild.Remove(
|
||||
childGuid,
|
||||
out ParentAttachmentRelation relation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parent = new ParentIncarnation(
|
||||
relation.ParentGuid,
|
||||
relation.ParentInstanceSequence);
|
||||
if (!_committedChildrenByParent.TryGetValue(
|
||||
parent,
|
||||
out List<uint>? children))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int childIndex = children.IndexOf(childGuid);
|
||||
if (childIndex >= 0)
|
||||
{
|
||||
int lastIndex = children.Count - 1;
|
||||
children[childIndex] = children[lastIndex];
|
||||
children.RemoveAt(lastIndex);
|
||||
}
|
||||
|
||||
if (children.Count == 0)
|
||||
_committedChildrenByParent.Remove(parent);
|
||||
}
|
||||
|
||||
private void RemoveCommittedParentReferences(uint parentGuid)
|
||||
{
|
||||
uint[] children = _lastAcceptedByChild
|
||||
.Where(pair => pair.Value.ParentGuid == parentGuid)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
RemoveCommittedChild(children[i]);
|
||||
}
|
||||
|
||||
private static void RemoveParentReferences(
|
||||
|
|
@ -377,6 +456,10 @@ public sealed class ParentAttachmentState
|
|||
else
|
||||
_unresolvedByChild[childGuid] = retained;
|
||||
}
|
||||
|
||||
private readonly record struct ParentIncarnation(
|
||||
uint ServerGuid,
|
||||
ushort InstanceSequence);
|
||||
}
|
||||
|
||||
public readonly record struct ParentAttachmentRelation(
|
||||
|
|
|
|||
|
|
@ -254,6 +254,12 @@ public sealed class RuntimeEntityDirectory
|
|||
record.AdvanceMovementCommit();
|
||||
}
|
||||
|
||||
public void AdvancePlacementCommit(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureKnown(record);
|
||||
record.AdvancePlacementCommit();
|
||||
}
|
||||
|
||||
public void AdvanceParentCommit(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureKnown(record);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Entities;
|
||||
|
||||
|
|
@ -14,6 +15,11 @@ public interface IRuntimeEntityObjectEventSource
|
|||
IDisposable Subscribe(IRuntimeEntityObjectObserver observer);
|
||||
}
|
||||
|
||||
public interface IRuntimePlacementObserver
|
||||
{
|
||||
void OnPlacement(in RuntimePlacementDelta delta);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One synchronous, generation-stamped commit stream for the canonical entity
|
||||
/// directory and retained object table. It owns no cross-frame queue and
|
||||
|
|
@ -31,6 +37,7 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
private readonly object _observerGate = new();
|
||||
private readonly List<PendingDispatch> _pendingDispatch = [];
|
||||
private IRuntimeEntityObjectObserver[] _observers = [];
|
||||
private IRuntimePlacementObserver[] _placementObservers = [];
|
||||
private Func<RuntimeGenerationToken> _generation = static () => default;
|
||||
private Func<ulong> _frameNumber = static () => 0UL;
|
||||
private bool _contextBound;
|
||||
|
|
@ -53,6 +60,8 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
|
||||
public ulong LastSequence => _sequencer.LastSequence;
|
||||
public int SubscriberCount => Volatile.Read(ref _observers).Length;
|
||||
public int PlacementSubscriberCount =>
|
||||
Volatile.Read(ref _placementObservers).Length;
|
||||
public int PendingDispatchCount => _pendingDispatch.Count;
|
||||
public bool IsDispatching => _dispatching;
|
||||
public long DispatchFailureCount { get; private set; }
|
||||
|
|
@ -107,6 +116,26 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
return new ObserverSubscription(this, observer);
|
||||
}
|
||||
|
||||
public IDisposable SubscribePlacement(IRuntimePlacementObserver observer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(observer);
|
||||
lock (_observerGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
IRuntimePlacementObserver[] current = _placementObservers;
|
||||
if (Array.IndexOf(current, observer) >= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The Runtime placement observer is already subscribed.");
|
||||
}
|
||||
var replacement = new IRuntimePlacementObserver[current.Length + 1];
|
||||
Array.Copy(current, replacement, current.Length);
|
||||
replacement[^1] = observer;
|
||||
Volatile.Write(ref _placementObservers, replacement);
|
||||
}
|
||||
return new PlacementObserverSubscription(this, observer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates the next stamp for another domain on the same Runtime event
|
||||
/// surface. J4–J6 will move those remaining domain publishers into
|
||||
|
|
@ -132,6 +161,15 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
EnqueueAndDrain(PendingDispatch.ForEntity(delta));
|
||||
}
|
||||
|
||||
internal void PublishPlacement(
|
||||
in RuntimePlacementProjectionSnapshot placement)
|
||||
{
|
||||
var delta = new RuntimePlacementDelta(
|
||||
NextStamp(),
|
||||
placement);
|
||||
EnqueueAndDrain(PendingDispatch.ForPlacement(delta));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_observerGate)
|
||||
|
|
@ -140,6 +178,7 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
return;
|
||||
_disposed = true;
|
||||
Volatile.Write(ref _observers, []);
|
||||
Volatile.Write(ref _placementObservers, []);
|
||||
_objects.Cleared -= OnObjectsCleared;
|
||||
_objects.ObjectRemovalClassified -= OnObjectRemoved;
|
||||
_objects.ObjectMoved -= OnObjectMoved;
|
||||
|
|
@ -155,6 +194,7 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
if (_disposed)
|
||||
return;
|
||||
Volatile.Write(ref _observers, []);
|
||||
Volatile.Write(ref _placementObservers, []);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +285,7 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
RuntimeEntityDelta delta = pending.Entity;
|
||||
observer.OnEntity(in delta);
|
||||
}
|
||||
else
|
||||
else if (pending.Kind is PendingDispatchKind.Inventory)
|
||||
{
|
||||
RuntimeInventoryDelta delta = pending.Inventory;
|
||||
observer.OnInventory(in delta);
|
||||
|
|
@ -256,6 +296,24 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (pending.Kind is PendingDispatchKind.Placement)
|
||||
{
|
||||
IRuntimePlacementObserver[] placementObservers =
|
||||
Volatile.Read(ref _placementObservers);
|
||||
foreach (IRuntimePlacementObserver observer in placementObservers)
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimePlacementDelta delta = pending.Placement;
|
||||
observer.OnPlacement(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordDispatchFailure(Exception error)
|
||||
|
|
@ -295,24 +353,54 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
}
|
||||
}
|
||||
|
||||
private void UnsubscribePlacement(IRuntimePlacementObserver observer)
|
||||
{
|
||||
lock (_observerGate)
|
||||
{
|
||||
IRuntimePlacementObserver[] current = _placementObservers;
|
||||
int index = Array.IndexOf(current, observer);
|
||||
if (index < 0)
|
||||
return;
|
||||
var replacement = new IRuntimePlacementObserver[current.Length - 1];
|
||||
if (index != 0)
|
||||
Array.Copy(current, 0, replacement, 0, index);
|
||||
if (index != current.Length - 1)
|
||||
{
|
||||
Array.Copy(
|
||||
current,
|
||||
index + 1,
|
||||
replacement,
|
||||
index,
|
||||
current.Length - index - 1);
|
||||
}
|
||||
Volatile.Write(ref _placementObservers, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
private enum PendingDispatchKind : byte
|
||||
{
|
||||
Entity,
|
||||
Inventory,
|
||||
Placement,
|
||||
}
|
||||
|
||||
private readonly record struct PendingDispatch(
|
||||
PendingDispatchKind Kind,
|
||||
RuntimeEntityDelta Entity,
|
||||
RuntimeInventoryDelta Inventory)
|
||||
RuntimeInventoryDelta Inventory,
|
||||
RuntimePlacementDelta Placement)
|
||||
{
|
||||
public static PendingDispatch ForEntity(
|
||||
RuntimeEntityDelta entity) =>
|
||||
new(PendingDispatchKind.Entity, entity, default);
|
||||
new(PendingDispatchKind.Entity, entity, default, default);
|
||||
|
||||
public static PendingDispatch ForInventory(
|
||||
RuntimeInventoryDelta inventory) =>
|
||||
new(PendingDispatchKind.Inventory, default, inventory);
|
||||
new(PendingDispatchKind.Inventory, default, inventory, default);
|
||||
|
||||
public static PendingDispatch ForPlacement(
|
||||
RuntimePlacementDelta placement) =>
|
||||
new(PendingDispatchKind.Placement, default, default, placement);
|
||||
}
|
||||
|
||||
private sealed class ObserverSubscription(
|
||||
|
|
@ -325,4 +413,17 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Unsubscribe(observer);
|
||||
}
|
||||
|
||||
|
||||
private sealed class PlacementObserverSubscription(
|
||||
RuntimeEntityObjectEventStream owner,
|
||||
IRuntimePlacementObserver observer)
|
||||
: IDisposable
|
||||
{
|
||||
private RuntimeEntityObjectEventStream? _owner = owner;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?
|
||||
.UnsubscribePlacement(observer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
|||
int EquipmentOwnerCount,
|
||||
int PendingMoveCount,
|
||||
int StreamSubscriberCount,
|
||||
int PlacementStreamSubscriberCount,
|
||||
long StreamDispatchFailureCount,
|
||||
bool HasLastStreamDispatchFailure,
|
||||
int PendingDispatchCount,
|
||||
|
|
@ -51,6 +52,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
|||
&& EquipmentOwnerCount == 0
|
||||
&& PendingMoveCount == 0
|
||||
&& StreamSubscriberCount == 0
|
||||
&& PlacementStreamSubscriberCount == 0
|
||||
&& PendingDispatchCount == 0
|
||||
&& !IsDispatching
|
||||
&& !IsSessionClearInProgress;
|
||||
|
|
@ -95,10 +97,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
public RuntimeEntityObjectLifetime(
|
||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IGameRuntimeClock? gameClock = null)
|
||||
{
|
||||
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
||||
Physics = new RuntimePhysicsState(Entities, timeProvider: timeProvider);
|
||||
Physics = new RuntimePhysicsState(
|
||||
Entities,
|
||||
timeProvider: timeProvider,
|
||||
gameClock: gameClock);
|
||||
Objects = new ClientObjectTable();
|
||||
// AP-129 (Campaign P Slice P4 review fix, 2026-07-30): the physics
|
||||
// entry-restriction gate (ObjectInfo.CheckEntryRestrictions) resolves
|
||||
|
|
@ -111,44 +117,51 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
EntityView = views.Entities;
|
||||
InventoryView = views.Inventory;
|
||||
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
||||
Physics.SetPosition.BindEventStream(Events);
|
||||
}
|
||||
|
||||
internal RuntimeEntityObjectLifetime(
|
||||
PhysicsDataCache physicsDataCache,
|
||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IGameRuntimeClock? gameClock = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(physicsDataCache);
|
||||
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
||||
Physics = new RuntimePhysicsState(
|
||||
Entities,
|
||||
physicsDataCache,
|
||||
timeProvider);
|
||||
timeProvider,
|
||||
gameClock);
|
||||
Objects = new ClientObjectTable();
|
||||
Physics.Engine.Objects = Objects;
|
||||
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
||||
EntityView = views.Entities;
|
||||
InventoryView = views.Inventory;
|
||||
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
||||
Physics.SetPosition.BindEventStream(Events);
|
||||
}
|
||||
|
||||
internal RuntimeEntityObjectLifetime(
|
||||
PhysicsEngine physicsEngine,
|
||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IGameRuntimeClock? gameClock = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(physicsEngine);
|
||||
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
||||
Physics = new RuntimePhysicsState(
|
||||
Entities,
|
||||
physicsEngine,
|
||||
timeProvider);
|
||||
timeProvider,
|
||||
gameClock);
|
||||
Objects = new ClientObjectTable();
|
||||
Physics.Engine.Objects = Objects;
|
||||
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
||||
EntityView = views.Entities;
|
||||
InventoryView = views.Inventory;
|
||||
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
||||
Physics.SetPosition.BindEventStream(Events);
|
||||
}
|
||||
|
||||
public RuntimeEntityDirectory Entities { get; }
|
||||
|
|
@ -176,6 +189,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Objects.EquipmentOwnerCount,
|
||||
Objects.PendingMoveCount,
|
||||
Events.SubscriberCount,
|
||||
Events.PlacementSubscriberCount,
|
||||
Events.DispatchFailureCount,
|
||||
Events.LastDispatchFailure is not null,
|
||||
Events.PendingDispatchCount,
|
||||
|
|
@ -421,6 +435,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
Physics.SetPosition.Forget(
|
||||
canonical,
|
||||
releasePreparedMover: true);
|
||||
Physics.RemoveSpatialProjection(canonical);
|
||||
Entities.SetRemoteMotion(canonical, null);
|
||||
Entities.SetRemoteMotionBindingInProgress(canonical, false);
|
||||
|
|
@ -432,6 +450,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false);
|
||||
Entities.SetHasPartArray(canonical, false);
|
||||
Entities.ReleaseLocalId(canonical);
|
||||
Physics.SetPosition.PublishCancellation(cancellation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -510,6 +529,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvancePositionAuthority(canonical);
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
Physics.SetPosition.Forget(canonical);
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
||||
|
|
@ -520,7 +541,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Withdrawn,
|
||||
() => canonical.PositionAuthorityVersion == positionVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
||||
cancellation);
|
||||
}
|
||||
|
||||
public bool TryApplyCreateParent(
|
||||
|
|
@ -596,6 +618,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
Physics.SetPosition.Forget(canonical);
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
|
|
@ -605,7 +629,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
RuntimeEntityChange.Withdrawn,
|
||||
() => canonical.PositionAuthorityVersion
|
||||
== positionAuthorityVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
||||
cancellation);
|
||||
}
|
||||
|
||||
public bool TryApplyMotion(
|
||||
|
|
@ -764,6 +789,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
RuntimePlacementCancellationReceipt cancellation = acceptedPosition
|
||||
? Physics.SetPosition.Forget(canonical)
|
||||
: default;
|
||||
Entities.RefreshSnapshot(
|
||||
canonical,
|
||||
snapshot,
|
||||
|
|
@ -793,7 +821,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
? RuntimeEntityChange.Rebucketed
|
||||
: RuntimeEntityChange.Updated,
|
||||
() => canonical.PositionAuthorityVersion == positionVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
||||
cancellation);
|
||||
}
|
||||
|
||||
public bool CommitRebucket(
|
||||
|
|
@ -915,8 +944,13 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
&& active.Incarnation == delete.InstanceSequence
|
||||
&& Entities.RemoveActive(active))
|
||||
{
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
Physics.SetPosition.Forget(
|
||||
active,
|
||||
releasePreparedMover: true);
|
||||
retiredCanonical = active;
|
||||
Entities.RetainTeardown(active);
|
||||
Physics.SetPosition.PublishCancellation(cancellation);
|
||||
PublishEntity(
|
||||
removeRetainedObject
|
||||
? RuntimeEntityChange.Deleted
|
||||
|
|
@ -985,10 +1019,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
return Array.Empty<RuntimeEntityRecord>();
|
||||
|
||||
_sessionClearInProgress = true;
|
||||
Physics.SetPosition.ResetSession();
|
||||
Entities.BeginSessionClear();
|
||||
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
|
||||
foreach (RuntimeEntityRecord canonical in active)
|
||||
{
|
||||
Physics.SetPosition.Forget(canonical);
|
||||
if (!Entities.RemoveActive(canonical))
|
||||
continue;
|
||||
Entities.RetainTeardown(canonical);
|
||||
|
|
@ -1119,6 +1155,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvancePositionAuthority(canonical);
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
Physics.SetPosition.Forget(canonical);
|
||||
ulong positionVersion = canonical.PositionAuthorityVersion;
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
|
|
@ -1126,7 +1164,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.PositionAuthorityVersion == positionVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion,
|
||||
cancellation);
|
||||
}
|
||||
|
||||
private void PublishEntity(
|
||||
|
|
@ -1138,8 +1177,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
RuntimeEntityRecord canonical,
|
||||
Action acknowledgeProjection,
|
||||
RuntimeEntityChange change,
|
||||
Func<bool> matchesCommittedMutation)
|
||||
Func<bool> matchesCommittedMutation,
|
||||
RuntimePlacementCancellationReceipt cancellation = default)
|
||||
{
|
||||
Physics.SetPosition.PublishCancellation(cancellation);
|
||||
if (!IsExpectedCanonical(canonical, matchesCommittedMutation))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
acknowledgeProjection();
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public sealed class RuntimeEntityRecord
|
|||
public uint RawPhysicsState { get; internal set; }
|
||||
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
|
||||
public ulong SpatialAuthorityVersion { get; private set; }
|
||||
public ulong PlacementCommitVersion { get; private set; }
|
||||
public ulong PhysicsStateMutationVersion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -136,6 +137,8 @@ public sealed class RuntimeEntityRecord
|
|||
|
||||
internal void AdvanceMovementCommit() => MovementCommitVersion++;
|
||||
|
||||
internal void AdvancePlacementCommit() => PlacementCommitVersion++;
|
||||
|
||||
internal void AdvanceParentCommit() => ParentCommitVersion++;
|
||||
|
||||
internal void AdvanceObjDescAuthority() => ObjDescAuthorityVersion++;
|
||||
|
|
|
|||
|
|
@ -178,7 +178,8 @@ public sealed class GameRuntime
|
|||
|
||||
context.EntityObjects = new RuntimeEntityObjectLifetime(
|
||||
dependencies.FirstLocalEntityId,
|
||||
dependencies.TimeProvider);
|
||||
dependencies.TimeProvider,
|
||||
clock);
|
||||
construction.Own(context.EntityObjects);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.EntityObjectsCreated,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
namespace AcDream.Runtime;
|
||||
|
||||
|
|
@ -50,6 +51,10 @@ public readonly record struct RuntimeEntityDelta(
|
|||
RuntimeEntityChange Change,
|
||||
RuntimeEntitySnapshot Entity);
|
||||
|
||||
public readonly record struct RuntimePlacementDelta(
|
||||
RuntimeEventStamp Stamp,
|
||||
RuntimePlacementProjectionSnapshot Placement);
|
||||
|
||||
public enum RuntimeInventoryChange
|
||||
{
|
||||
Added,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,20 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
|||
int SpatialRootCount,
|
||||
int SpatialRemoteCount,
|
||||
int SpatialProjectileCount,
|
||||
int SetPositionOperationCount,
|
||||
int AwaitingSetPositionPreparationCount,
|
||||
int DeferredSetPositionCount,
|
||||
int PendingSetPositionHostAcknowledgementCount,
|
||||
int LostCellDeadlineCount,
|
||||
int LostCellDeadlineNodeCount,
|
||||
int LostCellDeadlineIndexCount,
|
||||
int ExpiredLostCellCount,
|
||||
int ExpiredLostCellIndexCount,
|
||||
int DeferredSetPositionBucketCount,
|
||||
int DeferredSetPositionBucketOrderCount,
|
||||
int UnboundDeferredSetPositionCellCount,
|
||||
int UnboundDeferredSetPositionCellOrderCount,
|
||||
int PreparedSetPositionMoverCount,
|
||||
int CollisionAdmissionCount,
|
||||
int CollisionGenerationCount,
|
||||
bool OwnsProductionDataCache,
|
||||
|
|
@ -21,6 +35,20 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
|||
&& SpatialRootCount == 0
|
||||
&& SpatialRemoteCount == 0
|
||||
&& SpatialProjectileCount == 0
|
||||
&& SetPositionOperationCount == 0
|
||||
&& AwaitingSetPositionPreparationCount == 0
|
||||
&& DeferredSetPositionCount == 0
|
||||
&& PendingSetPositionHostAcknowledgementCount == 0
|
||||
&& LostCellDeadlineCount == 0
|
||||
&& LostCellDeadlineNodeCount == 0
|
||||
&& LostCellDeadlineIndexCount == 0
|
||||
&& ExpiredLostCellCount == 0
|
||||
&& ExpiredLostCellIndexCount == 0
|
||||
&& DeferredSetPositionBucketCount == 0
|
||||
&& DeferredSetPositionBucketOrderCount == 0
|
||||
&& UnboundDeferredSetPositionCellCount == 0
|
||||
&& UnboundDeferredSetPositionCellOrderCount == 0
|
||||
&& PreparedSetPositionMoverCount == 0
|
||||
&& CollisionAdmissionCount == 0
|
||||
&& CollisionGenerationCount == 0
|
||||
&& OwnsProductionDataCache;
|
||||
|
|
@ -979,6 +1007,7 @@ internal readonly record struct RuntimeCollisionSealStep(
|
|||
public sealed class RuntimePhysicsState : IDisposable
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGameRuntimeClock? _gameClock;
|
||||
private readonly Dictionary<RuntimeEntityKey, RuntimeEntityRecord>
|
||||
_spatialRoots = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
|
||||
|
|
@ -1020,10 +1049,12 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
internal RuntimePhysicsState(
|
||||
RuntimeEntityDirectory entities,
|
||||
PhysicsDataCache? dataCache = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IGameRuntimeClock? gameClock = null)
|
||||
{
|
||||
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_gameClock = gameClock;
|
||||
DataCache = dataCache ?? PhysicsDataCache.CreateProduction();
|
||||
Engine = new PhysicsEngine
|
||||
{
|
||||
|
|
@ -1032,15 +1063,18 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
Engine.ShadowObjects.OwnerMutated += OnCollisionOwnerMutated;
|
||||
Engine.ShadowObjects.OwnerPrefixMembershipChanged +=
|
||||
OnCollisionOwnerPrefixMembershipChanged;
|
||||
SetPosition = new RuntimeSetPositionState(this, Entities);
|
||||
}
|
||||
|
||||
internal RuntimePhysicsState(
|
||||
RuntimeEntityDirectory entities,
|
||||
PhysicsEngine engine,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IGameRuntimeClock? gameClock = null)
|
||||
{
|
||||
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_gameClock = gameClock;
|
||||
Engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
||||
DataCache = engine.DataCache
|
||||
?? PhysicsDataCache.CreateProduction(engine.CollisionWorld);
|
||||
|
|
@ -1048,11 +1082,13 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
Engine.ShadowObjects.OwnerMutated += OnCollisionOwnerMutated;
|
||||
Engine.ShadowObjects.OwnerPrefixMembershipChanged +=
|
||||
OnCollisionOwnerPrefixMembershipChanged;
|
||||
SetPosition = new RuntimeSetPositionState(this, Entities);
|
||||
}
|
||||
|
||||
internal RuntimeEntityDirectory Entities { get; }
|
||||
public PhysicsEngine Engine { get; }
|
||||
public PhysicsDataCache DataCache { get; }
|
||||
internal RuntimeSetPositionState SetPosition { get; }
|
||||
public int SpatialRootCount => _spatialRoots.Count;
|
||||
public int SpatialRemoteCount => _spatialRemotes.Count;
|
||||
public int SpatialProjectileCount => _spatialProjectiles.Count;
|
||||
|
|
@ -1061,18 +1097,41 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
internal double UtcNowSeconds =>
|
||||
(_timeProvider.GetUtcNow() - DateTimeOffset.UnixEpoch)
|
||||
.TotalSeconds;
|
||||
internal double MonotonicNowSeconds =>
|
||||
_timeProvider.GetTimestamp()
|
||||
/ (double)_timeProvider.TimestampFrequency;
|
||||
internal double PlacementSimulationTime(double fallback) =>
|
||||
_gameClock?.SimulationTimeSeconds ?? fallback;
|
||||
|
||||
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
|
||||
new(
|
||||
public RuntimePhysicsOwnershipSnapshot CaptureOwnership()
|
||||
{
|
||||
RuntimeSetPositionOwnershipSnapshot setPosition =
|
||||
SetPosition.CaptureOwnership();
|
||||
return new(
|
||||
Engine.LandblockCount,
|
||||
Engine.ShadowObjects.RetainedRegistrationCount,
|
||||
_spatialRoots.Count,
|
||||
_spatialRemotes.Count,
|
||||
_spatialProjectiles.Count,
|
||||
setPosition.ActiveOperationCount,
|
||||
setPosition.AwaitingPreparationCount,
|
||||
setPosition.DeferredCellCount,
|
||||
setPosition.PendingProjectionAcknowledgementCount,
|
||||
setPosition.LostDeadlineCount,
|
||||
setPosition.LostDeadlineNodeCount,
|
||||
setPosition.LostDeadlineIndexCount,
|
||||
setPosition.ExpiredLostCellCount,
|
||||
setPosition.ExpiredLostCellIndexCount,
|
||||
setPosition.DeferredBucketCount,
|
||||
setPosition.DeferredBucketOrderCount,
|
||||
setPosition.UnboundDeferredCellCount,
|
||||
setPosition.UnboundDeferredCellOrderCount,
|
||||
setPosition.PreparedMoverCount,
|
||||
_collisionAdmissions.Count,
|
||||
_collisionGenerations.Count,
|
||||
ReferenceEquals(Engine.DataCache, DataCache),
|
||||
_disposed);
|
||||
}
|
||||
|
||||
public void AcknowledgeSpatialProjection(
|
||||
RuntimeEntityRecord record,
|
||||
|
|
@ -1806,6 +1865,20 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
EnsureNotDisposed();
|
||||
EnsureCollisionMutationThread();
|
||||
uint canonical = CanonicalLandblock(landblockId);
|
||||
if (_collisionAdmissions.Remove(
|
||||
canonical,
|
||||
out RuntimeCollisionAdmission? superseded))
|
||||
{
|
||||
SetPosition.CancelCollisionGeneration(
|
||||
canonical,
|
||||
superseded.Generation);
|
||||
if (_preparedCollisionGenerations.Remove(
|
||||
canonical,
|
||||
out PreparedLandblockCollisionGeneration? prepared))
|
||||
{
|
||||
prepared.Dispose();
|
||||
}
|
||||
}
|
||||
ulong generation = _collisionGenerations.TryGetValue(
|
||||
canonical,
|
||||
out ulong current)
|
||||
|
|
@ -1817,6 +1890,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
canonical,
|
||||
generation);
|
||||
_collisionAdmissions[canonical] = admission;
|
||||
SetPosition.BeginCollisionGeneration(canonical, generation);
|
||||
return admission;
|
||||
}
|
||||
|
||||
|
|
@ -1901,6 +1975,9 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
&& ReferenceEquals(current, admission))
|
||||
{
|
||||
_collisionAdmissions.Remove(admission.LandblockId);
|
||||
SetPosition.CancelCollisionGeneration(
|
||||
admission.LandblockId,
|
||||
admission.Generation);
|
||||
_collisionGenerations[admission.LandblockId] = checked(
|
||||
admission.Generation + 1UL);
|
||||
}
|
||||
|
|
@ -2137,6 +2214,10 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
admission.Generation,
|
||||
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||
Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
|
||||
SetPosition.CommitCollisionGeneration(
|
||||
acknowledgement.LandblockId,
|
||||
acknowledgement.Generation,
|
||||
acknowledgement.Ready);
|
||||
PublishCollisionGenerationCommitted(
|
||||
new RuntimeCollisionGenerationCommitted(
|
||||
acknowledgement.LandblockId,
|
||||
|
|
@ -2221,6 +2302,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
prepared.Dispose();
|
||||
}
|
||||
_preparedCollisionGenerations.Clear();
|
||||
SetPosition.Dispose();
|
||||
_collisionOwnerJournal.Clear();
|
||||
_collisionOwnerSubscribers.Clear();
|
||||
Engine.Clear();
|
||||
|
|
@ -2380,12 +2462,22 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
|
||||
private void InvalidateCollisionAdmission(uint landblockId)
|
||||
{
|
||||
ulong generation = _collisionGenerations.TryGetValue(
|
||||
ulong currentGeneration = _collisionGenerations.TryGetValue(
|
||||
landblockId,
|
||||
out ulong current)
|
||||
? checked(current + 1UL)
|
||||
: 1UL;
|
||||
? current
|
||||
: 0UL;
|
||||
ulong invalidatedGeneration = _collisionAdmissions.TryGetValue(
|
||||
landblockId,
|
||||
out RuntimeCollisionAdmission? admission)
|
||||
? admission.Generation
|
||||
: checked(currentGeneration + 1UL);
|
||||
ulong generation = checked(
|
||||
Math.Max(currentGeneration, invalidatedGeneration) + 1UL);
|
||||
_collisionGenerations[landblockId] = generation;
|
||||
SetPosition.CancelCollisionGeneration(
|
||||
landblockId,
|
||||
invalidatedGeneration);
|
||||
_collisionAdmissions.Remove(landblockId);
|
||||
if (_preparedCollisionGenerations.Remove(
|
||||
landblockId,
|
||||
|
|
@ -2396,6 +2488,70 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
TrimCollisionOwnerJournal();
|
||||
}
|
||||
|
||||
internal ulong ExpectedCollisionGeneration(uint exactCellId)
|
||||
{
|
||||
uint landblockId = CanonicalLandblock(exactCellId);
|
||||
if (landblockId == 0u)
|
||||
return 0UL;
|
||||
if (_collisionAdmissions.TryGetValue(
|
||||
landblockId,
|
||||
out RuntimeCollisionAdmission? admission))
|
||||
{
|
||||
return admission.Generation;
|
||||
}
|
||||
return _collisionGenerations.TryGetValue(
|
||||
landblockId,
|
||||
out ulong generation)
|
||||
? checked(generation + 1UL)
|
||||
: 1UL;
|
||||
}
|
||||
|
||||
internal bool HandleSetPositionCollisions(
|
||||
RuntimeEntityRecord record,
|
||||
ulong positionAuthorityVersion,
|
||||
ulong spatialAuthorityVersion,
|
||||
ulong velocityAuthorityVersion,
|
||||
bool previousContact,
|
||||
bool previousOnWalkable,
|
||||
in PhysicsSetPositionCollisionReport report)
|
||||
{
|
||||
if (!Entities.IsCurrent(record)
|
||||
|| record.PositionAuthorityVersion != positionAuthorityVersion
|
||||
|| record.SpatialAuthorityVersion != spatialAuthorityVersion
|
||||
|| (velocityAuthorityVersion != 0UL
|
||||
&& record.VelocityAuthorityVersion
|
||||
!= velocityAuthorityVersion)
|
||||
|| record.PhysicsBody is not { } body)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
body.FramesStationaryFall = report.FramesStationaryFall;
|
||||
PhysicsObjUpdate.HandleAllCollisions(
|
||||
body,
|
||||
report.CollisionNormalValid,
|
||||
report.CollisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable,
|
||||
body.OnWalkable);
|
||||
body.TransientState &= ~(TransientStateFlags.StationaryFall
|
||||
| TransientStateFlags.StationaryStop
|
||||
| TransientStateFlags.StationaryStuck);
|
||||
body.TransientState |= report.FramesStationaryFall switch
|
||||
{
|
||||
1 => TransientStateFlags.StationaryFall,
|
||||
2 => TransientStateFlags.StationaryStop,
|
||||
3 => TransientStateFlags.StationaryStuck,
|
||||
_ => TransientStateFlags.None,
|
||||
};
|
||||
// Retail returns the result of collision reporting, not a collision-
|
||||
// presence guess. Runtime does not yet own the per-object report/
|
||||
// tracking table required to reproduce that return value, so fail
|
||||
// closed. This preserves ordinary placement rejection and leaves the
|
||||
// already-registered reporting seam explicit for the 4B2 cutover.
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnCollisionOwnerMutated(uint ownerId, ulong version)
|
||||
{
|
||||
_ = version;
|
||||
|
|
|
|||
2014
src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
Normal file
2014
src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue