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

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