feat(runtime): seal dormant SetPosition evaluations
This commit is contained in:
parent
22651c823d
commit
99f867f053
16 changed files with 2298 additions and 62 deletions
|
|
@ -2,14 +2,16 @@
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This is placement Slice 4B2 checkpoint 4. It adds the dormant,
|
This is placement Slice 4B2 checkpoints 4-5. It adds the dormant,
|
||||||
presentation-independent transaction which prepares and assigns ownership of
|
presentation-independent transaction which prepares and assigns ownership of
|
||||||
one local-player `PhysicsBody` and `PlayerMovementController`. No App or
|
one local-player `PhysicsBody` and `PlayerMovementController`, then retains an
|
||||||
|
exact post-ownership activation lease which can evaluate retail SetPosition
|
||||||
|
without publishing it. No App or
|
||||||
Headless production route invokes this transaction, so graphical and no-window
|
Headless production route invokes this transaction, so graphical and no-window
|
||||||
game behavior is unchanged and AP-1/AD-1 remain open.
|
game behavior is unchanged and AP-1/AD-1 remain open.
|
||||||
|
|
||||||
The checkpoint deliberately stops before canonical SetPosition activation.
|
The checkpoints deliberately stop before canonical SetPosition activation.
|
||||||
It does not consume the prepared placement operation, enter the body into the
|
They do not consume the prepared placement operation, enter the body into the
|
||||||
physics engine, publish FullCell/world/host/shadow/workset state, or project a
|
physics engine, publish FullCell/world/host/shadow/workset state, or project a
|
||||||
presentation entity. Those effects belong to the next transaction and must all
|
presentation entity. Those effects belong to the next transaction and must all
|
||||||
use the same Runtime-owned dormant body.
|
use the same Runtime-owned dormant body.
|
||||||
|
|
@ -88,6 +90,70 @@ Repeated stores of the same body/controller do not advance their epochs; real
|
||||||
bind, replacement, and unbind edges do. This makes ABA-shaped reference changes
|
bind, replacement, and unbind edges do. This makes ABA-shaped reference changes
|
||||||
observable even if a later value happens to equal an earlier reference.
|
observable even if a later value happens to equal an earlier reference.
|
||||||
|
|
||||||
|
## Dormant SetPosition evaluation lease
|
||||||
|
|
||||||
|
Ownership commit now returns one private activation token captured only after
|
||||||
|
the canonical body and controller stores. It binds the exact entity key,
|
||||||
|
authored placement token and sealed command, local identity GUID/revision,
|
||||||
|
session lifetime, and the post-store physics-body, object-clock, and controller
|
||||||
|
ownership epochs. The owner retains the same record, body, controller, and
|
||||||
|
command behind that token; no caller can substitute an equivalent-looking
|
||||||
|
body or rebuild the mover.
|
||||||
|
|
||||||
|
`EvaluateActivation` revalidates that complete lease and calls Core
|
||||||
|
`PhysicsEngine.SetPosition` synchronously with an immutable request. Core's
|
||||||
|
transaction is pure: it returns committed, deferred-cell, or rejected
|
||||||
|
placement data without writing the canonical body, FullCell, clock, spatial
|
||||||
|
worksets, shadows, collision-report owners, host, operation stage, or Place
|
||||||
|
projection. A missing cell therefore leaves the exact body dormant and the
|
||||||
|
authored operation retryable. A valid result likewise remains only an
|
||||||
|
immutable evaluation receipt; this checkpoint has no activation/commit API.
|
||||||
|
|
||||||
|
Each evaluation carries an append-only, stable-order union of every cell read
|
||||||
|
by the complete Core transaction: the AdjustPosition seed and adjusted cell,
|
||||||
|
visible-child probes (including rejected lateral siblings), rejected
|
||||||
|
portal/building containment probes, transition/compass retries, and every
|
||||||
|
normal or scatter attempt. Rejected probes enter only the authority union and
|
||||||
|
never the final successful shadow/CrossCell footprint. Scatter keeps that union
|
||||||
|
in retained scratch and materializes its
|
||||||
|
immutable receipt exactly once after the final attempt, avoiding quadratic
|
||||||
|
copy/allocation growth at the 64-attempt retail ceiling. The final
|
||||||
|
`CrossCellIds` remains the successful placement's authored
|
||||||
|
shadow footprint; failed scatter probes cannot leak into that commit payload.
|
||||||
|
Runtime seals every distinct queried landblock against the exact collision
|
||||||
|
generation, the global collision-world authority, and the dynamic-shadow
|
||||||
|
mutation revision. An active replacement admission rejects evaluation even
|
||||||
|
before it commits, while begin/cancel, a re-entrant generation commit, or any
|
||||||
|
owner insert/remove/move/state/suspend/reflood mutation invalidates an older
|
||||||
|
receipt.
|
||||||
|
|
||||||
|
Entry restrictions also consult the live `ClientObjectTable` for the resolved
|
||||||
|
house object, owner and complete restriction record, plus the mover's monarch.
|
||||||
|
The receipt therefore seals the exact object-table reference, the engine's
|
||||||
|
monotonic binding epoch, and the table's synchronous mutation revision. Object
|
||||||
|
creation/removal, owner-property, guest-list, or mover-monarch updates invalidate
|
||||||
|
the receipt; a null/fresh replacement and an equal-revision A-B-A binding cycle
|
||||||
|
cannot resurrect it. Retained `ClientObject` owner, monarch, and restriction
|
||||||
|
setters synchronously advance every exact owning table even when callers mutate
|
||||||
|
the object directly rather than re-submit it through `AddOrUpdate`. Replacement,
|
||||||
|
removal, and clear detach that observer exactly, and every
|
||||||
|
`HouseRestrictionRecord` freezes a defensive snapshot of its input guest map so
|
||||||
|
no caller-owned dictionary or mutable downcast can alter entry authority behind
|
||||||
|
the revision.
|
||||||
|
|
||||||
|
`IsEvaluationCurrent` accepts only the newest receipt for the exact activation
|
||||||
|
lease and rejects it after a position/vector/state/object-description/Create
|
||||||
|
authority change, identity revision, body/controller replacement, session or
|
||||||
|
incarnation change, or any sealed collision/shadow authority change.
|
||||||
|
Re-evaluation supersedes the older receipt without mutating world state.
|
||||||
|
Re-entrant reset or delete-plus-GUID-reuse during Core evaluation immediately
|
||||||
|
retires the invalid lease instead of leaving an orphaned dormant graph. An
|
||||||
|
existing activation lease also blocks candidate preparation even if an
|
||||||
|
external owner has already cleared the body/controller references; explicit
|
||||||
|
discard is required before a new candidate can be prepared. Reset and disposal
|
||||||
|
retire the lease, body, and dormant controller and include the pending
|
||||||
|
activation in the ownership convergence ledger.
|
||||||
|
|
||||||
## Gates
|
## Gates
|
||||||
|
|
||||||
- Candidate privacy and live-operation rejection.
|
- Candidate privacy and live-operation rejection.
|
||||||
|
|
@ -104,23 +170,53 @@ observable even if a later value happens to equal an earlier reference.
|
||||||
placement cancellation.
|
placement cancellation.
|
||||||
- Delete plus same-GUID reincarnation.
|
- Delete plus same-GUID reincarnation.
|
||||||
- Candidate replacement, reset, disposal, and ownership convergence.
|
- Candidate replacement, reset, disposal, and ownership convergence.
|
||||||
|
- Pure committed/deferred/rejected SetPosition evaluation with bit-exact
|
||||||
|
body-state snapshots and no canonical, collision-report, projection,
|
||||||
|
clock, FullCell, host, shadow, workset, or operation-stage mutation.
|
||||||
|
- Complete stable-order queried-cell capture across AdjustPosition,
|
||||||
|
visible-child lookup, normal/scatter retries, map-edge/deferred, rejected,
|
||||||
|
committed, and defensive NoCell outcomes. Scatter deliberately retains
|
||||||
|
retail's RNG consumption; only its authority footprint and commit payload
|
||||||
|
are deterministic for a fixed draw sequence.
|
||||||
|
- Newest-receipt selection, active-admission rejection, collision-generation
|
||||||
|
replacement, re-entrant begin/cancel and commit invalidation, plus dynamic
|
||||||
|
shadow insert/move/state/suspend/remove invalidation.
|
||||||
|
- Exact object-table reference/revision/binding authority, including
|
||||||
|
post-evaluation and re-entrant house-object, owner, guest-list, and mover-
|
||||||
|
monarch mutations plus null/fresh/equal-revision ABA replacement. Direct
|
||||||
|
retained-object setters, replacement/removal/clear observer lifetime, shared
|
||||||
|
multi-table ownership, and frozen guest-map input are covered explicitly.
|
||||||
|
- Re-entrant reset and delete/GUID-reuse convergence plus activation-lease
|
||||||
|
overwrite prevention after an external body/controller clear.
|
||||||
|
- Post-ownership position, vector, object-description, Create, identity,
|
||||||
|
body, and controller authority replacement.
|
||||||
- Terminal stale-controller rejection after replacement, reset, and disposal.
|
- Terminal stale-controller rejection after replacement, reset, and disposal.
|
||||||
- Body/controller epochs advance only on actual ownership changes.
|
- Body/controller epochs advance only on actual ownership changes.
|
||||||
|
|
||||||
Focused publication, controller, movement, and SetPosition tests pass 167/167.
|
The checkpoint-5 focused publication suite passes 70/70 and the focused Core
|
||||||
The complete Runtime project passes 627/627 under invariant culture. The App
|
SetPosition suite passes 59/59. The complete Runtime project passes 666/666;
|
||||||
Runtime-ownership guard passes 4/4, the complete Release solution builds with
|
the complete Core project passes 4,230 tests / 1 skip; and the App Runtime
|
||||||
zero errors, and the complete Release solution test gate passes 10,374 tests
|
physics/movement ownership guard passes 6/6. The complete Release solution
|
||||||
with 4 intentional skips. Under the machine's Swedish current culture, the
|
builds with zero errors (three pre-existing App-test nullability warnings are
|
||||||
three previously known formatting assertions still fail (`0,5` versus `0.5`
|
outside this checkpoint), and its invariant-culture test gate passes 10,419
|
||||||
and localized sky text); they are unrelated to this checkpoint.
|
tests / 4 intentional skips. The installed prepared-package gate
|
||||||
|
uses the exact local `acdream.pak`. Under the machine's Swedish current culture,
|
||||||
|
the same three previously known formatting assertions remain unrelated (`0,5`
|
||||||
|
versus `0.5` and localized sky text), so the canonical gate runs under
|
||||||
|
invariant culture.
|
||||||
|
|
||||||
## Next checkpoint
|
## Next checkpoint
|
||||||
|
|
||||||
Add the canonical Runtime SetPosition activation transaction. It must evaluate
|
Add the canonical Runtime SetPosition activation transaction. Before its
|
||||||
and commit the already-owned dormant body, consume the exact prepared placement,
|
callback-free tail, it must prepare a presentation-independent Runtime
|
||||||
and atomically establish physics-engine/workset/shadow/FullCell/world/host
|
`PhysicsHost`/`PositionManager`/`MoveToManager` graph, the existing authoritative
|
||||||
ownership before presentation receives an acknowledgement, then invoke the sole
|
authored shadow payload (without rebuilding AP-22 shapes), and an exact staged
|
||||||
`ActivateRuntimePublication` transition. The activation must
|
collision-report batch. Then it may commit the already-owned dormant body,
|
||||||
roll back or leave the operation retryable on every pre-commit failure and must
|
contact/water/walkable/response state, object clock, FullCell, exact shadow,
|
||||||
not construct a second body or controller.
|
spatial/ordinary workset, host, and SetPosition operation versions; seal the
|
||||||
|
ordered Place receipt; and invoke the sole `ActivateRuntimePublication`
|
||||||
|
transition as the final tickable edge. Collision reports and the Place observer
|
||||||
|
publish only afterward and must tolerate delete/reset re-entry without replaying
|
||||||
|
physics. Every pre-commit failure leaves the dormant lease and authored
|
||||||
|
operation retryable or rejects them; no rollback mutation and no second body or
|
||||||
|
controller are allowed.
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,17 @@ public sealed class PropertyBundle
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ClientObject
|
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 ObjectId { get; init; }
|
||||||
public uint WeenieClassId { get; set; } // "blueprint"
|
public uint WeenieClassId { get; set; } // "blueprint"
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
|
|
@ -273,7 +284,16 @@ public sealed class ClientObject
|
||||||
/// object (wire <c>WeenieHeaderFlag.Owner</c>, 0x02000000). Zero or a
|
/// object (wire <c>WeenieHeaderFlag.Owner</c>, 0x02000000). Zero or a
|
||||||
/// match against the mover's own id admits regardless of the guest list.
|
/// match against the mover's own id admits regardless of the guest list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint? HouseOwnerId { get; set; }
|
public uint? HouseOwnerId
|
||||||
|
{
|
||||||
|
get => _houseOwnerId;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_houseOwnerId == value) return;
|
||||||
|
_houseOwnerId = value;
|
||||||
|
RestrictionAuthorityChanged?.Invoke(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AP-129 (Campaign P Slice P4 review fix): retail <c>PublicWeenieDesc
|
/// AP-129 (Campaign P Slice P4 review fix): retail <c>PublicWeenieDesc
|
||||||
/// ._monarch_iid</c> (wire <c>WeenieHeaderFlag.Monarch</c>, 0x40) — this
|
/// ._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>
|
/// objects; a player's own value is what <c>RestrictionDB::IsAllowedIn</c>
|
||||||
/// compares against a house's <see cref="HouseRestrictionRecord.AllegianceMonarchId"/>.
|
/// compares against a house's <see cref="HouseRestrictionRecord.AllegianceMonarchId"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint? MonarchId { get; set; }
|
public uint? MonarchId
|
||||||
|
{
|
||||||
|
get => _monarchId;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_monarchId == value) return;
|
||||||
|
_monarchId = value;
|
||||||
|
RestrictionAuthorityChanged?.Invoke(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AP-129 (Campaign P Slice P4 review fix): retail <c>PublicWeenieDesc
|
/// AP-129 (Campaign P Slice P4 review fix): retail <c>PublicWeenieDesc
|
||||||
/// ._db</c> (<c>RestrictionDB*</c>) — the house's own guest/ban list.
|
/// ._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
|
/// object — acdream cannot distinguish the two, and both resolve to
|
||||||
/// the same retail-faithful "allow" default.
|
/// the same retail-faithful "allow" default.
|
||||||
/// </summary>
|
/// </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();
|
public PropertyBundle Properties { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -116,11 +116,71 @@ public sealed class ClientObjectTable
|
||||||
private readonly ConcurrentDictionary<uint, Container> _containers = new();
|
private readonly ConcurrentDictionary<uint, Container> _containers = new();
|
||||||
private readonly Dictionary<uint, List<uint>> _containerIndex = new();
|
private readonly Dictionary<uint, List<uint>> _containerIndex = new();
|
||||||
private readonly Dictionary<uint, List<uint>> _equipmentIndex = 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
|
// B-Drag: pre-move snapshots for optimistic inventory moves. itemId → (container, slot, equip) BEFORE
|
||||||
// the optimistic MoveItem; restored by RollbackMove on InventoryServerSaveFailed (0x00A0),
|
// the optimistic MoveItem; restored by RollbackMove on InventoryServerSaveFailed (0x00A0),
|
||||||
// cleared by ConfirmMove on the InventoryPutObjInContainer (0x0022) echo.
|
// cleared by ConfirmMove on the InventoryPutObjInContainer (0x0022) echo.
|
||||||
private readonly Dictionary<uint, (ClientObjectPlacement placement, int outstanding)> _pendingMoves = new();
|
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>
|
/// <summary>Fires when an object is first added to the session.</summary>
|
||||||
public event Action<ClientObject>? ObjectAdded;
|
public event Action<ClientObject>? ObjectAdded;
|
||||||
|
|
@ -270,7 +330,7 @@ public sealed class ClientObjectTable
|
||||||
ClientObjectPlacement previous = prior is null
|
ClientObjectPlacement previous = prior is null
|
||||||
? default
|
? default
|
||||||
: ClientObjectPlacement.From(prior);
|
: ClientObjectPlacement.From(prior);
|
||||||
_objects[item.ObjectId] = item;
|
RetainObject(item);
|
||||||
UpdateEquipmentIndex(item.ObjectId, previous, ClientObjectPlacement.From(item));
|
UpdateEquipmentIndex(item.ObjectId, previous, ClientObjectPlacement.From(item));
|
||||||
if (!existed) ObjectAdded?.Invoke(item);
|
if (!existed) ObjectAdded?.Invoke(item);
|
||||||
else ObjectUpdated?.Invoke(item);
|
else ObjectUpdated?.Invoke(item);
|
||||||
|
|
@ -615,6 +675,7 @@ public sealed class ClientObjectTable
|
||||||
bool notifyObjectRemoved)
|
bool notifyObjectRemoved)
|
||||||
{
|
{
|
||||||
if (!_objects.TryRemove(itemId, out var item)) return false;
|
if (!_objects.TryRemove(itemId, out var item)) return false;
|
||||||
|
UnbindRestrictionAuthority(item);
|
||||||
List<uint>? changedContainers = RemoveFromOtherContainerIndexes(
|
List<uint>? changedContainers = RemoveFromOtherContainerIndexes(
|
||||||
itemId,
|
itemId,
|
||||||
exceptContainerId: 0u);
|
exceptContainerId: 0u);
|
||||||
|
|
@ -690,7 +751,7 @@ public sealed class ClientObjectTable
|
||||||
if (!existed || item is null)
|
if (!existed || item is null)
|
||||||
{
|
{
|
||||||
item = new ClientObject { ObjectId = guid };
|
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.Ints) item.Properties.Ints[kv.Key] = kv.Value;
|
||||||
foreach (var kv in incoming.Int64s) item.Properties.Int64s[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
|
if (!existed || obj is null) // keep: satisfies nullable flow analysis
|
||||||
{
|
{
|
||||||
obj = new ClientObject { ObjectId = d.Guid };
|
obj = new ClientObject { ObjectId = d.Guid };
|
||||||
_objects[d.Guid] = obj;
|
RetainObject(obj);
|
||||||
}
|
}
|
||||||
uint oldContainer = obj.ContainerId;
|
uint oldContainer = obj.ContainerId;
|
||||||
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
||||||
|
|
@ -876,7 +937,7 @@ public sealed class ClientObjectTable
|
||||||
if (!existed || obj is null) // keep: satisfies nullable flow analysis
|
if (!existed || obj is null) // keep: satisfies nullable flow analysis
|
||||||
{
|
{
|
||||||
obj = new ClientObject { ObjectId = guid };
|
obj = new ClientObject { ObjectId = guid };
|
||||||
_objects[guid] = obj;
|
RetainObject(obj);
|
||||||
}
|
}
|
||||||
uint oldContainer = obj.ContainerId;
|
uint oldContainer = obj.ContainerId;
|
||||||
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
||||||
|
|
@ -955,7 +1016,7 @@ public sealed class ClientObjectTable
|
||||||
if (!existed || obj is null)
|
if (!existed || obj is null)
|
||||||
{
|
{
|
||||||
obj = new ClientObject { ObjectId = entry.Guid };
|
obj = new ClientObject { ObjectId = entry.Guid };
|
||||||
_objects[entry.Guid] = obj;
|
RetainObject(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
||||||
|
|
@ -1249,7 +1310,7 @@ public sealed class ClientObjectTable
|
||||||
if (!existed || obj is null)
|
if (!existed || obj is null)
|
||||||
{
|
{
|
||||||
obj = new ClientObject { ObjectId = entry.Guid };
|
obj = new ClientObject { ObjectId = entry.Guid };
|
||||||
_objects[entry.Guid] = obj;
|
RetainObject(obj);
|
||||||
}
|
}
|
||||||
obj.ContainerTypeHint = entry.ContainerType;
|
obj.ContainerTypeHint = entry.ContainerType;
|
||||||
if (!existed) added.Add(obj);
|
if (!existed) added.Add(obj);
|
||||||
|
|
@ -1361,7 +1422,7 @@ public sealed class ClientObjectTable
|
||||||
if (!existed || obj is null)
|
if (!existed || obj is null)
|
||||||
{
|
{
|
||||||
obj = new ClientObject { ObjectId = entry.Guid };
|
obj = new ClientObject { ObjectId = entry.Guid };
|
||||||
_objects[entry.Guid] = obj;
|
RetainObject(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
ClientObjectPlacement previous = ClientObjectPlacement.From(obj);
|
||||||
|
|
@ -1426,6 +1487,8 @@ public sealed class ClientObjectTable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
|
foreach (ClientObject item in _restrictionObservedObjects.ToArray())
|
||||||
|
UnbindRestrictionAuthority(item);
|
||||||
_objects.Clear();
|
_objects.Clear();
|
||||||
_containers.Clear();
|
_containers.Clear();
|
||||||
_containerIndex.Clear();
|
_containerIndex.Clear();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace AcDream.Core.Items;
|
namespace AcDream.Core.Items;
|
||||||
|
|
@ -22,11 +23,29 @@ namespace AcDream.Core.Items;
|
||||||
/// (0 = dwelling access only, 1 = storage access also). Retail's
|
/// (0 = dwelling access only, 1 = storage access also). Retail's
|
||||||
/// <c>IsAllowedIn</c> only consults key membership for entry; the permission
|
/// <c>IsAllowedIn</c> only consults key membership for entry; the permission
|
||||||
/// value is preserved for wire fidelity but not consulted here.</param>
|
/// value is preserved for wire fidelity but not consulted here.</param>
|
||||||
public sealed record HouseRestrictionRecord(
|
public sealed record HouseRestrictionRecord
|
||||||
bool OpenToPublic,
|
|
||||||
uint AllegianceMonarchId,
|
|
||||||
IReadOnlyDictionary<uint, uint> Guests)
|
|
||||||
{
|
{
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Verbatim port of retail <c>RestrictionDB::IsAllowedIn</c>
|
/// Verbatim port of retail <c>RestrictionDB::IsAllowedIn</c>
|
||||||
/// (named-retail pc:444493-444516, 0x005ae8f0):
|
/// (named-retail pc:444493-444516, 0x005ae8f0):
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,14 @@ public sealed class CellArray : ICollection<uint>, IReadOnlyCollection<uint>
|
||||||
private readonly List<uint> _order = new();
|
private readonly List<uint> _order = new();
|
||||||
private readonly HashSet<uint> _seen = 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 int Count => _order.Count;
|
||||||
public bool IsReadOnly => false;
|
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>
|
/// <summary>Append <paramref name="id"/> iff not already present (retail add_cell dedup).</summary>
|
||||||
public void Add(uint id)
|
public void Add(uint id)
|
||||||
{
|
{
|
||||||
|
if (UnionTarget is { } target
|
||||||
|
&& !ReferenceEquals(target, this))
|
||||||
|
{
|
||||||
|
target.Add(id);
|
||||||
|
}
|
||||||
if (_seen.Add(id))
|
if (_seen.Add(id))
|
||||||
_order.Add(id);
|
_order.Add(id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ public static class CellTransit
|
||||||
// Retail CEnvCell::find_transit_cells first asks the loaded
|
// Retail CEnvCell::find_transit_cells first asks the loaded
|
||||||
// neighbour cell whether the sphere intersects its CellBSP.
|
// neighbour cell whether the sphere intersects its CellBSP.
|
||||||
// The portal-plane side test is only the unloaded-cell load hint.
|
// The portal-plane side test is only the unloaded-cell load hint.
|
||||||
|
RecordUnionOnlyProbe(candidates, otherId);
|
||||||
var otherCell = cache.GetCellStruct(otherId);
|
var otherCell = cache.GetCellStruct(otherId);
|
||||||
if (otherCell is not null &&
|
if (otherCell is not null &&
|
||||||
CollisionTraversal.HasCellContainment(cache, otherCell))
|
CollisionTraversal.HasCellContainment(cache, otherCell))
|
||||||
|
|
@ -446,6 +447,7 @@ public static class CellTransit
|
||||||
if (portal.OtherPortalId < 0)
|
if (portal.OtherPortalId < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
RecordUnionOnlyProbe(candidates, portal.OtherCellId);
|
||||||
var otherCell = cache.GetCellStruct(portal.OtherCellId);
|
var otherCell = cache.GetCellStruct(portal.OtherCellId);
|
||||||
if (otherCell is null ||
|
if (otherCell is null ||
|
||||||
!CollisionTraversal.HasCellContainment(cache, otherCell))
|
!CollisionTraversal.HasCellContainment(cache, otherCell))
|
||||||
|
|
@ -679,8 +681,13 @@ public static class CellTransit
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static uint FindVisibleChildCell(
|
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);
|
var start = cache.GetCellStruct(startCellId);
|
||||||
if (start is null) return 0u;
|
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)
|
// arg3 != 0 → iterate stab_list, GetVisible + point_in_cell (:311444-311465)
|
||||||
foreach (uint id in start.VisibleCellIds)
|
foreach (uint id in start.VisibleCellIds)
|
||||||
|
{
|
||||||
|
probedCells?.Add(id);
|
||||||
if (PointInCell(cache, cache.GetCellStruct(id), worldPoint)) return id;
|
if (PointInCell(cache, cache.GetCellStruct(id), worldPoint)) return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// arg3 == 0 → iterate direct portals, GetOtherCell + point_in_cell (:311411-311434)
|
// arg3 == 0 → iterate direct portals, GetOtherCell + point_in_cell (:311411-311434)
|
||||||
foreach (var portal in start.Portals)
|
foreach (var portal in start.Portals)
|
||||||
|
{
|
||||||
|
probedCells?.Add(portal.OtherCellId);
|
||||||
if (PointInCell(
|
if (PointInCell(
|
||||||
cache,
|
cache,
|
||||||
cache.GetCellStruct(portal.OtherCellId),
|
cache.GetCellStruct(portal.OtherCellId),
|
||||||
|
|
@ -704,6 +716,7 @@ public static class CellTransit
|
||||||
{
|
{
|
||||||
return portal.OtherCellId;
|
return portal.OtherCellId;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0u;
|
return 0u;
|
||||||
|
|
@ -1068,7 +1081,11 @@ public static class CellTransit
|
||||||
sphereRadius))
|
sphereRadius))
|
||||||
{
|
{
|
||||||
uint recovered = FindVisibleChildCell(
|
uint recovered = FindVisibleChildCell(
|
||||||
cache, currentCellId, worldSphereCenter, useStabList: true);
|
cache,
|
||||||
|
currentCellId,
|
||||||
|
worldSphereCenter,
|
||||||
|
useStabList: true,
|
||||||
|
(candidates as CellArray)?.UnionTarget);
|
||||||
if (recovered != 0u && recovered != currentCellId)
|
if (recovered != 0u && recovered != currentCellId)
|
||||||
return recovered;
|
return recovered;
|
||||||
}
|
}
|
||||||
|
|
@ -1078,6 +1095,14 @@ public static class CellTransit
|
||||||
return currentCellId;
|
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)
|
private static int EffectiveSphereCount(IReadOnlyList<Sphere> worldSpheres, int numSpheres)
|
||||||
{
|
{
|
||||||
if (numSpheres <= 0 || worldSpheres.Count == 0) return 0;
|
if (numSpheres <= 0 || worldSpheres.Count == 0) return 0;
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,22 @@ public sealed class PhysicsEngine
|
||||||
/// when the restriction weenie can't be resolved — so production MUST
|
/// when the restriction weenie can't be resolved — so production MUST
|
||||||
/// wire this to the live table for the fix to actually admit anyone.
|
/// wire this to the live table for the fix to actually admit anyone.
|
||||||
/// </summary>
|
/// </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(
|
internal sealed record LandblockPhysics(
|
||||||
TerrainSurface Terrain,
|
TerrainSurface Terrain,
|
||||||
|
|
@ -1553,8 +1568,10 @@ public sealed class PhysicsEngine
|
||||||
private AdjustedSetPosition AdjustSetPosition(
|
private AdjustedSetPosition AdjustSetPosition(
|
||||||
uint seedCellId,
|
uint seedCellId,
|
||||||
Vector3 cellLocalPosition,
|
Vector3 cellLocalPosition,
|
||||||
Vector3 firstWorldSphereCenter)
|
Vector3 firstWorldSphereCenter,
|
||||||
|
CellArray queryFootprint)
|
||||||
{
|
{
|
||||||
|
queryFootprint.Add(seedCellId);
|
||||||
uint low = seedCellId & 0xFFFFu;
|
uint low = seedCellId & 0xFFFFu;
|
||||||
bool lowInRange = low is (>= 1u and <= 0x40u)
|
bool lowInRange = low is (>= 1u and <= 0x40u)
|
||||||
or (>= 0x0100u and <= 0xFFFDu)
|
or (>= 0x0100u and <= 0xFFFDu)
|
||||||
|
|
@ -1582,7 +1599,8 @@ public sealed class PhysicsEngine
|
||||||
cache,
|
cache,
|
||||||
seedCellId,
|
seedCellId,
|
||||||
firstWorldSphereCenter,
|
firstWorldSphereCenter,
|
||||||
useStabList: true);
|
useStabList: true,
|
||||||
|
queryFootprint);
|
||||||
if (child != 0u)
|
if (child != 0u)
|
||||||
{
|
{
|
||||||
return new AdjustedSetPosition(
|
return new AdjustedSetPosition(
|
||||||
|
|
@ -1606,6 +1624,7 @@ public sealed class PhysicsEngine
|
||||||
bool adjusted = LandDefs.AdjustToOutside(
|
bool adjusted = LandDefs.AdjustToOutside(
|
||||||
ref adjustedCell,
|
ref adjustedCell,
|
||||||
ref adjustedLocal);
|
ref adjustedLocal);
|
||||||
|
queryFootprint.Add(adjustedCell);
|
||||||
bool resident = adjusted
|
bool resident = adjusted
|
||||||
&& IsLandblockTerrainResident(adjustedCell);
|
&& IsLandblockTerrainResident(adjustedCell);
|
||||||
return new AdjustedSetPosition(
|
return new AdjustedSetPosition(
|
||||||
|
|
@ -1635,33 +1654,54 @@ public sealed class PhysicsEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
Transition transition = RentTransition();
|
Transition transition = RentTransition();
|
||||||
|
CellArray queryFootprint =
|
||||||
|
transition.SpherePath.SetPositionQueryFootprint;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
queryFootprint.Clear();
|
||||||
|
transition.SpherePath.CellCandidates.UnionTarget = queryFootprint;
|
||||||
InitializeSetPositionTransition(transition, request);
|
InitializeSetPositionTransition(transition, request);
|
||||||
bool randomOnly = request.Flags.HasFlag(
|
bool randomOnly = request.Flags.HasFlag(
|
||||||
PhysicsSetPositionFlags.RandomScatter);
|
PhysicsSetPositionFlags.RandomScatter);
|
||||||
|
PhysicsSetPositionResult result;
|
||||||
if (randomOnly)
|
if (randomOnly)
|
||||||
{
|
{
|
||||||
return SetScatterPositionInternal(
|
result = SetScatterPositionInternal(
|
||||||
transition,
|
transition,
|
||||||
request,
|
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 =
|
// Scatter retains one append-only query union across every inner
|
||||||
SetPositionInternal(transition, request, handleCollisions);
|
// attempt. Materialize it exactly once at the public transaction
|
||||||
if (result.Error != PhysicsSetPositionError.Ok
|
// boundary; copying it per attempt is quadratic at retail's
|
||||||
&& request.Flags.HasFlag(PhysicsSetPositionFlags.Scatter))
|
// maximum retry count.
|
||||||
|
return result with
|
||||||
{
|
{
|
||||||
return SetScatterPositionInternal(
|
QueriedCellIds = queryFootprint.OrderedIds.ToImmutableArray(),
|
||||||
transition,
|
};
|
||||||
request,
|
|
||||||
handleCollisions);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
transition.SpherePath.CellCandidates.UnionTarget = null;
|
||||||
ReturnTransition(transition);
|
ReturnTransition(transition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1686,7 +1726,8 @@ public sealed class PhysicsEngine
|
||||||
private PhysicsSetPositionResult SetScatterPositionInternal(
|
private PhysicsSetPositionResult SetScatterPositionInternal(
|
||||||
Transition transition,
|
Transition transition,
|
||||||
in PhysicsSetPositionRequest request,
|
in PhysicsSetPositionRequest request,
|
||||||
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
|
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions,
|
||||||
|
CellArray queryFootprint)
|
||||||
{
|
{
|
||||||
PhysicsSetPositionResult result = ErrorResult(
|
PhysicsSetPositionResult result = ErrorResult(
|
||||||
request,
|
request,
|
||||||
|
|
@ -1706,7 +1747,8 @@ public sealed class PhysicsEngine
|
||||||
result = SetPositionInternal(
|
result = SetPositionInternal(
|
||||||
transition,
|
transition,
|
||||||
scattered,
|
scattered,
|
||||||
handleCollisions);
|
handleCollisions,
|
||||||
|
queryFootprint);
|
||||||
if (result.Error == PhysicsSetPositionError.Ok)
|
if (result.Error == PhysicsSetPositionError.Ok)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -1716,7 +1758,8 @@ public sealed class PhysicsEngine
|
||||||
private PhysicsSetPositionResult SetPositionInternal(
|
private PhysicsSetPositionResult SetPositionInternal(
|
||||||
Transition transition,
|
Transition transition,
|
||||||
in PhysicsSetPositionRequest request,
|
in PhysicsSetPositionRequest request,
|
||||||
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
|
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions,
|
||||||
|
CellArray queryFootprint)
|
||||||
{
|
{
|
||||||
transition.SpherePath.CellCandidates.Clear();
|
transition.SpherePath.CellCandidates.Clear();
|
||||||
transition.SpherePath.ClearWalkable();
|
transition.SpherePath.ClearWalkable();
|
||||||
|
|
@ -1731,7 +1774,8 @@ public sealed class PhysicsEngine
|
||||||
AdjustedSetPosition adjusted = AdjustSetPosition(
|
AdjustedSetPosition adjusted = AdjustSetPosition(
|
||||||
request.CellId,
|
request.CellId,
|
||||||
request.CellLocalPosition,
|
request.CellLocalPosition,
|
||||||
firstWorldCenter);
|
firstWorldCenter,
|
||||||
|
queryFootprint);
|
||||||
if (!adjusted.Resident)
|
if (!adjusted.Resident)
|
||||||
{
|
{
|
||||||
return new PhysicsSetPositionResult(
|
return new PhysicsSetPositionResult(
|
||||||
|
|
@ -1849,7 +1893,9 @@ public sealed class PhysicsEngine
|
||||||
}
|
}
|
||||||
if (spherePath.CurCellId == 0u)
|
if (spherePath.CurCellId == 0u)
|
||||||
{
|
{
|
||||||
return ErrorResult(request, PhysicsSetPositionError.NoCell);
|
return ErrorResult(
|
||||||
|
request,
|
||||||
|
PhysicsSetPositionError.NoCell);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool inContact = collision.ContactPlaneValid;
|
bool inContact = collision.ContactPlaneValid;
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,8 @@ internal readonly record struct PhysicsSetPositionResult(
|
||||||
bool CellChanged = false,
|
bool CellChanged = false,
|
||||||
PhysicsShadowCommitAction ShadowAction = PhysicsShadowCommitAction.None,
|
PhysicsShadowCommitAction ShadowAction = PhysicsShadowCommitAction.None,
|
||||||
ImmutableArray<uint> CrossCellIds = default,
|
ImmutableArray<uint> CrossCellIds = default,
|
||||||
ImmutableArray<uint> CollidedObjectIds = default)
|
ImmutableArray<uint> CollidedObjectIds = default,
|
||||||
|
ImmutableArray<uint> QueriedCellIds = default)
|
||||||
{
|
{
|
||||||
internal bool IsSuccessful => Error == PhysicsSetPositionError.Ok;
|
internal bool IsSuccessful => Error == PhysicsSetPositionError.Ok;
|
||||||
internal bool IsCommitted =>
|
internal bool IsCommitted =>
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ public sealed class ShadowObjectRegistry
|
||||||
_collisionWorld.Current.ShadowOwnerFreeSlots;
|
_collisionWorld.Current.ShadowOwnerFreeSlots;
|
||||||
private readonly HashSet<uint> _prefixScratch = new();
|
private readonly HashSet<uint> _prefixScratch = new();
|
||||||
private readonly List<uint> _removedPrefixScratch = new();
|
private readonly List<uint> _removedPrefixScratch = new();
|
||||||
|
private ulong _mutationRevision;
|
||||||
internal event Action<uint, ulong>? OwnerMutated;
|
internal event Action<uint, ulong>? OwnerMutated;
|
||||||
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
|
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
|
||||||
|
|
||||||
|
|
@ -101,6 +102,7 @@ public sealed class ShadowObjectRegistry
|
||||||
"A populated shadow registry cannot change collision roots.");
|
"A populated shadow registry cannot change collision roots.");
|
||||||
}
|
}
|
||||||
_collisionWorld = collisionWorld;
|
_collisionWorld = collisionWorld;
|
||||||
|
AdvanceMutationRevision();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record RegistrationRecord(
|
internal sealed record RegistrationRecord(
|
||||||
|
|
@ -123,8 +125,20 @@ public sealed class ShadowObjectRegistry
|
||||||
? version
|
? version
|
||||||
: 0UL;
|
: 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)
|
private void BumpOwnerVersion(uint entityId)
|
||||||
{
|
{
|
||||||
|
AdvanceMutationRevision();
|
||||||
ulong version = checked(GetOwnerVersion(entityId) + 1UL);
|
ulong version = checked(GetOwnerVersion(entityId) + 1UL);
|
||||||
_ownerVersions[entityId] = version;
|
_ownerVersions[entityId] = version;
|
||||||
RefreshOwnerPrefixIndex(entityId);
|
RefreshOwnerPrefixIndex(entityId);
|
||||||
|
|
@ -1308,6 +1322,7 @@ public sealed class ShadowObjectRegistry
|
||||||
DeregisterCore(entityId, publishMutation: false);
|
DeregisterCore(entityId, publishMutation: false);
|
||||||
RemoveOwnerPrefixMembership(entityId);
|
RemoveOwnerPrefixMembership(entityId);
|
||||||
_ownerVersions.Remove(entityId);
|
_ownerVersions.Remove(entityId);
|
||||||
|
AdvanceMutationRevision();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
||||||
|
|
@ -1877,6 +1892,13 @@ public sealed class ShadowObjectRegistry
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Clear()
|
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();
|
_cells.Clear();
|
||||||
_entityToCells.Clear();
|
_entityToCells.Clear();
|
||||||
_suspendedEntities.Clear();
|
_suspendedEntities.Clear();
|
||||||
|
|
|
||||||
|
|
@ -719,6 +719,7 @@ public sealed class SpherePath
|
||||||
private Vector3[]? _walkableVertexStorage;
|
private Vector3[]? _walkableVertexStorage;
|
||||||
private Vector3[]? _lastWalkableVertexStorage;
|
private Vector3[]? _lastWalkableVertexStorage;
|
||||||
internal readonly CellArray CellCandidates = new();
|
internal readonly CellArray CellCandidates = new();
|
||||||
|
internal readonly CellArray SetPositionQueryFootprint = new();
|
||||||
internal readonly CellOrderScratchArena OrderedCellScratch = new();
|
internal readonly CellOrderScratchArena OrderedCellScratch = new();
|
||||||
|
|
||||||
internal Vector3[]? RetainedWalkableVertexStorage => _walkableVertexStorage;
|
internal Vector3[]? RetainedWalkableVertexStorage => _walkableVertexStorage;
|
||||||
|
|
@ -938,6 +939,8 @@ public sealed class SpherePath
|
||||||
if (_lastWalkableVertexStorage is not null)
|
if (_lastWalkableVertexStorage is not null)
|
||||||
System.Array.Clear(_lastWalkableVertexStorage);
|
System.Array.Clear(_lastWalkableVertexStorage);
|
||||||
CellCandidates.Clear();
|
CellCandidates.Clear();
|
||||||
|
CellCandidates.UnionTarget = null;
|
||||||
|
SetPositionQueryFootprint.Clear();
|
||||||
OrderedCellScratch.ResetForReuse();
|
OrderedCellScratch.ResetForReuse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -704,6 +704,9 @@ public sealed class PlayerMovementController
|
||||||
internal bool IsSealedPublicationCandidate => _publicationLifecycle
|
internal bool IsSealedPublicationCandidate => _publicationLifecycle
|
||||||
is PlayerMovementControllerPublicationLifecycle.CandidateSealed;
|
is PlayerMovementControllerPublicationLifecycle.CandidateSealed;
|
||||||
|
|
||||||
|
internal bool IsRuntimeOwnedDormant => _publicationLifecycle
|
||||||
|
is PlayerMovementControllerPublicationLifecycle.RuntimeOwnedDormant;
|
||||||
|
|
||||||
internal void SealPublicationCandidate()
|
internal void SealPublicationCandidate()
|
||||||
{
|
{
|
||||||
if (_publicationLifecycle
|
if (_publicationLifecycle
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,15 @@ internal enum RuntimeLocalPlayerPhysicsPublicationStatus
|
||||||
Discarded,
|
Discarded,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum RuntimeLocalPlayerPhysicsActivationStatus
|
||||||
|
{
|
||||||
|
Evaluated,
|
||||||
|
DeferredCell,
|
||||||
|
RejectedPlacement,
|
||||||
|
RejectedAuthority,
|
||||||
|
RejectedToken,
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
|
internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
|
||||||
RuntimeEntityKey Entity,
|
RuntimeEntityKey Entity,
|
||||||
RuntimeEntityPlacementToken Placement,
|
RuntimeEntityPlacementToken Placement,
|
||||||
|
|
@ -31,6 +40,33 @@ internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
|
||||||
&& Entity == Placement.Entity;
|
&& 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(
|
internal readonly record struct RuntimeLocalPlayerPhysicsCandidateSnapshot(
|
||||||
Vector3 Position,
|
Vector3 Position,
|
||||||
Quaternion Orientation,
|
Quaternion Orientation,
|
||||||
|
|
@ -45,10 +81,13 @@ public readonly record struct
|
||||||
bool IsBound,
|
bool IsBound,
|
||||||
bool IsDisposed,
|
bool IsDisposed,
|
||||||
int CandidateCount,
|
int CandidateCount,
|
||||||
|
int PendingActivationCount,
|
||||||
ulong LastPublicationId)
|
ulong LastPublicationId)
|
||||||
{
|
{
|
||||||
internal bool IsConverged => !IsBound
|
internal bool IsConverged => !IsBound
|
||||||
|| (IsDisposed && CandidateCount == 0);
|
|| (IsDisposed
|
||||||
|
&& CandidateCount == 0
|
||||||
|
&& PendingActivationCount == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -74,12 +113,28 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
internal required PhysicsBody Body { get; init; }
|
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 RuntimeEntityDirectory _entities;
|
||||||
private readonly RuntimePhysicsState _physics;
|
private readonly RuntimePhysicsState _physics;
|
||||||
private readonly RuntimeLocalPlayerMovementState _movement;
|
private readonly RuntimeLocalPlayerMovementState _movement;
|
||||||
private readonly RuntimeLocalPlayerIdentityState _identity;
|
private readonly RuntimeLocalPlayerIdentityState _identity;
|
||||||
private Candidate? _candidate;
|
private Candidate? _candidate;
|
||||||
|
private Activation? _activation;
|
||||||
private ulong _nextPublicationId;
|
private ulong _nextPublicationId;
|
||||||
|
private ulong _nextActivationId;
|
||||||
|
private ulong _nextEvaluationId;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
internal RuntimeLocalPlayerPhysicsPublicationState(
|
internal RuntimeLocalPlayerPhysicsPublicationState(
|
||||||
|
|
@ -161,9 +216,15 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
internal RuntimeLocalPlayerPhysicsPublicationStatus Commit(
|
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);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
activationToken = default;
|
||||||
if (!token.IsValid
|
if (!token.IsValid
|
||||||
|| _candidate is not { } candidate
|
|| _candidate is not { } candidate
|
||||||
|| candidate.Token != token)
|
|| candidate.Token != token)
|
||||||
|
|
@ -184,10 +245,104 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
candidate.Record.ObjectClock);
|
candidate.Record.ObjectClock);
|
||||||
candidate.Record.SetPhysicsBody(candidate.Body);
|
candidate.Record.SetPhysicsBody(candidate.Body);
|
||||||
_movement.CommitRuntimeOwnedController(candidate.Controller);
|
_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;
|
_candidate = null;
|
||||||
return RuntimeLocalPlayerPhysicsPublicationStatus.Committed;
|
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(
|
internal RuntimeLocalPlayerPhysicsPublicationStatus Discard(
|
||||||
in RuntimeLocalPlayerPhysicsPublicationToken token)
|
in RuntimeLocalPlayerPhysicsPublicationToken token)
|
||||||
{
|
{
|
||||||
|
|
@ -231,12 +386,14 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
IsBound: true,
|
IsBound: true,
|
||||||
_disposed,
|
_disposed,
|
||||||
_candidate is null ? 0 : 1,
|
_candidate is null ? 0 : 1,
|
||||||
|
_activation is null ? 0 : 1,
|
||||||
_nextPublicationId);
|
_nextPublicationId);
|
||||||
|
|
||||||
internal void ResetSession()
|
internal void ResetSession()
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
DiscardCurrent();
|
DiscardCurrent();
|
||||||
|
DiscardActivation();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -244,6 +401,7 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
DiscardCurrent();
|
DiscardCurrent();
|
||||||
|
DiscardActivation();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,7 +409,8 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
in RuntimeEntityPlacementToken placement,
|
in RuntimeEntityPlacementToken placement,
|
||||||
in RuntimeSetPositionCommand command) =>
|
in RuntimeSetPositionCommand command) =>
|
||||||
record.Key is { } key
|
_activation is null
|
||||||
|
&& record.Key is { } key
|
||||||
&& key == placement.Entity
|
&& key == placement.Entity
|
||||||
&& _entities.IsCurrent(record)
|
&& _entities.IsCurrent(record)
|
||||||
&& !record.DeleteAcceptedForTeardown
|
&& !record.DeleteAcceptedForTeardown
|
||||||
|
|
@ -276,7 +435,8 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
command);
|
command);
|
||||||
|
|
||||||
private bool IsCurrent(Candidate candidate) =>
|
private bool IsCurrent(Candidate candidate) =>
|
||||||
candidate.Controller.IsSealedPublicationCandidate
|
_activation is null
|
||||||
|
&& candidate.Controller.IsSealedPublicationCandidate
|
||||||
&& candidate.Controller.OwnsPhysicsBody(candidate.Body)
|
&& candidate.Controller.OwnsPhysicsBody(candidate.Body)
|
||||||
&& _entities.SessionLifetimeVersion
|
&& _entities.SessionLifetimeVersion
|
||||||
== candidate.Token.SessionGenerationAuthority
|
== candidate.Token.SessionGenerationAuthority
|
||||||
|
|
@ -313,4 +473,55 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
_candidate = null;
|
_candidate = null;
|
||||||
candidate?.Controller.DiscardRuntimeCandidate();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
|
|
@ -1042,6 +1044,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
private bool _suppressCollisionOwnerJournal;
|
private bool _suppressCollisionOwnerJournal;
|
||||||
private long _nextCollisionPreparationSequence;
|
private long _nextCollisionPreparationSequence;
|
||||||
private long _latestCollisionPreparationStartSequence;
|
private long _latestCollisionPreparationStartSequence;
|
||||||
|
private ulong _collisionWorldAuthority = 1UL;
|
||||||
private readonly List<Action<RuntimeCollisionGenerationCommitted>>
|
private readonly List<Action<RuntimeCollisionGenerationCommitted>>
|
||||||
_collisionGenerationCommittedObservers = new();
|
_collisionGenerationCommittedObservers = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
@ -1898,6 +1901,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
EnsureCollisionMutationThread();
|
EnsureCollisionMutationThread();
|
||||||
uint canonical = CanonicalLandblock(landblockId);
|
uint canonical = CanonicalLandblock(landblockId);
|
||||||
|
AdvanceCollisionWorldAuthority();
|
||||||
if (_collisionAdmissions.Remove(
|
if (_collisionAdmissions.Remove(
|
||||||
canonical,
|
canonical,
|
||||||
out RuntimeCollisionAdmission? superseded))
|
out RuntimeCollisionAdmission? superseded))
|
||||||
|
|
@ -2013,6 +2017,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
admission.Generation);
|
admission.Generation);
|
||||||
_collisionGenerations[admission.LandblockId] = checked(
|
_collisionGenerations[admission.LandblockId] = checked(
|
||||||
admission.Generation + 1UL);
|
admission.Generation + 1UL);
|
||||||
|
AdvanceCollisionWorldAuthority();
|
||||||
}
|
}
|
||||||
TrimCollisionOwnerJournal();
|
TrimCollisionOwnerJournal();
|
||||||
}
|
}
|
||||||
|
|
@ -2226,6 +2231,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Engine.CommitLandblockReplacement(replacement);
|
Engine.CommitLandblockReplacement(replacement);
|
||||||
|
AdvanceCollisionWorldAuthority();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -2496,6 +2502,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
|
|
||||||
private void InvalidateCollisionAdmission(uint landblockId)
|
private void InvalidateCollisionAdmission(uint landblockId)
|
||||||
{
|
{
|
||||||
|
AdvanceCollisionWorldAuthority();
|
||||||
ulong currentGeneration = _collisionGenerations.TryGetValue(
|
ulong currentGeneration = _collisionGenerations.TryGetValue(
|
||||||
landblockId,
|
landblockId,
|
||||||
out ulong current)
|
out ulong current)
|
||||||
|
|
@ -2540,6 +2547,130 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
: 1UL;
|
: 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(
|
internal bool HandleSetPositionCollisions(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
ulong positionAuthorityVersion,
|
ulong positionAuthorityVersion,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
@ -82,6 +83,32 @@ internal readonly record struct RuntimeSetPositionCommand(
|
||||||
float ShadowWorldOffsetY = 0f,
|
float ShadowWorldOffsetY = 0f,
|
||||||
RuntimePortalPlacementAuthority Portal = default);
|
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(
|
public readonly record struct RuntimePlacementProjectionToken(
|
||||||
ulong Sequence,
|
ulong Sequence,
|
||||||
ulong Revision,
|
ulong Revision,
|
||||||
|
|
@ -597,6 +624,98 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
&& IsPreparationAuthorityCurrent(operation, authority);
|
&& 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(
|
internal RuntimeSetPositionOutcome SubmitPreparedPlacement(
|
||||||
in RuntimeEntityPlacementToken token,
|
in RuntimeEntityPlacementToken token,
|
||||||
in RuntimeSetPositionCommand command) =>
|
in RuntimeSetPositionCommand command) =>
|
||||||
|
|
@ -1881,6 +2000,69 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
&& operation.Record.PlacementCommitVersion
|
&& operation.Record.PlacementCommitVersion
|
||||||
== operation.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) =>
|
private bool IsVelocityCurrent(Operation operation) =>
|
||||||
operation.SourceVelocityAuthorityVersion == 0UL
|
operation.SourceVelocityAuthorityVersion == 0UL
|
||||||
|| operation.Record.VelocityAuthorityVersion
|
|| operation.Record.VelocityAuthorityVersion
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
namespace AcDream.Core.Tests.Physics;
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
|
@ -32,7 +34,7 @@ public sealed class PhysicsSetPositionTests
|
||||||
var local = new Vector3(193f, 12f, 7f);
|
var local = new Vector3(193f, 12f, 7f);
|
||||||
uint expectedCell = Cell;
|
uint expectedCell = Cell;
|
||||||
Vector3 expectedLocal = local;
|
Vector3 expectedLocal = local;
|
||||||
Assert.True(LandDefs.AdjustToOutside(
|
Assert.True(AcDream.Core.Physics.LandDefs.AdjustToOutside(
|
||||||
ref expectedCell,
|
ref expectedCell,
|
||||||
ref expectedLocal));
|
ref expectedLocal));
|
||||||
|
|
||||||
|
|
@ -46,6 +48,9 @@ public sealed class PhysicsSetPositionTests
|
||||||
Assert.Equal(expectedCell, result.CellId);
|
Assert.Equal(expectedCell, result.CellId);
|
||||||
Assert.Equal(expectedLocal, result.CellLocalPosition);
|
Assert.Equal(expectedLocal, result.CellLocalPosition);
|
||||||
Assert.Equal(new Vector3(193f, 12f, 7f), result.Position);
|
Assert.Equal(new Vector3(193f, 12f, 7f), result.Position);
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { Cell, expectedCell }.Distinct(),
|
||||||
|
result.QueriedCellIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -82,6 +87,194 @@ public sealed class PhysicsSetPositionTests
|
||||||
Assert.Equal(0u, result.CellId);
|
Assert.Equal(0u, result.CellId);
|
||||||
Assert.Equal(local, result.CellLocalPosition);
|
Assert.Equal(local, result.CellLocalPosition);
|
||||||
Assert.Equal(local, result.Position);
|
Assert.Equal(local, result.Position);
|
||||||
|
Assert.Equal(new[] { southWestCell, 0u }, result.QueriedCellIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void QueryFootprintIncludesAdjustPositionVisibleChildProbes()
|
||||||
|
{
|
||||||
|
const uint start = Landblock | 0x0101u;
|
||||||
|
const uint sibling = Landblock | 0x0102u;
|
||||||
|
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||||
|
engine.DataCache.RegisterCellStructForTest(
|
||||||
|
start,
|
||||||
|
ContainmentCell(
|
||||||
|
new Plane(new Vector3(0f, -1f, 0f), 3f),
|
||||||
|
[sibling]));
|
||||||
|
engine.DataCache.RegisterCellStructForTest(
|
||||||
|
sibling,
|
||||||
|
ContainmentCell(
|
||||||
|
new Plane(new Vector3(0f, 1f, 0f), -7f),
|
||||||
|
[]));
|
||||||
|
|
||||||
|
PhysicsSetPositionResult result = engine.SetPosition(
|
||||||
|
Request(
|
||||||
|
start,
|
||||||
|
new Vector3(0f, 8f, 1f),
|
||||||
|
new Vector3(0f, 8f, 1f)) with
|
||||||
|
{
|
||||||
|
MoverPhysicsState = PhysicsStateFlags.Missile,
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Contains(start, result.QueriedCellIds);
|
||||||
|
Assert.Contains(sibling, result.QueriedCellIds);
|
||||||
|
Assert.True(
|
||||||
|
result.QueriedCellIds.IndexOf(start)
|
||||||
|
< result.QueriedCellIds.IndexOf(sibling));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LateralVisibleChildRecoveryRecordsRejectedAndWinningSiblingsUnionOnly()
|
||||||
|
{
|
||||||
|
const uint start = Landblock | 0x0101u;
|
||||||
|
const uint rejected = Landblock | 0x0102u;
|
||||||
|
const uint winner = Landblock | 0x0103u;
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
cache.RegisterCellStructForTest(
|
||||||
|
start,
|
||||||
|
ContainmentCell(
|
||||||
|
new Plane(new Vector3(0f, -1f, 0f), 3f),
|
||||||
|
[rejected, winner]));
|
||||||
|
cache.RegisterCellStructForTest(
|
||||||
|
rejected,
|
||||||
|
ContainmentCell(
|
||||||
|
new Plane(new Vector3(0f, 1f, 0f), -20f),
|
||||||
|
[]));
|
||||||
|
cache.RegisterCellStructForTest(
|
||||||
|
winner,
|
||||||
|
ContainmentCell(
|
||||||
|
new Plane(new Vector3(0f, 1f, 0f), -7f),
|
||||||
|
[]));
|
||||||
|
var candidates = new CellArray();
|
||||||
|
var queryFootprint = new CellArray();
|
||||||
|
candidates.UnionTarget = queryFootprint;
|
||||||
|
var spheres = new[]
|
||||||
|
{
|
||||||
|
new Sphere
|
||||||
|
{
|
||||||
|
Origin = new Vector3(0f, 8f, 1f),
|
||||||
|
Radius = 0.48f,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
uint containing = CellTransit.FindCellSet(
|
||||||
|
cache,
|
||||||
|
spheres,
|
||||||
|
spheres.Length,
|
||||||
|
start,
|
||||||
|
candidates);
|
||||||
|
|
||||||
|
Assert.Equal(winner, containing);
|
||||||
|
Assert.Equal(new[] { start }, candidates.OrderedIds);
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { start, rejected, winner },
|
||||||
|
queryFootprint.OrderedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectedPortalContainmentProbeIsRecordedUnionOnly()
|
||||||
|
{
|
||||||
|
const uint start = Landblock | 0x0101u;
|
||||||
|
const uint rejected = Landblock | 0x0102u;
|
||||||
|
const ushort portalPolygonId = 10;
|
||||||
|
var portalPolygon = new ResolvedPolygon
|
||||||
|
{
|
||||||
|
Id = portalPolygonId,
|
||||||
|
Vertices =
|
||||||
|
[
|
||||||
|
new Vector3(10f, -1f, 0f),
|
||||||
|
new Vector3(10f, 1f, 0f),
|
||||||
|
new Vector3(10f, 1f, 2f),
|
||||||
|
],
|
||||||
|
Plane = new Plane(Vector3.UnitX, -10f),
|
||||||
|
NumPoints = 3,
|
||||||
|
SidesType = CullMode.None,
|
||||||
|
};
|
||||||
|
var startCell = new CellPhysics
|
||||||
|
{
|
||||||
|
BSP = new PhysicsBSPTree
|
||||||
|
{
|
||||||
|
Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf },
|
||||||
|
},
|
||||||
|
WorldTransform = Matrix4x4.Identity,
|
||||||
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
|
CellBSP = new CellBSPTree
|
||||||
|
{
|
||||||
|
Root = new CellBSPNode { Type = BSPNodeType.Leaf },
|
||||||
|
},
|
||||||
|
Portals =
|
||||||
|
[
|
||||||
|
new PortalInfo(
|
||||||
|
(ushort)(rejected & 0xFFFFu),
|
||||||
|
portalPolygonId,
|
||||||
|
0),
|
||||||
|
],
|
||||||
|
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>
|
||||||
|
{
|
||||||
|
[portalPolygonId] = portalPolygon,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
cache.RegisterCellStructForTest(start, startCell);
|
||||||
|
cache.RegisterCellStructForTest(
|
||||||
|
rejected,
|
||||||
|
ContainmentCell(
|
||||||
|
new Plane(Vector3.UnitX, -100f),
|
||||||
|
[]));
|
||||||
|
var candidates = new CellArray();
|
||||||
|
var queryFootprint = new CellArray();
|
||||||
|
candidates.UnionTarget = queryFootprint;
|
||||||
|
var spheres = new[]
|
||||||
|
{
|
||||||
|
new Sphere
|
||||||
|
{
|
||||||
|
Origin = Vector3.Zero,
|
||||||
|
Radius = 0.48f,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
uint containing = CellTransit.FindCellSet(
|
||||||
|
cache,
|
||||||
|
spheres,
|
||||||
|
spheres.Length,
|
||||||
|
start,
|
||||||
|
candidates);
|
||||||
|
|
||||||
|
Assert.Equal(start, containing);
|
||||||
|
Assert.Equal(new[] { start }, candidates.OrderedIds);
|
||||||
|
Assert.Equal(new[] { start, rejected }, queryFootprint.OrderedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectedBuildingContainmentProbeIsRecordedUnionOnly()
|
||||||
|
{
|
||||||
|
const uint rejected = Landblock | 0x0102u;
|
||||||
|
var building = new BuildingPhysics
|
||||||
|
{
|
||||||
|
WorldTransform = Matrix4x4.Identity,
|
||||||
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
|
Portals =
|
||||||
|
[
|
||||||
|
new BldPortalInfo(
|
||||||
|
rejected,
|
||||||
|
otherPortalId: 0,
|
||||||
|
flags: 0),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
var candidates = new CellArray();
|
||||||
|
var queryFootprint = new CellArray();
|
||||||
|
candidates.UnionTarget = queryFootprint;
|
||||||
|
|
||||||
|
CellTransit.CheckBuildingTransit(
|
||||||
|
new PhysicsDataCache(),
|
||||||
|
building,
|
||||||
|
Vector3.Zero,
|
||||||
|
sphereRadius: 0.48f,
|
||||||
|
candidates);
|
||||||
|
|
||||||
|
Assert.Empty(candidates);
|
||||||
|
Assert.Equal(new[] { rejected }, queryFootprint.OrderedIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -867,6 +1060,91 @@ public sealed class PhysicsSetPositionTests
|
||||||
Assert.Equal(new Vector3(11f, 11f, 10f), result.Position);
|
Assert.Equal(new Vector3(11f, 11f, 10f), result.Position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalThenScatterUnionsEveryLandblockProbeInStableOrder()
|
||||||
|
{
|
||||||
|
PhysicsEngine engine = FlatEngine();
|
||||||
|
const uint eastLandblock = 0xAAB40000u;
|
||||||
|
AddFlatLandblock(engine, eastLandblock, worldOffsetX: 192f);
|
||||||
|
int pass = 0;
|
||||||
|
engine.TransitionCellCollisionTestHook =
|
||||||
|
(_, phase, _, observed) => phase
|
||||||
|
is TransitionCellCollisionPhase.Environment
|
||||||
|
&& pass++ == 0
|
||||||
|
? TransitionState.Collided
|
||||||
|
: observed;
|
||||||
|
var random = new Queue<double>([1d, 0.5d]);
|
||||||
|
engine.SetPositionRandomUnit = random.Dequeue;
|
||||||
|
|
||||||
|
PhysicsSetPositionResult result = engine.SetPosition(
|
||||||
|
Request(
|
||||||
|
Cell,
|
||||||
|
new Vector3(180f, 12f, 7f),
|
||||||
|
new Vector3(180f, 12f, 7f)) with
|
||||||
|
{
|
||||||
|
MoverPhysicsState = PhysicsStateFlags.Missile,
|
||||||
|
Flags = PhysicsSetPositionFlags.Placement
|
||||||
|
| PhysicsSetPositionFlags.Scatter,
|
||||||
|
ScatterRadiusX = 20f,
|
||||||
|
ScatterAttempts = 1u,
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(result.IsCommitted);
|
||||||
|
int source = IndexOfLandblock(result.QueriedCellIds, Landblock);
|
||||||
|
int east = IndexOfLandblock(
|
||||||
|
result.QueriedCellIds,
|
||||||
|
eastLandblock);
|
||||||
|
Assert.True(source >= 0);
|
||||||
|
Assert.True(east > source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiScatterUnionsFailedAndSuccessfulAttemptLandblocks()
|
||||||
|
{
|
||||||
|
PhysicsEngine engine = FlatEngine();
|
||||||
|
const uint westLandblock = 0xA8B40000u;
|
||||||
|
const uint eastLandblock = 0xAAB40000u;
|
||||||
|
AddFlatLandblock(engine, westLandblock, worldOffsetX: -192f);
|
||||||
|
AddFlatLandblock(engine, eastLandblock, worldOffsetX: 192f);
|
||||||
|
int pass = 0;
|
||||||
|
engine.TransitionCellCollisionTestHook =
|
||||||
|
(_, phase, _, observed) => phase
|
||||||
|
is TransitionCellCollisionPhase.Environment
|
||||||
|
&& pass++ == 0
|
||||||
|
? TransitionState.Collided
|
||||||
|
: observed;
|
||||||
|
var random = new Queue<double>([0d, 0.5d, 1d, 0.5d]);
|
||||||
|
engine.SetPositionRandomUnit = random.Dequeue;
|
||||||
|
|
||||||
|
PhysicsSetPositionResult result = engine.SetPosition(
|
||||||
|
Request(
|
||||||
|
Cell,
|
||||||
|
new Vector3(10f, 12f, 7f),
|
||||||
|
new Vector3(10f, 12f, 7f)) with
|
||||||
|
{
|
||||||
|
MoverPhysicsState = PhysicsStateFlags.Missile,
|
||||||
|
Flags = PhysicsSetPositionFlags.RandomScatter,
|
||||||
|
ScatterRadiusX = 200f,
|
||||||
|
ScatterAttempts = 2u,
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(result.IsCommitted);
|
||||||
|
int west = IndexOfLandblock(
|
||||||
|
result.QueriedCellIds,
|
||||||
|
westLandblock);
|
||||||
|
int east = IndexOfLandblock(
|
||||||
|
result.QueriedCellIds,
|
||||||
|
eastLandblock);
|
||||||
|
Assert.True(west >= 0);
|
||||||
|
Assert.True(east > west);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
result.CrossCellIds,
|
||||||
|
cell => (cell & 0xFFFF0000u) == westLandblock);
|
||||||
|
Assert.Contains(
|
||||||
|
result.CrossCellIds,
|
||||||
|
cell => (cell & 0xFFFF0000u) == eastLandblock);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Scatter_ReusesOneTransitionAndFailedInnerProbeCannotLeakIntoSuccess()
|
public void Scatter_ReusesOneTransitionAndFailedInnerProbeCannotLeakIntoSuccess()
|
||||||
{
|
{
|
||||||
|
|
@ -1038,4 +1316,43 @@ public sealed class PhysicsSetPositionTests
|
||||||
worldOffsetX,
|
worldOffsetX,
|
||||||
worldOffsetY);
|
worldOffsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CellPhysics ContainmentCell(
|
||||||
|
Plane plane,
|
||||||
|
uint[] visibleCells) => new()
|
||||||
|
{
|
||||||
|
BSP = new PhysicsBSPTree
|
||||||
|
{
|
||||||
|
Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf },
|
||||||
|
},
|
||||||
|
WorldTransform = Matrix4x4.Identity,
|
||||||
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
|
CellBSP = new CellBSPTree
|
||||||
|
{
|
||||||
|
Root = new CellBSPNode
|
||||||
|
{
|
||||||
|
SplittingPlane = plane,
|
||||||
|
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Portals = [new PortalInfo(0xFFFF, 0, 0)],
|
||||||
|
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
|
VisibleCellIds = new HashSet<uint>(visibleCells),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static int IndexOfLandblock(
|
||||||
|
ImmutableArray<uint> cells,
|
||||||
|
uint landblock)
|
||||||
|
{
|
||||||
|
for (int index = 0; index < cells.Length; index++)
|
||||||
|
{
|
||||||
|
if ((cells[index] & 0xFFFF0000u)
|
||||||
|
== (landblock & 0xFFFF0000u))
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue