fix(physics): seal collision generations before activation
This commit is contained in:
parent
be94bc9b06
commit
d94145e6b8
15 changed files with 1556 additions and 410 deletions
|
|
@ -488,14 +488,19 @@ What exists and is active:
|
|||
- Landblock collision activation is generation-owned by
|
||||
`RuntimePhysicsState`. Graphical and no-window hosts populate a private
|
||||
`PreparedLandblockCollisionGeneration` over bounded cursors; its cache,
|
||||
`CellGraph`, engine landblock, buildings, static shadows, and dynamic-owner
|
||||
refloods are never visible through the borrowed live engine. Runtime validates
|
||||
the exact admission plus every affected dynamic shadow-owner version, seals
|
||||
the complete replacement, and commits it synchronously on the one update
|
||||
thread before emitting `CollisionGenerationCommitted`. A moving, spawned, or
|
||||
deleted owner makes the generation not-ready and returns only that dirty owner
|
||||
to the cursor-budgeted refresh phase. Thus readers see the complete old
|
||||
generation or complete new generation, never a mixed cell/cache/shadow world.
|
||||
`CellGraph`, engine landblock, buildings, static shadows, and retained-owner
|
||||
refloods are never visible through the borrowed live engine. Retained owners
|
||||
comprise every non-suspended dynamic or adjacent-root static touching the
|
||||
target prefix (including a withdrawn repair marker); target-root statics come
|
||||
from the authored replacement. Runtime mutation-gates their exact capture,
|
||||
refreshes each through the host work meter, and builds every cache/graph/
|
||||
shadow replacement list through one-work-unit seal cursors. The final update-
|
||||
thread activation performs one mutation-version check and installs the sealed
|
||||
records without heap allocation before emitting
|
||||
`CollisionGenerationCommitted`. Cancellation disposes only the named staging
|
||||
generation and never withdraws the previous active world. Thus readers see
|
||||
the complete old generation or complete new generation, never a mixed
|
||||
cell/cache/shadow world.
|
||||
- `ShadowObjectRegistry` gives movement a per-cell broadphase over nearby
|
||||
objects and buildings. Streaming reflood is structurally part of the Runtime
|
||||
collision-generation commit; there is no independent post-publication
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ AD-53..AD-55 (Campaign P response-layer findings).
|
|||
| AD-1 | Lost-cell machinery replaced by recoverable outdoor demote (**#107** safety net) + outdoor-restore `max(terrainZ, z)` under-terrain lift; retail goes `GotoLostCell` | `src/AcDream.Core/Physics/PhysicsEngine.cs:553` (+ :808) | acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable | Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server | `GotoLostCell` pc:283418; `SetPositionInternal` 0x00515bd0, pc:283892-283945 |
|
||||
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the required Near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud unhydratable-placement path. `RuntimeWorldTransitState` owns the shared reveal generation, accepted readiness, transit correlation, and exact generation/cell-scoped host-acknowledgement suffix. `WorldRevealCoordinator` is a graphical adapter holding only App resource receipts; normalized Runtime checkpoints observe ownership without defining another readiness path. **Slice E3 refinement (2026-07-24):** the same generation now publishes an immediate `WorldGenerationQuiescence` edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. **Slice E4 refinement (2026-07-24):** accepted render/physics/static publication may span update frames through retained exact cursors, but reveal still consumes only the completed spatial/render-ready generation; building and EnvCell snapshots remain invisible until complete and the final spatial identity swap stays observer-atomic. **Slice E5 refinement (2026-07-24):** the reveal generation owns one exact destination reservation across every typed budget dimension. Stale completion cannot consume or clear its replacement, and hydratable incomplete content is never force-revealed; portal transit retains the DAT tunnel and centered retail wait cue until readiness converges. The hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.Runtime/World/RuntimeWorldTransitState.cs`; `src/AcDream.App/Streaming/WorldRevealCoordinator.cs`; `src/AcDream.App/Streaming/WorldGenerationQuiescence.cs`; `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs`; `src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs`; `src/AcDream.App/Streaming/LandblockPresentationPipeline.cs`; `src/AcDream.App/Streaming/StreamingController.cs`; `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs`; `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail leaving `SmartBox::position_update_complete` false while `CellManager::blocking_for_cells` is set: neither initial login nor portal arrival may reveal or continue simulating an old/partial collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. | Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, missing scenery, or a still-active old generation; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit remains in the authored tunnel and presents the centered wait cue after five seconds. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::UseTime` 0x004D6E30; `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 |
|
||||
| AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) |
|
||||
| ~~AD-6~~ | **RETIRED 2026-07-31 (placement/streaming Slice 3B).** Cell/cache/topology/building/static-shadow publication plus retained dynamic-owner `recalc_cross_cells` is one Runtime-owned collision generation. App and Headless build only an off-side `PreparedLandblockCollisionGeneration`; the complete previous generation remains queryable until the one update-thread commit. Every affected dynamic owner carries an exact mutation version. Movement, spawn, or deletion during staging rejects activation and returns only dirty owners to a bounded refresh cursor; a stale admission/recenter/cancellation can dispose only its private generation. The commit installs the precomputed cell rows before its single notification/readiness acknowledgement, so no optional hydration callback can omit reflood and no observer sees mixed old/new cells. | `src/AcDream.Runtime/Physics/RuntimePhysicsState.cs` (`PrepareCollisionGeneration`, `CommitCollisionGeneration`); `src/AcDream.Core/Physics/PhysicsDataCache.cs`; `PhysicsEngine.cs`; `ShadowObjectRegistry.cs`; `src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs`; `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs`; `tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs`; `tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs` | — | — | `CObjCell::init_objects` → `CPhysicsObj::recalc_cross_cells`, 0x0052b420 / 0x00515a30; `CPhysicsObj::SetPositionInternal` shadow replacement tail 0x00515330 |
|
||||
| ~~AD-6~~ | **RETIRED 2026-07-31 (placement/streaming Slice 3B).** Cell/cache/topology/building/static-shadow publication plus every retained non-suspended owner touching or withdrawn from the prefix is one Runtime-owned collision generation. Retained includes dynamics and adjacent-root statics; only target-root statics are superseded by the authored replacement. App and Headless build off-side through mutation-stable, one-work-unit capture/seal cursors; rowless owner state/payload updates, movement, spawn, deletion, and seam-static changes reject stale sealing. The complete previous generation remains queryable until one allocation-free update-thread activation. A stale admission or staging failure disposes only that private generation and cannot withdraw the active world or invalidate a newer admission. The commit installs precomputed cell rows and clears repaired withdrawal markers before its single notification/readiness acknowledgement, so no optional hydration callback can omit reflood and no observer sees mixed old/new cells. | `src/AcDream.Runtime/Physics/RuntimePhysicsState.cs` (`PrepareCollisionGeneration`, `AdvanceCollisionGenerationSeal`, `CommitCollisionGeneration`); `src/AcDream.Core/Physics/PhysicsDataCache.cs`; `PhysicsEngine.cs`; `ShadowObjectRegistry.cs`; `src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs`; `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs`; `tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs`; `tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs`; `tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs` | — | — | `CObjCell::init_objects` → `CPhysicsObj::recalc_cross_cells`, 0x0052b420 / 0x00515a30; `CPhysicsObj::SetPositionInternal` shadow replacement tail 0x00515330 |
|
||||
| AD-10 | Remote slope projection relocated to the queue-empty/head-reached combiner boundary; retail projects inside `CTransition::adjust_offset` during the sweep | `src/AcDream.Core/Physics/PositionManager.cs:47` | Remote bodies don't run a full local transition sweep; boundary projection removes the ~5 Hz Z staircase on slopes, no-op on flat ground | The single-point terrain-normal sample can differ from the sweep's contact plane (cell boundaries, props underfoot) — remote Z drift / stair-stepping | `CTransition::adjust_offset` pc:272296-272346 |
|
||||
| ~~AD-11~~ | **RETIRED 2026-07-23** — the matching binary disproved the old nonzero interpretation: `ItemUses::IsUseable` executes `not bitfield; and eax,1`, so absent/reset zero is usable and only `USEABLE_NO` disables use. Toolbar, item policy, and world interaction now share that exact Core predicate. | `src/AcDream.Core/Items/ClientObject.cs` (`ItemUseability.IsUseable`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs`; `src/AcDream.App/Interaction/WorldSelectionQuery.cs` | — | — | `ItemUses::IsUseable @ 0x004FCCC0`; matching v11.4186 instructions recorded in `docs/research/2026-07-23-retail-item-use-and-autowear-pseudocode.md` |
|
||||
| AD-12 | SecondaryAttributeTable coefficients hardcoded (Health=End×0.5, Stam=End×1.0, Mana=Self×1.0) instead of dat-read; unknown attributes contribute 0 | `src/AcDream.Core/Player/LocalPlayerState.cs:279` | Coefficients never vary across retail dat versions; re-confirmed by ACE AttributeFormula.cs + holtburger; dat port can replace later | A customized portal.dat with modified vital formulas silently yields wrong max-vitals; a missing attribute snapshot underestimates max | SecondaryAttributeTable portal.dat 0x0E0..0x0E2; `CreatureVital::GetMaxValue` 0x0058F2DD |
|
||||
|
|
|
|||
|
|
@ -25,19 +25,25 @@ The asynchronous unit is now one Runtime-owned collision generation:
|
|||
GfxObj/Setup catalogs are not copied; the accepted build's exact closure is
|
||||
populated by the existing cursors.
|
||||
3. App and Headless publish terrain, EnvCells, topology, buildings, prepared
|
||||
collision assets, static owners, and retained dynamic-owner cell sets only
|
||||
into that private generation.
|
||||
4. Each dynamic owner refresh captures its exact `ShadowObjectRegistry`
|
||||
mutation version. Movement, state/payload change, spawn, suspension, or
|
||||
deletion changes that version.
|
||||
5. `CommitCollisionGeneration` validates the admission, exact affected-owner
|
||||
set, and all captured versions. If any owner is dirty, it returns the sorted
|
||||
dirty IDs without touching the active world; App advances those IDs through
|
||||
its existing work meter and retries.
|
||||
6. Once fresh, Runtime precomputes the replacement arrays and synchronously
|
||||
replaces the landblock's cache/graph/engine/building/static and dynamic
|
||||
shadow state on the same update thread. Only after the complete replacement
|
||||
does it emit `CollisionGenerationCommitted` and a ready acknowledgement.
|
||||
collision assets, and target-root static owners only into that private
|
||||
generation.
|
||||
4. A mutation-stable cursor captures every non-suspended owner that touches —
|
||||
or has a withdrawn repair marker for — the target prefix. That includes
|
||||
live dynamic owners and statics rooted in an adjacent landblock. Only a
|
||||
target-root static is omitted, because the authored replacement supersedes
|
||||
it. Each retained owner refresh captures its exact
|
||||
`ShadowObjectRegistry` mutation version; a rowless withdrawn owner therefore
|
||||
remains freshness-gated when state or payload changes.
|
||||
5. Explicit one-work-unit cursors build the complete replacement before the
|
||||
activation frame: requested global collision records, cells/topology,
|
||||
buildings, cell graph removals, affected static owners, retained-owner
|
||||
states, and removal lists. An active-owner mutation restarts capture and
|
||||
sealing without touching the active world.
|
||||
6. `CommitCollisionGeneration` performs only the final mutation-version check
|
||||
and installs the already sealed replacement synchronously on the update
|
||||
thread. The dense 256-owner gate measures zero managed bytes in this final
|
||||
activation. Only after the complete replacement does Runtime emit
|
||||
`CollisionGenerationCommitted` and a ready acknowledgement.
|
||||
|
||||
The stable borrowed `PhysicsEngine` and `PhysicsDataCache` object identities do
|
||||
not change. Presentation and no-window hosts use the same Runtime transaction.
|
||||
|
|
@ -47,6 +53,9 @@ shadow state.
|
|||
## Failure and lifetime rules
|
||||
|
||||
- A newer admission invalidates an older prepared generation.
|
||||
- Cancellation names one admission and its private staging generation. It can
|
||||
never withdraw or demote the active landblock, and cancelling a stale receipt
|
||||
cannot invalidate a newer admission.
|
||||
- Demotion, withdrawal, reset, and disposal invalidate the admission before
|
||||
changing the active generation.
|
||||
- Disposing a stale/cancelled prepared generation clears only its private
|
||||
|
|
@ -64,7 +73,15 @@ The focused Runtime/App tests pin:
|
|||
- stale admission replacement has no active-world side effect;
|
||||
- movement during staging rejects, refreshes only the dirty owner, and then
|
||||
installs its latest cell set;
|
||||
- an authoritative state change on a retained rowless owner rejects a stale
|
||||
seal and installs the refreshed state on retry;
|
||||
- a neighboring static whose shadow crossed the seam is restored atomically
|
||||
on reload and its withdrawn-prefix marker clears only at activation;
|
||||
- spawn and deletion during staging both reject stale activation;
|
||||
- Headless faults immediately after admission and after staging preserve the
|
||||
prior complete world and leave no collision admission behind;
|
||||
- dense sealing consumes at most one work unit per call, while its final
|
||||
256-owner activation allocates zero managed bytes;
|
||||
- graphical and no-window publishers use the same Runtime transaction;
|
||||
- removal and terminal teardown converge the active ownership ledger.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,15 @@ as "delete everything and start over." A partial retail transition port exists:
|
|||
- `PhysicsDataCache` loads GfxObj, Setup, and CellStruct physics data.
|
||||
- Landblock streaming never edits the live collision world incrementally.
|
||||
`PreparedLandblockCollisionGeneration` owns an off-side cache, CellGraph,
|
||||
engine snapshot, buildings, statics, and versioned dynamic-owner refloods;
|
||||
`RuntimePhysicsState.CommitCollisionGeneration` validates freshness and
|
||||
activates the complete generation on the single update thread. A dirty live
|
||||
owner returns to the App cursor for refresh. The old generation remains
|
||||
queryable until that commit. See
|
||||
engine snapshot, buildings, statics, and versioned retained-owner refloods.
|
||||
Retained means every non-suspended dynamic or adjacent-root static touching
|
||||
(or withdrawn from) the target prefix; only authored target-root statics are
|
||||
superseded. All scans and replacement construction run through bounded
|
||||
capture/seal cursors. `RuntimePhysicsState.CommitCollisionGeneration` does
|
||||
one final mutation-version check and activates the sealed generation without
|
||||
managed allocation on the single update thread. Cancellation tears down only
|
||||
the named staging generation. The old generation remains queryable until
|
||||
commit. See
|
||||
`docs/research/2026-07-31-atomic-collision-generation.md`.
|
||||
- `ShadowObjectRegistry` gives the resolver a broadphase over nearby objects.
|
||||
- `TerrainSurface` uses triangle-aware terrain contact.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public sealed class LandblockPhysicsPublication : IDisposable
|
|||
uint currentCellId,
|
||||
BuildingInfo[] buildings,
|
||||
uint[] priorStaticOwnerIds,
|
||||
RuntimePhysicsState physics,
|
||||
RuntimeCollisionAdmission collisionAdmission,
|
||||
PreparedLandblockCollisionGeneration preparedGeneration)
|
||||
{
|
||||
|
|
@ -30,6 +31,7 @@ public sealed class LandblockPhysicsPublication : IDisposable
|
|||
CurrentCellId = currentCellId;
|
||||
Buildings = buildings;
|
||||
PriorStaticOwnerIds = priorStaticOwnerIds;
|
||||
Physics = physics;
|
||||
CollisionAdmission = collisionAdmission;
|
||||
PreparedGeneration = preparedGeneration;
|
||||
}
|
||||
|
|
@ -39,6 +41,7 @@ public sealed class LandblockPhysicsPublication : IDisposable
|
|||
internal uint CurrentCellId { get; }
|
||||
internal BuildingInfo[] Buildings { get; }
|
||||
internal uint[] PriorStaticOwnerIds { get; }
|
||||
internal RuntimePhysicsState Physics { get; }
|
||||
internal RuntimeCollisionAdmission CollisionAdmission { get; }
|
||||
internal PreparedLandblockCollisionGeneration PreparedGeneration { get; }
|
||||
internal PhysicsDataCache StagingCache => PreparedGeneration.DataCache;
|
||||
|
|
@ -63,16 +66,19 @@ public sealed class LandblockPhysicsPublication : IDisposable
|
|||
internal int CylinderOwnerCount { get; set; }
|
||||
internal int NoCollisionCount { get; set; }
|
||||
internal int SceneryTried { get; set; }
|
||||
internal uint[]? RefloodOwnerIds { get; set; }
|
||||
internal IReadOnlyList<uint>? RefloodOwnerIds { get; set; }
|
||||
internal int RefloodCursor { get; set; }
|
||||
internal bool RefloodCommitted { get; set; }
|
||||
internal bool SealCommitted { get; set; }
|
||||
internal bool BeginCommitted { get; set; }
|
||||
internal bool CompletionCommitted { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!CompletionCommitted)
|
||||
PreparedGeneration.Dispose();
|
||||
Physics.CancelCollisionGeneration(
|
||||
CollisionAdmission,
|
||||
PreparedGeneration);
|
||||
}
|
||||
|
||||
public uint LandblockId => Build.Landblock.LandblockId;
|
||||
|
|
@ -217,23 +223,34 @@ public sealed class LandblockPhysicsPublisher
|
|||
?? Array.Empty<BuildingInfo>();
|
||||
RuntimeCollisionAdmission collisionAdmission =
|
||||
_physics.BeginCollisionAdmission(build.Landblock.LandblockId);
|
||||
var publication = new LandblockPhysicsPublication(
|
||||
_receiptOwner,
|
||||
build,
|
||||
origin,
|
||||
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
|
||||
buildings,
|
||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||
build.Landblock.LandblockId),
|
||||
collisionAdmission,
|
||||
_physics.PrepareCollisionGeneration(collisionAdmission));
|
||||
publication.SetupObjectIds = build.Collisions is { } collisions
|
||||
? [.. collisions.SetupIds]
|
||||
: datBundle.Setups.Keys.Order().ToArray();
|
||||
publication.PreparedGeneration.SetAssetClosure(
|
||||
publication.GfxObjectIds,
|
||||
publication.SetupObjectIds);
|
||||
return publication;
|
||||
PreparedLandblockCollisionGeneration? prepared = null;
|
||||
try
|
||||
{
|
||||
prepared = _physics.PrepareCollisionGeneration(collisionAdmission);
|
||||
var publication = new LandblockPhysicsPublication(
|
||||
_receiptOwner,
|
||||
build,
|
||||
origin,
|
||||
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
|
||||
buildings,
|
||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||
build.Landblock.LandblockId),
|
||||
_physics,
|
||||
collisionAdmission,
|
||||
prepared);
|
||||
publication.SetupObjectIds = build.Collisions is { } collisions
|
||||
? [.. collisions.SetupIds]
|
||||
: datBundle.Setups.Keys.Order().ToArray();
|
||||
publication.PreparedGeneration.SetAssetClosure(
|
||||
publication.GfxObjectIds,
|
||||
publication.SetupObjectIds);
|
||||
return publication;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_physics.CancelCollisionGeneration(collisionAdmission, prepared);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -481,15 +498,20 @@ public sealed class LandblockPhysicsPublisher
|
|||
}
|
||||
else if (publication.RefloodOwnerIds is null)
|
||||
{
|
||||
publication.RefloodOwnerIds =
|
||||
_physics.CaptureCollisionDynamicOwners(
|
||||
RuntimeCollisionOwnerCaptureStep capture =
|
||||
_physics.AdvanceCollisionRetainedOwnerCapture(
|
||||
publication.CollisionAdmission,
|
||||
publication.PreparedGeneration);
|
||||
if (capture.Completed)
|
||||
{
|
||||
publication.RefloodOwnerIds =
|
||||
publication.PreparedGeneration.RetainedOwnerIds;
|
||||
}
|
||||
}
|
||||
else if (publication.RefloodCursor
|
||||
< publication.RefloodOwnerIds.Length)
|
||||
< publication.RefloodOwnerIds.Count)
|
||||
{
|
||||
_physics.RefreshCollisionDynamicOwner(
|
||||
_physics.RefreshCollisionRetainedOwner(
|
||||
publication.CollisionAdmission,
|
||||
publication.PreparedGeneration,
|
||||
publication.RefloodOwnerIds[publication.RefloodCursor]);
|
||||
|
|
@ -507,6 +529,25 @@ public sealed class LandblockPhysicsPublisher
|
|||
LogMissingSceneryBounds(landblock, publication.StagingCache);
|
||||
publication.RefloodCommitted = true;
|
||||
}
|
||||
else if (!publication.SealCommitted)
|
||||
{
|
||||
RuntimeCollisionSealStep seal =
|
||||
_physics.AdvanceCollisionGenerationSeal(
|
||||
publication.CollisionAdmission,
|
||||
publication.PreparedGeneration);
|
||||
if (seal.WorkUnits > 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision seal exceeded its one-unit publication budget.");
|
||||
}
|
||||
publication.SealCommitted = seal.Completed;
|
||||
if (seal.Restarted)
|
||||
{
|
||||
publication.RefloodOwnerIds = null;
|
||||
publication.RefloodCursor = 0;
|
||||
publication.RefloodCommitted = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeCollisionGenerationCommit commit =
|
||||
|
|
@ -515,9 +556,13 @@ public sealed class LandblockPhysicsPublisher
|
|||
publication.PreparedGeneration);
|
||||
if (!commit.Committed)
|
||||
{
|
||||
publication.RefloodOwnerIds = commit.DirtyDynamicOwnerIds;
|
||||
_physics.RestartCollisionRetainedOwnerCapture(
|
||||
publication.CollisionAdmission,
|
||||
publication.PreparedGeneration);
|
||||
publication.RefloodOwnerIds = null;
|
||||
publication.RefloodCursor = 0;
|
||||
publication.RefloodCommitted = false;
|
||||
publication.SealCommitted = false;
|
||||
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1011,5 +1056,11 @@ public sealed class LandblockPhysicsPublisher
|
|||
"The physics publication receipt belongs to another publisher.",
|
||||
nameof(publication));
|
||||
}
|
||||
if (!publication.CompletionCommitted)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(
|
||||
publication.PreparedGeneration.IsDisposed,
|
||||
publication);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -731,13 +731,15 @@ public sealed class LandblockPresentationPipeline
|
|||
< transaction.Build.Landblock.Entities.Count
|
||||
? 1
|
||||
: transaction.PhysicsPublication
|
||||
.RefloodOwnerIds is not null
|
||||
&& transaction.PhysicsPublication
|
||||
.RefloodOwnerIds is null
|
||||
|| transaction.PhysicsPublication
|
||||
.RefloodCursor
|
||||
< transaction.PhysicsPublication
|
||||
.RefloodOwnerIds.Length
|
||||
? 1
|
||||
: 0;
|
||||
.RefloodOwnerIds.Count
|
||||
|| !transaction.PhysicsPublication
|
||||
.SealCommitted
|
||||
? 1
|
||||
: 0;
|
||||
if (!TryRun(
|
||||
new StreamingWorkCost(
|
||||
EntityOperations: entityOperations),
|
||||
|
|
|
|||
|
|
@ -121,31 +121,24 @@ public sealed class PhysicsDataCache
|
|||
return copy;
|
||||
}
|
||||
|
||||
internal PreparedPhysicsDataCacheLandblock PrepareLandblockReplacement(
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
PhysicsDataCache staging,
|
||||
uint landblockId,
|
||||
ReadOnlySpan<uint> gfxObjectIds,
|
||||
ReadOnlySpan<uint> setupIds)
|
||||
{
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
return new PreparedPhysicsDataCacheLandblock(
|
||||
prefix,
|
||||
CaptureRequested(_gfxObj, gfxObjectIds),
|
||||
CaptureRequested(_visualBounds, gfxObjectIds),
|
||||
CaptureRequested(_flatGfxObj, gfxObjectIds),
|
||||
CaptureRequested(_setup, setupIds),
|
||||
CaptureRequested(_flatSetup, setupIds),
|
||||
CapturePrefix(_cellStruct, prefix),
|
||||
CapturePrefix(_flatCellStruct, prefix),
|
||||
CapturePrefix(_flatEnvCell, prefix),
|
||||
CapturePrefix(_buildings, prefix),
|
||||
CellGraph.PrepareLandblockReplacement(prefix));
|
||||
}
|
||||
uint[] gfxObjectIds,
|
||||
uint[] setupIds) => new(
|
||||
this,
|
||||
staging,
|
||||
landblockId,
|
||||
gfxObjectIds,
|
||||
setupIds);
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedPhysicsDataCacheLandblock replacement)
|
||||
{
|
||||
RemoveCellsForLandblock(replacement.LandblockPrefix);
|
||||
RemoveBuildingsForLandblock(replacement.LandblockPrefix);
|
||||
RemoveEntries(_cellStruct, replacement.CellIdsToRemove);
|
||||
RemoveEntries(_flatCellStruct, replacement.FlatCellIdsToRemove);
|
||||
RemoveEntries(_flatEnvCell, replacement.FlatEnvCellIdsToRemove);
|
||||
RemoveEntries(_buildings, replacement.BuildingIdsToRemove);
|
||||
CommitEntries(_gfxObj, replacement.GfxObjects, replace: false);
|
||||
CommitEntries(_visualBounds, replacement.VisualBounds, replace: false);
|
||||
CommitEntries(_flatGfxObj, replacement.FlatGfxObjects, replace: false);
|
||||
|
|
@ -166,34 +159,14 @@ public sealed class PhysicsDataCache
|
|||
destination.TryAdd(id, value);
|
||||
}
|
||||
|
||||
private static KeyValuePair<uint, T>[] CaptureRequested<T>(
|
||||
ConcurrentDictionary<uint, T> source,
|
||||
ReadOnlySpan<uint> ids)
|
||||
{
|
||||
var result = new List<KeyValuePair<uint, T>>(ids.Length);
|
||||
for (int index = 0; index < ids.Length; index++)
|
||||
{
|
||||
uint id = ids[index];
|
||||
if (source.TryGetValue(id, out T? value))
|
||||
result.Add(new KeyValuePair<uint, T>(id, value));
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static KeyValuePair<uint, T>[] CapturePrefix<T>(
|
||||
ConcurrentDictionary<uint, T> source,
|
||||
uint prefix) => source
|
||||
.Where(pair => (pair.Key & 0xFFFF0000u) == prefix)
|
||||
.OrderBy(static pair => pair.Key)
|
||||
.ToArray();
|
||||
|
||||
private static void CommitEntries<T>(
|
||||
ConcurrentDictionary<uint, T> destination,
|
||||
KeyValuePair<uint, T>[] entries,
|
||||
IReadOnlyList<KeyValuePair<uint, T>> entries,
|
||||
bool replace)
|
||||
{
|
||||
foreach ((uint id, T value) in entries)
|
||||
for (int index = 0; index < entries.Count; index++)
|
||||
{
|
||||
(uint id, T value) = entries[index];
|
||||
if (replace)
|
||||
destination[id] = value;
|
||||
else
|
||||
|
|
@ -201,6 +174,14 @@ public sealed class PhysicsDataCache
|
|||
}
|
||||
}
|
||||
|
||||
private static void RemoveEntries<T>(
|
||||
ConcurrentDictionary<uint, T> destination,
|
||||
IReadOnlyList<uint> ids)
|
||||
{
|
||||
for (int index = 0; index < ids.Count; index++)
|
||||
destination.TryRemove(ids[index], out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract and cache the physics BSP + polygon data from a GfxObj,
|
||||
/// PLUS always cache a visual AABB from the vertex data regardless of
|
||||
|
|
@ -1043,19 +1024,268 @@ public sealed class PhysicsDataCache
|
|||
|
||||
/// <summary>Test helper, mirrors <see cref="RegisterCellStructForTest"/>.</summary>
|
||||
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) => _buildings[landcellId] = b;
|
||||
|
||||
internal sealed class LandblockReplacementBuilder : IDisposable
|
||||
{
|
||||
private readonly PhysicsDataCache _active;
|
||||
private readonly PhysicsDataCache _staging;
|
||||
private readonly uint _prefix;
|
||||
private readonly uint[] _gfxIds;
|
||||
private readonly uint[] _setupIds;
|
||||
private readonly List<KeyValuePair<uint, GfxObjPhysics>> _gfx = new();
|
||||
private readonly List<KeyValuePair<uint, GfxObjVisualBounds>> _bounds = new();
|
||||
private readonly List<KeyValuePair<uint, FlatGfxObjCollisionAsset>> _flatGfx = new();
|
||||
private readonly List<KeyValuePair<uint, SetupPhysics>> _setups = new();
|
||||
private readonly List<KeyValuePair<uint, FlatSetupCollision>> _flatSetups = new();
|
||||
private readonly List<KeyValuePair<uint, CellPhysics>> _cells = new();
|
||||
private readonly List<KeyValuePair<uint, FlatCellStructureCollisionAsset>> _flatCells = new();
|
||||
private readonly List<KeyValuePair<uint, FlatEnvCellTopology>> _flatEnvCells = new();
|
||||
private readonly List<KeyValuePair<uint, BuildingPhysics>> _buildings = new();
|
||||
private readonly HashSet<uint> _cellIds = new();
|
||||
private readonly HashSet<uint> _flatCellIds = new();
|
||||
private readonly HashSet<uint> _flatEnvCellIds = new();
|
||||
private readonly HashSet<uint> _buildingIds = new();
|
||||
private readonly List<uint> _removeCells = new();
|
||||
private readonly List<uint> _removeFlatCells = new();
|
||||
private readonly List<uint> _removeFlatEnvCells = new();
|
||||
private readonly List<uint> _removeBuildings = new();
|
||||
private readonly UcgCellGraph.LandblockReplacementBuilder _cellGraph;
|
||||
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cellEnumerator;
|
||||
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>? _flatCellEnumerator;
|
||||
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvEnumerator;
|
||||
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildingEnumerator;
|
||||
private int _phase;
|
||||
private int _cursor;
|
||||
|
||||
internal LandblockReplacementBuilder(
|
||||
PhysicsDataCache active,
|
||||
PhysicsDataCache staging,
|
||||
uint landblockId,
|
||||
uint[] gfxIds,
|
||||
uint[] setupIds)
|
||||
{
|
||||
_active = active;
|
||||
_staging = staging;
|
||||
_prefix = landblockId & 0xFFFF0000u;
|
||||
_gfxIds = gfxIds;
|
||||
_setupIds = setupIds;
|
||||
_cellGraph = active.CellGraph.CreateLandblockReplacementBuilder(
|
||||
staging.CellGraph,
|
||||
_prefix);
|
||||
}
|
||||
|
||||
internal int WorkUnits { get; private set; }
|
||||
internal PreparedPhysicsDataCacheLandblock? Prepared { get; private set; }
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (_cursor < _gfxIds.Length)
|
||||
{
|
||||
uint id = _gfxIds[_cursor++];
|
||||
Capture(_staging._gfxObj, id, _gfx);
|
||||
Capture(_staging._visualBounds, id, _bounds);
|
||||
Capture(_staging._flatGfxObj, id, _flatGfx);
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_cursor = 0;
|
||||
_phase++;
|
||||
return false;
|
||||
case 1:
|
||||
if (_cursor < _setupIds.Length)
|
||||
{
|
||||
uint id = _setupIds[_cursor++];
|
||||
Capture(_staging._setup, id, _setups);
|
||||
Capture(_staging._flatSetup, id, _flatSetups);
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_phase++;
|
||||
return false;
|
||||
case 2:
|
||||
_cellEnumerator ??= _staging._cellStruct.GetEnumerator();
|
||||
if (CapturePrefixOne(_cellEnumerator, _prefix, _cells, _cellIds))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_cellEnumerator.Dispose();
|
||||
_cellEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 3:
|
||||
_cellEnumerator ??= _active._cellStruct.GetEnumerator();
|
||||
if (CaptureRemovalOne(_cellEnumerator, _prefix, _cellIds, _removeCells))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_cellEnumerator.Dispose();
|
||||
_cellEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 4:
|
||||
_flatCellEnumerator ??= _staging._flatCellStruct.GetEnumerator();
|
||||
if (CapturePrefixOne(_flatCellEnumerator, _prefix, _flatCells, _flatCellIds))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_flatCellEnumerator.Dispose();
|
||||
_flatCellEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 5:
|
||||
_flatCellEnumerator ??= _active._flatCellStruct.GetEnumerator();
|
||||
if (CaptureRemovalOne(_flatCellEnumerator, _prefix, _flatCellIds, _removeFlatCells))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_flatCellEnumerator.Dispose();
|
||||
_flatCellEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 6:
|
||||
_flatEnvEnumerator ??= _staging._flatEnvCell.GetEnumerator();
|
||||
if (CapturePrefixOne(_flatEnvEnumerator, _prefix, _flatEnvCells, _flatEnvCellIds))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_flatEnvEnumerator.Dispose();
|
||||
_flatEnvEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 7:
|
||||
_flatEnvEnumerator ??= _active._flatEnvCell.GetEnumerator();
|
||||
if (CaptureRemovalOne(_flatEnvEnumerator, _prefix, _flatEnvCellIds, _removeFlatEnvCells))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_flatEnvEnumerator.Dispose();
|
||||
_flatEnvEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 8:
|
||||
_buildingEnumerator ??= _staging._buildings.GetEnumerator();
|
||||
if (CapturePrefixOne(_buildingEnumerator, _prefix, _buildings, _buildingIds))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_buildingEnumerator.Dispose();
|
||||
_buildingEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 9:
|
||||
_buildingEnumerator ??= _active._buildings.GetEnumerator();
|
||||
if (CaptureRemovalOne(_buildingEnumerator, _prefix, _buildingIds, _removeBuildings))
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_buildingEnumerator.Dispose();
|
||||
_buildingEnumerator = null;
|
||||
_phase++;
|
||||
return false;
|
||||
case 10:
|
||||
WorkUnits++;
|
||||
if (!_cellGraph.Advance())
|
||||
return false;
|
||||
Prepared = new PreparedPhysicsDataCacheLandblock(
|
||||
_prefix,
|
||||
_gfx,
|
||||
_bounds,
|
||||
_flatGfx,
|
||||
_setups,
|
||||
_flatSetups,
|
||||
_removeCells,
|
||||
_cells,
|
||||
_removeFlatCells,
|
||||
_flatCells,
|
||||
_removeFlatEnvCells,
|
||||
_flatEnvCells,
|
||||
_removeBuildings,
|
||||
_buildings,
|
||||
_cellGraph.Prepared!);
|
||||
_phase++;
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Capture<T>(
|
||||
ConcurrentDictionary<uint, T> source,
|
||||
uint id,
|
||||
List<KeyValuePair<uint, T>> destination)
|
||||
{
|
||||
if (source.TryGetValue(id, out T? value))
|
||||
destination.Add(new KeyValuePair<uint, T>(id, value));
|
||||
}
|
||||
|
||||
private static bool CapturePrefixOne<T>(
|
||||
IEnumerator<KeyValuePair<uint, T>> enumerator,
|
||||
uint prefix,
|
||||
List<KeyValuePair<uint, T>> destination,
|
||||
HashSet<uint> ids)
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
return false;
|
||||
KeyValuePair<uint, T> pair = enumerator.Current;
|
||||
if ((pair.Key & 0xFFFF0000u) == prefix)
|
||||
{
|
||||
destination.Add(pair);
|
||||
ids.Add(pair.Key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CaptureRemovalOne<T>(
|
||||
IEnumerator<KeyValuePair<uint, T>> enumerator,
|
||||
uint prefix,
|
||||
HashSet<uint> retained,
|
||||
List<uint> destination)
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
return false;
|
||||
uint id = enumerator.Current.Key;
|
||||
if ((id & 0xFFFF0000u) == prefix && !retained.Contains(id))
|
||||
destination.Add(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cellEnumerator?.Dispose();
|
||||
_flatCellEnumerator?.Dispose();
|
||||
_flatEnvEnumerator?.Dispose();
|
||||
_buildingEnumerator?.Dispose();
|
||||
_cellGraph.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PreparedPhysicsDataCacheLandblock(
|
||||
uint LandblockPrefix,
|
||||
KeyValuePair<uint, GfxObjPhysics>[] GfxObjects,
|
||||
KeyValuePair<uint, GfxObjVisualBounds>[] VisualBounds,
|
||||
KeyValuePair<uint, FlatGfxObjCollisionAsset>[] FlatGfxObjects,
|
||||
KeyValuePair<uint, SetupPhysics>[] Setups,
|
||||
KeyValuePair<uint, FlatSetupCollision>[] FlatSetups,
|
||||
KeyValuePair<uint, CellPhysics>[] Cells,
|
||||
KeyValuePair<uint, FlatCellStructureCollisionAsset>[] FlatCells,
|
||||
KeyValuePair<uint, FlatEnvCellTopology>[] FlatEnvCells,
|
||||
KeyValuePair<uint, BuildingPhysics>[] Buildings,
|
||||
IReadOnlyList<KeyValuePair<uint, GfxObjPhysics>> GfxObjects,
|
||||
IReadOnlyList<KeyValuePair<uint, GfxObjVisualBounds>> VisualBounds,
|
||||
IReadOnlyList<KeyValuePair<uint, FlatGfxObjCollisionAsset>> FlatGfxObjects,
|
||||
IReadOnlyList<KeyValuePair<uint, SetupPhysics>> Setups,
|
||||
IReadOnlyList<KeyValuePair<uint, FlatSetupCollision>> FlatSetups,
|
||||
IReadOnlyList<uint> CellIdsToRemove,
|
||||
IReadOnlyList<KeyValuePair<uint, CellPhysics>> Cells,
|
||||
IReadOnlyList<uint> FlatCellIdsToRemove,
|
||||
IReadOnlyList<KeyValuePair<uint, FlatCellStructureCollisionAsset>> FlatCells,
|
||||
IReadOnlyList<uint> FlatEnvCellIdsToRemove,
|
||||
IReadOnlyList<KeyValuePair<uint, FlatEnvCellTopology>> FlatEnvCells,
|
||||
IReadOnlyList<uint> BuildingIdsToRemove,
|
||||
IReadOnlyList<KeyValuePair<uint, BuildingPhysics>> Buildings,
|
||||
PreparedCellGraphLandblock CellGraph);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -182,12 +182,12 @@ public sealed class PhysicsEngine
|
|||
return staging;
|
||||
}
|
||||
|
||||
internal PreparedPhysicsEngineLandblock PrepareLandblockReplacement(
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
PhysicsEngine staging,
|
||||
uint landblockId,
|
||||
ReadOnlySpan<uint> gfxObjectIds,
|
||||
ReadOnlySpan<uint> setupIds,
|
||||
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
|
||||
uint[] gfxObjectIds,
|
||||
uint[] setupIds,
|
||||
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staging);
|
||||
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
|
@ -201,31 +201,25 @@ public sealed class PhysicsEngine
|
|||
PhysicsDataCache stagingCache = staging.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Staging collision engine has no data cache.");
|
||||
return new PreparedPhysicsEngineLandblock(
|
||||
return new LandblockReplacementBuilder(
|
||||
canonical,
|
||||
landblock,
|
||||
stagingCache.PrepareLandblockReplacement(
|
||||
(DataCache ?? throw new InvalidOperationException(
|
||||
"Active collision engine has no data cache."))
|
||||
.CreateLandblockReplacementBuilder(
|
||||
stagingCache,
|
||||
canonical,
|
||||
gfxObjectIds,
|
||||
setupIds),
|
||||
ShadowObjects.PrepareLandblockReplacement(
|
||||
ShadowObjects.CreateLandblockReplacementBuilder(
|
||||
staging.ShadowObjects,
|
||||
canonical,
|
||||
expectedDynamicVersions));
|
||||
expectedRetainedVersions));
|
||||
}
|
||||
|
||||
internal bool ValidateLandblockReplacement(
|
||||
PreparedPhysicsEngineLandblock replacement) =>
|
||||
ShadowObjects.ValidateLandblockReplacement(replacement.Shadows);
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedPhysicsEngineLandblock replacement)
|
||||
{
|
||||
if (!ValidateLandblockReplacement(replacement))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision generation changed after it was sealed.");
|
||||
}
|
||||
(DataCache ?? throw new InvalidOperationException(
|
||||
"Active collision engine has no data cache."))
|
||||
.CommitLandblockReplacement(replacement.DataCache);
|
||||
|
|
@ -253,6 +247,64 @@ public sealed class PhysicsEngine
|
|||
internal ShadowObjectRegistry.PreparedLandblockShadowReplacement Shadows { get; }
|
||||
}
|
||||
|
||||
internal sealed class LandblockReplacementBuilder : IDisposable
|
||||
{
|
||||
private readonly uint _landblockId;
|
||||
private readonly LandblockPhysics _landblock;
|
||||
private readonly PhysicsDataCache.LandblockReplacementBuilder _data;
|
||||
private readonly ShadowObjectRegistry.LandblockReplacementBuilder _shadows;
|
||||
private int _phase;
|
||||
|
||||
internal LandblockReplacementBuilder(
|
||||
uint landblockId,
|
||||
LandblockPhysics landblock,
|
||||
PhysicsDataCache.LandblockReplacementBuilder data,
|
||||
ShadowObjectRegistry.LandblockReplacementBuilder shadows)
|
||||
{
|
||||
_landblockId = landblockId;
|
||||
_landblock = landblock;
|
||||
_data = data;
|
||||
_shadows = shadows;
|
||||
}
|
||||
|
||||
internal int WorkUnits => _data.WorkUnits + _shadows.WorkUnits;
|
||||
internal bool IsStable => _shadows.IsStable;
|
||||
internal PreparedPhysicsEngineLandblock? Prepared { get; private set; }
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
if (_phase == 0)
|
||||
{
|
||||
if (!_data.Advance())
|
||||
return false;
|
||||
_phase++;
|
||||
return false;
|
||||
}
|
||||
if (_phase == 1)
|
||||
{
|
||||
if (!_shadows.Advance())
|
||||
return false;
|
||||
if (IsStable && _data.Prepared is not null
|
||||
&& _shadows.Prepared is not null)
|
||||
{
|
||||
Prepared = new PreparedPhysicsEngineLandblock(
|
||||
_landblockId,
|
||||
_landblock,
|
||||
_data.Prepared,
|
||||
_shadows.Prepared);
|
||||
}
|
||||
_phase++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
_shadows.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a landblock with its terrain surface, indoor cells, portal
|
||||
/// planes, and world-space origin offset.
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public sealed class ShadowObjectRegistry
|
|||
/// </summary>
|
||||
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
|
||||
private readonly Dictionary<uint, ulong> _ownerVersions = new();
|
||||
private ulong _mutationVersion;
|
||||
|
||||
internal sealed record RegistrationRecord(
|
||||
uint SeedCellId,
|
||||
|
|
@ -76,8 +77,88 @@ public sealed class ShadowObjectRegistry
|
|||
private void BumpOwnerVersion(uint entityId)
|
||||
{
|
||||
_ownerVersions[entityId] = checked(GetOwnerVersion(entityId) + 1UL);
|
||||
_mutationVersion = checked(_mutationVersion + 1UL);
|
||||
}
|
||||
|
||||
internal ulong MutationVersion => _mutationVersion;
|
||||
|
||||
internal RetainedRefloodOwnerScan CreateRetainedRefloodOwnerScan(
|
||||
uint landblockId) => new(this, landblockId & 0xFFFF0000u);
|
||||
|
||||
internal sealed class RetainedRefloodOwnerScan : IDisposable
|
||||
{
|
||||
private readonly ShadowObjectRegistry _owner;
|
||||
private readonly uint _prefix;
|
||||
private readonly ulong _sourceMutationVersion;
|
||||
private Dictionary<uint, RegistrationRecord>.Enumerator _enumerator;
|
||||
private bool _completed;
|
||||
|
||||
internal RetainedRefloodOwnerScan(
|
||||
ShadowObjectRegistry owner,
|
||||
uint prefix)
|
||||
{
|
||||
_owner = owner;
|
||||
_prefix = prefix;
|
||||
_sourceMutationVersion = owner.MutationVersion;
|
||||
_enumerator = owner._entityReg.GetEnumerator();
|
||||
}
|
||||
|
||||
internal RetainedRefloodOwnerScanStep Advance()
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: true,
|
||||
Stable: _owner.MutationVersion == _sourceMutationVersion,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
}
|
||||
if (_owner.MutationVersion != _sourceMutationVersion)
|
||||
{
|
||||
_completed = true;
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: true,
|
||||
Stable: false,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
}
|
||||
if (!_enumerator.MoveNext())
|
||||
{
|
||||
_completed = true;
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: true,
|
||||
Stable: true,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
}
|
||||
|
||||
(uint ownerId, RegistrationRecord registration) =
|
||||
_enumerator.Current;
|
||||
bool retained = !_owner._suspendedEntities.Contains(ownerId)
|
||||
&& (!registration.IsStatic
|
||||
|| (registration.SeedCellId & 0xFFFF0000u) != _prefix)
|
||||
&& _owner.OwnerTouchesLandblock(ownerId, _prefix);
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: false,
|
||||
Stable: true,
|
||||
HasOwner: retained,
|
||||
OwnerId: retained ? ownerId : 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
}
|
||||
|
||||
public void Dispose() => _enumerator.Dispose();
|
||||
}
|
||||
|
||||
internal readonly record struct RetainedRefloodOwnerScanStep(
|
||||
bool Completed,
|
||||
bool Stable,
|
||||
bool HasOwner,
|
||||
uint OwnerId,
|
||||
ulong SourceMutationVersion);
|
||||
|
||||
/// <summary>
|
||||
/// The flood's data source (cells, buildings, terrain origins). Wired by
|
||||
/// <see cref="PhysicsEngine"/> when its own <c>DataCache</c> is set.
|
||||
|
|
@ -624,8 +705,14 @@ public sealed class ShadowObjectRegistry
|
|||
{
|
||||
// Suspended dynamic objects have no cell rows, but their retained
|
||||
// registration must still receive authoritative state changes.
|
||||
if (_entityReg.TryGetValue(entityId, out var retainedRegistration))
|
||||
_entityReg[entityId] = retainedRegistration with { State = newState };
|
||||
bool retained = _entityReg.TryGetValue(
|
||||
entityId,
|
||||
out RegistrationRecord? retainedRegistration);
|
||||
if (retained)
|
||||
{
|
||||
_entityReg[entityId] = retainedRegistration! with { State = newState };
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
return; // not registered — no-op
|
||||
|
|
@ -640,9 +727,6 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
}
|
||||
|
||||
if (_entityReg.TryGetValue(entityId, out var reg))
|
||||
_entityReg[entityId] = reg with { State = newState };
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>Remove an entity from all cells it was registered in.</summary>
|
||||
|
|
@ -657,7 +741,7 @@ public sealed class ShadowObjectRegistry
|
|||
foreach (var cellId in cellIds)
|
||||
{
|
||||
if (_cells.TryGetValue(cellId, out var list))
|
||||
list.RemoveAll(e => e.EntityId == entityId);
|
||||
RemoveOwnerRows(list, entityId);
|
||||
}
|
||||
_entityToCells.Remove(entityId);
|
||||
}
|
||||
|
|
@ -669,6 +753,17 @@ public sealed class ShadowObjectRegistry
|
|||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
private static void RemoveOwnerRows(
|
||||
List<ShadowEntry> entries,
|
||||
uint entityId)
|
||||
{
|
||||
for (int index = entries.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (entries[index].EntityId == entityId)
|
||||
entries.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logically tear down every static object owned by a landblock, including
|
||||
/// shadow rows flooded into adjacent landblocks. Dynamic/server-live owners
|
||||
|
|
@ -824,6 +919,11 @@ public sealed class ShadowObjectRegistry
|
|||
/// <summary>Suspended logical registrations awaiting spatial re-entry.</summary>
|
||||
public int SuspendedRegistrationCount => _suspendedEntities.Count;
|
||||
|
||||
public bool HasOwnerRowsInLandblock(uint ownerId, uint landblockId) =>
|
||||
_entityToCells.TryGetValue(ownerId, out List<uint>? cells)
|
||||
&& cells.Exists(cell =>
|
||||
(cell & 0xFFFF0000u) == (landblockId & 0xFFFF0000u));
|
||||
|
||||
/// <summary>
|
||||
/// Copies the committed registry into an off-side collision generation.
|
||||
/// All mutable lists and sets are cloned; immutable registration and shape
|
||||
|
|
@ -860,16 +960,7 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
foreach ((uint ownerId, ulong version) in source._ownerVersions)
|
||||
_ownerVersions[ownerId] = version;
|
||||
}
|
||||
|
||||
internal uint[] CaptureDynamicRefloodOwnersForLandblock(
|
||||
uint landblockId)
|
||||
{
|
||||
uint[] owners = CaptureRefloodOwnersForLandblock(landblockId);
|
||||
return owners.Where(ownerId =>
|
||||
_entityReg.TryGetValue(ownerId, out RegistrationRecord? record)
|
||||
&& !record.IsStatic)
|
||||
.ToArray();
|
||||
_mutationVersion = source._mutationVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -877,7 +968,7 @@ public sealed class ShadowObjectRegistry
|
|||
/// it against the staging generation's complete cell graph. The returned
|
||||
/// source version is the commit-time freshness token.
|
||||
/// </summary>
|
||||
internal bool RefreshDynamicOwnerFrom(
|
||||
internal bool RefreshRetainedOwnerFrom(
|
||||
ShadowObjectRegistry source,
|
||||
uint entityId,
|
||||
uint landblockId,
|
||||
|
|
@ -889,8 +980,10 @@ public sealed class ShadowObjectRegistry
|
|||
if (!source._entityReg.TryGetValue(
|
||||
entityId,
|
||||
out RegistrationRecord? registration)
|
||||
|| registration.IsStatic
|
||||
|| source._suspendedEntities.Contains(entityId)
|
||||
|| (registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u)
|
||||
== (landblockId & 0xFFFF0000u))
|
||||
|| !source.OwnerTouchesLandblock(entityId, landblockId))
|
||||
{
|
||||
return false;
|
||||
|
|
@ -912,7 +1005,7 @@ public sealed class ShadowObjectRegistry
|
|||
0f,
|
||||
landblockId,
|
||||
registration.SeedCellId,
|
||||
isStatic: false);
|
||||
isStatic: registration.IsStatic);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -931,92 +1024,42 @@ public sealed class ShadowObjectRegistry
|
|||
registration.State,
|
||||
registration.Flags,
|
||||
registration.SeedCellId,
|
||||
isStatic: false);
|
||||
isStatic: registration.IsStatic);
|
||||
}
|
||||
|
||||
if (source._withdrawnPrefixesByOwner.TryGetValue(
|
||||
entityId,
|
||||
out HashSet<uint>? sourceWithdrawn))
|
||||
{
|
||||
var retainedWithdrawn = new HashSet<uint>(sourceWithdrawn);
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
if (_entityToCells.TryGetValue(entityId, out List<uint>? cells)
|
||||
&& cells.Exists(cell => (cell & 0xFFFF0000u) == prefix))
|
||||
{
|
||||
retainedWithdrawn.Remove(prefix);
|
||||
}
|
||||
if (retainedWithdrawn.Count != 0)
|
||||
_withdrawnPrefixesByOwner[entityId] = retainedWithdrawn;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal uint[] FindDirtyDynamicOwners(
|
||||
uint landblockId,
|
||||
IReadOnlyDictionary<uint, ulong> expectedVersions)
|
||||
{
|
||||
var dirty = new HashSet<uint>(
|
||||
CaptureDynamicRefloodOwnersForLandblock(landblockId));
|
||||
dirty.UnionWith(expectedVersions.Keys);
|
||||
dirty.RemoveWhere(ownerId =>
|
||||
expectedVersions.TryGetValue(ownerId, out ulong expected)
|
||||
&& OwnerTouchesLandblock(ownerId, landblockId)
|
||||
&& GetOwnerVersion(ownerId) == expected);
|
||||
uint[] result = dirty.ToArray();
|
||||
Array.Sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal PreparedLandblockShadowReplacement PrepareLandblockReplacement(
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
ShadowObjectRegistry staging,
|
||||
uint landblockId,
|
||||
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staging);
|
||||
uint[] dirty = FindDirtyDynamicOwners(
|
||||
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions) => new(
|
||||
this,
|
||||
staging,
|
||||
landblockId,
|
||||
expectedDynamicVersions);
|
||||
if (dirty.Length != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dynamic shadow owners changed before collision generation sealing.");
|
||||
}
|
||||
|
||||
var owners = new HashSet<uint>(CaptureStaticOwnersForLandblock(landblockId));
|
||||
owners.UnionWith(staging.CaptureStaticOwnersForLandblock(landblockId));
|
||||
owners.UnionWith(expectedDynamicVersions.Keys);
|
||||
uint[] ownerIds = owners.ToArray();
|
||||
Array.Sort(ownerIds);
|
||||
var states = new List<PreparedShadowOwnerState>(ownerIds.Length);
|
||||
foreach (uint ownerId in ownerIds)
|
||||
{
|
||||
if (staging.TryCaptureOwnerState(ownerId, out PreparedShadowOwnerState? state)
|
||||
&& state is not null)
|
||||
states.Add(state);
|
||||
}
|
||||
return new PreparedLandblockShadowReplacement(
|
||||
landblockId & 0xFFFF0000u,
|
||||
ownerIds,
|
||||
states.ToArray(),
|
||||
expectedDynamicVersions.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => pair.Value));
|
||||
}
|
||||
|
||||
internal bool ValidateLandblockReplacement(
|
||||
PreparedLandblockShadowReplacement replacement)
|
||||
{
|
||||
foreach ((uint ownerId, ulong version) in replacement.DynamicVersions)
|
||||
{
|
||||
if (GetOwnerVersion(ownerId) != version
|
||||
|| !OwnerTouchesLandblock(ownerId, replacement.LandblockPrefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return CaptureDynamicRefloodOwnersForLandblock(
|
||||
replacement.LandblockPrefix)
|
||||
.SequenceEqual(replacement.DynamicVersions.Keys.Order());
|
||||
}
|
||||
expectedRetainedVersions);
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedLandblockShadowReplacement replacement)
|
||||
{
|
||||
if (!ValidateLandblockReplacement(replacement))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dynamic shadow owners changed before collision generation commit.");
|
||||
}
|
||||
|
||||
foreach (uint ownerId in replacement.OwnerIds)
|
||||
Deregister(ownerId);
|
||||
foreach (PreparedShadowOwnerState state in replacement.OwnerStates)
|
||||
InstallOwnerState(state);
|
||||
for (int index = 0; index < replacement.OwnerIds.Count; index++)
|
||||
Deregister(replacement.OwnerIds[index]);
|
||||
for (int index = 0; index < replacement.OwnerStates.Count; index++)
|
||||
InstallOwnerState(replacement.OwnerStates[index]);
|
||||
}
|
||||
|
||||
private bool OwnerTouchesLandblock(uint entityId, uint landblockId)
|
||||
|
|
@ -1071,10 +1114,10 @@ public sealed class ShadowObjectRegistry
|
|||
entityId,
|
||||
registration,
|
||||
shapes,
|
||||
cells?.ToArray() ?? Array.Empty<uint>(),
|
||||
rows.ToArray(),
|
||||
cells is null ? null : new List<uint>(cells),
|
||||
rows,
|
||||
_suspendedEntities.Contains(entityId),
|
||||
withdrawn?.ToArray() ?? Array.Empty<uint>());
|
||||
withdrawn is null ? null : new HashSet<uint>(withdrawn));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1085,49 +1128,194 @@ public sealed class ShadowObjectRegistry
|
|||
_entityShapes[state.EntityId] = state.Shapes;
|
||||
if (state.Suspended)
|
||||
_suspendedEntities.Add(state.EntityId);
|
||||
if (state.WithdrawnPrefixes.Length != 0)
|
||||
if (state.WithdrawnPrefixes is not null)
|
||||
{
|
||||
_withdrawnPrefixesByOwner[state.EntityId] =
|
||||
new HashSet<uint>(state.WithdrawnPrefixes);
|
||||
_withdrawnPrefixesByOwner[state.EntityId] = state.WithdrawnPrefixes;
|
||||
}
|
||||
if (state.CellIds.Length != 0)
|
||||
_entityToCells[state.EntityId] = new List<uint>(state.CellIds);
|
||||
foreach (PreparedShadowCellRows row in state.Rows)
|
||||
if (state.CellIds is not null)
|
||||
_entityToCells[state.EntityId] = state.CellIds;
|
||||
for (int rowIndex = 0; rowIndex < state.Rows.Count; rowIndex++)
|
||||
{
|
||||
foreach (ShadowEntry entry in row.Entries)
|
||||
AddEntryToCell(entry, row.CellId);
|
||||
PreparedShadowCellRows row = state.Rows[rowIndex];
|
||||
for (int entryIndex = 0; entryIndex < row.Entries.Length; entryIndex++)
|
||||
AddEntryToCell(row.Entries[entryIndex], row.CellId);
|
||||
}
|
||||
BumpOwnerVersion(state.EntityId);
|
||||
}
|
||||
|
||||
internal sealed class LandblockReplacementBuilder : IDisposable
|
||||
{
|
||||
private readonly ShadowObjectRegistry _active;
|
||||
private readonly ShadowObjectRegistry _staging;
|
||||
private readonly uint _prefix;
|
||||
private readonly ulong _sourceMutationVersion;
|
||||
private readonly HashSet<uint> _owners = new();
|
||||
private readonly List<uint> _ownerIds = new();
|
||||
private readonly List<PreparedShadowOwnerState> _states = new();
|
||||
private IEnumerator<KeyValuePair<uint, ulong>>? _expectedEnumerator;
|
||||
private Dictionary<uint, RegistrationRecord>.Enumerator _registrationEnumerator;
|
||||
private HashSet<uint>.Enumerator _ownerEnumerator;
|
||||
private int _phase;
|
||||
|
||||
internal LandblockReplacementBuilder(
|
||||
ShadowObjectRegistry active,
|
||||
ShadowObjectRegistry staging,
|
||||
uint landblockId,
|
||||
IReadOnlyDictionary<uint, ulong> expected)
|
||||
{
|
||||
_active = active;
|
||||
_staging = staging;
|
||||
_prefix = landblockId & 0xFFFF0000u;
|
||||
_sourceMutationVersion = active.MutationVersion;
|
||||
_expectedEnumerator = expected.GetEnumerator();
|
||||
}
|
||||
|
||||
internal int WorkUnits { get; private set; }
|
||||
internal bool IsStable =>
|
||||
_active.MutationVersion == _sourceMutationVersion;
|
||||
internal PreparedLandblockShadowReplacement? Prepared { get; private set; }
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
if (!IsStable)
|
||||
return true;
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (_expectedEnumerator!.MoveNext())
|
||||
{
|
||||
(uint ownerId, ulong version) = _expectedEnumerator.Current;
|
||||
if (_active.GetOwnerVersion(ownerId) != version
|
||||
|| !_active.IsRetainedRefloodOwner(ownerId, _prefix))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
AddOwner(ownerId);
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_expectedEnumerator.Dispose();
|
||||
_expectedEnumerator = null;
|
||||
_registrationEnumerator = _active._entityReg.GetEnumerator();
|
||||
_phase++;
|
||||
return false;
|
||||
case 1:
|
||||
if (_registrationEnumerator.MoveNext())
|
||||
{
|
||||
(uint ownerId, RegistrationRecord registration) =
|
||||
_registrationEnumerator.Current;
|
||||
if (registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
|
||||
{
|
||||
AddOwner(ownerId);
|
||||
}
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_registrationEnumerator.Dispose();
|
||||
_registrationEnumerator = _staging._entityReg.GetEnumerator();
|
||||
_phase++;
|
||||
return false;
|
||||
case 2:
|
||||
if (_registrationEnumerator.MoveNext())
|
||||
{
|
||||
(uint ownerId, RegistrationRecord registration) =
|
||||
_registrationEnumerator.Current;
|
||||
if (registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
|
||||
{
|
||||
AddOwner(ownerId);
|
||||
}
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_registrationEnumerator.Dispose();
|
||||
_ownerEnumerator = _owners.GetEnumerator();
|
||||
_phase++;
|
||||
return false;
|
||||
case 3:
|
||||
if (_ownerEnumerator.MoveNext())
|
||||
{
|
||||
uint ownerId = _ownerEnumerator.Current;
|
||||
if (_staging.TryCaptureOwnerState(
|
||||
ownerId,
|
||||
out PreparedShadowOwnerState? state)
|
||||
&& state is not null)
|
||||
{
|
||||
_states.Add(state);
|
||||
}
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_ownerEnumerator.Dispose();
|
||||
if (IsStable)
|
||||
{
|
||||
Prepared = new PreparedLandblockShadowReplacement(
|
||||
_prefix,
|
||||
_ownerIds,
|
||||
_states);
|
||||
}
|
||||
_phase++;
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddOwner(uint ownerId)
|
||||
{
|
||||
if (_owners.Add(ownerId))
|
||||
_ownerIds.Add(ownerId);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_expectedEnumerator?.Dispose();
|
||||
if (_phase is 1 or 2)
|
||||
_registrationEnumerator.Dispose();
|
||||
if (_phase == 3)
|
||||
_ownerEnumerator.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsRetainedRefloodOwner(uint ownerId, uint landblockId)
|
||||
{
|
||||
if (!_entityReg.TryGetValue(ownerId, out RegistrationRecord? registration)
|
||||
|| _suspendedEntities.Contains(ownerId)
|
||||
|| (registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u)
|
||||
== (landblockId & 0xFFFF0000u)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return OwnerTouchesLandblock(ownerId, landblockId);
|
||||
}
|
||||
|
||||
internal sealed class PreparedLandblockShadowReplacement
|
||||
{
|
||||
internal PreparedLandblockShadowReplacement(
|
||||
uint landblockPrefix,
|
||||
uint[] ownerIds,
|
||||
PreparedShadowOwnerState[] ownerStates,
|
||||
Dictionary<uint, ulong> dynamicVersions)
|
||||
IReadOnlyList<uint> ownerIds,
|
||||
IReadOnlyList<PreparedShadowOwnerState> ownerStates)
|
||||
{
|
||||
LandblockPrefix = landblockPrefix;
|
||||
OwnerIds = ownerIds;
|
||||
OwnerStates = ownerStates;
|
||||
DynamicVersions = dynamicVersions;
|
||||
}
|
||||
|
||||
internal uint LandblockPrefix { get; }
|
||||
internal uint[] OwnerIds { get; }
|
||||
internal PreparedShadowOwnerState[] OwnerStates { get; }
|
||||
internal Dictionary<uint, ulong> DynamicVersions { get; }
|
||||
internal IReadOnlyList<uint> OwnerIds { get; }
|
||||
internal IReadOnlyList<PreparedShadowOwnerState> OwnerStates { get; }
|
||||
}
|
||||
|
||||
internal sealed record PreparedShadowOwnerState(
|
||||
uint EntityId,
|
||||
RegistrationRecord Registration,
|
||||
IReadOnlyList<ShadowShape>? Shapes,
|
||||
uint[] CellIds,
|
||||
PreparedShadowCellRows[] Rows,
|
||||
List<uint>? CellIds,
|
||||
IReadOnlyList<PreparedShadowCellRows> Rows,
|
||||
bool Suspended,
|
||||
uint[] WithdrawnPrefixes);
|
||||
HashSet<uint>? WithdrawnPrefixes);
|
||||
|
||||
internal sealed record PreparedShadowCellRows(
|
||||
uint CellId,
|
||||
|
|
@ -1146,6 +1334,7 @@ public sealed class ShadowObjectRegistry
|
|||
_entityShapes.Clear();
|
||||
_entityReg.Clear();
|
||||
_ownerVersions.Clear();
|
||||
_mutationVersion = 0UL;
|
||||
_fallback = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace AcDream.Core.World.Cells;
|
|||
public sealed class CellGraph
|
||||
{
|
||||
private readonly ConcurrentDictionary<uint, EnvCell> _envCells = new();
|
||||
private readonly ConcurrentDictionary<uint, (TerrainSurface Terrain, Vector3 Origin)> _terrain = new();
|
||||
private readonly ConcurrentDictionary<uint, CellGraphTerrain> _terrain = new();
|
||||
|
||||
/// <summary>The player's current cell — the render/lighting root. Written ONLY at the
|
||||
/// player chokepoint <see cref="AcDream.Core.Physics.PhysicsEngine.UpdatePlayerCurrCell"/>
|
||||
|
|
@ -34,7 +34,8 @@ public sealed class CellGraph
|
|||
|
||||
/// <param name="landblockPrefix">Any id in the cell's landblock; masked to (id & 0xFFFF0000).</param>
|
||||
public void RegisterTerrain(uint landblockPrefix, TerrainSurface terrain, Vector3 worldOrigin)
|
||||
=> _terrain[landblockPrefix & 0xFFFF0000u] = (terrain, worldOrigin);
|
||||
=> _terrain[landblockPrefix & 0xFFFF0000u] =
|
||||
new CellGraphTerrain(terrain, worldOrigin);
|
||||
|
||||
/// <summary>
|
||||
/// World origin (SW corner) of the landblock containing <paramref name="id"/>,
|
||||
|
|
@ -137,46 +138,36 @@ public sealed class CellGraph
|
|||
var copy = new CellGraph { CurrCell = CurrCell };
|
||||
foreach ((uint id, EnvCell cell) in _envCells)
|
||||
copy._envCells.TryAdd(id, cell);
|
||||
foreach ((uint id, (TerrainSurface Terrain, Vector3 Origin) terrain) in
|
||||
_terrain)
|
||||
foreach ((uint id, CellGraphTerrain terrain) in _terrain)
|
||||
{
|
||||
copy._terrain.TryAdd(id, terrain);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
internal PreparedCellGraphLandblock PrepareLandblockReplacement(
|
||||
uint landblockId)
|
||||
{
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
KeyValuePair<uint, EnvCell>[] envCells = _envCells
|
||||
.Where(static pair => (pair.Key & 0xFFFFu) >= 0x0100u)
|
||||
.Where(pair => (pair.Key & 0xFFFF0000u) == prefix)
|
||||
.OrderBy(static pair => pair.Key)
|
||||
.ToArray();
|
||||
bool hasTerrain = _terrain.TryGetValue(prefix, out var terrain);
|
||||
return new PreparedCellGraphLandblock(
|
||||
prefix,
|
||||
envCells,
|
||||
hasTerrain,
|
||||
terrain.Terrain,
|
||||
terrain.Origin,
|
||||
CurrCell?.Id ?? 0u);
|
||||
}
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
CellGraph staging,
|
||||
uint landblockId) => new(this, staging, landblockId);
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedCellGraphLandblock replacement)
|
||||
{
|
||||
uint currentCellId = CurrCell?.Id ?? 0u;
|
||||
RemoveLandblock(replacement.LandblockPrefix);
|
||||
for (int index = 0; index < replacement.EnvCellIdsToRemove.Count; index++)
|
||||
_envCells.TryRemove(replacement.EnvCellIdsToRemove[index], out _);
|
||||
if (replacement.HasTerrain)
|
||||
{
|
||||
_terrain[replacement.LandblockPrefix] = (
|
||||
replacement.Terrain!,
|
||||
replacement.Origin);
|
||||
_terrain[replacement.LandblockPrefix] = replacement.Terrain!;
|
||||
}
|
||||
foreach ((uint id, EnvCell cell) in replacement.EnvCells)
|
||||
else
|
||||
{
|
||||
_terrain.TryRemove(replacement.LandblockPrefix, out _);
|
||||
}
|
||||
for (int index = 0; index < replacement.EnvCells.Count; index++)
|
||||
{
|
||||
(uint id, EnvCell cell) = replacement.EnvCells[index];
|
||||
_envCells[id] = cell;
|
||||
}
|
||||
|
||||
uint desiredCurrentCellId =
|
||||
(currentCellId & 0xFFFF0000u) == replacement.LandblockPrefix
|
||||
|
|
@ -188,13 +179,101 @@ public sealed class CellGraph
|
|||
: 0u;
|
||||
if (desiredCurrentCellId != 0u)
|
||||
CurrCell = GetVisible(desiredCurrentCellId);
|
||||
else if ((currentCellId & 0xFFFF0000u)
|
||||
== replacement.LandblockPrefix)
|
||||
CurrCell = null;
|
||||
}
|
||||
|
||||
internal sealed class LandblockReplacementBuilder : IDisposable
|
||||
{
|
||||
private readonly CellGraph _active;
|
||||
private readonly CellGraph _staging;
|
||||
private readonly uint _prefix;
|
||||
private readonly List<KeyValuePair<uint, EnvCell>> _envCells = new();
|
||||
private readonly HashSet<uint> _stagingIds = new();
|
||||
private readonly List<uint> _removeIds = new();
|
||||
private IEnumerator<KeyValuePair<uint, EnvCell>>? _enumerator;
|
||||
private int _phase;
|
||||
|
||||
internal LandblockReplacementBuilder(
|
||||
CellGraph active,
|
||||
CellGraph staging,
|
||||
uint landblockId)
|
||||
{
|
||||
_active = active;
|
||||
_staging = staging;
|
||||
_prefix = landblockId & 0xFFFF0000u;
|
||||
_enumerator = staging._envCells.GetEnumerator();
|
||||
}
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
if (_phase == 0)
|
||||
{
|
||||
if (_enumerator!.MoveNext())
|
||||
{
|
||||
KeyValuePair<uint, EnvCell> pair = _enumerator.Current;
|
||||
if ((pair.Key & 0xFFFF0000u) == _prefix
|
||||
&& (pair.Key & 0xFFFFu) >= 0x0100u)
|
||||
{
|
||||
_envCells.Add(pair);
|
||||
_stagingIds.Add(pair.Key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_enumerator.Dispose();
|
||||
_enumerator = _active._envCells.GetEnumerator();
|
||||
_phase = 1;
|
||||
return false;
|
||||
}
|
||||
if (_phase == 1)
|
||||
{
|
||||
if (_enumerator!.MoveNext())
|
||||
{
|
||||
uint id = _enumerator.Current.Key;
|
||||
if ((id & 0xFFFF0000u) == _prefix
|
||||
&& (id & 0xFFFFu) >= 0x0100u
|
||||
&& !_stagingIds.Contains(id))
|
||||
{
|
||||
_removeIds.Add(id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_enumerator.Dispose();
|
||||
_enumerator = null;
|
||||
bool hasTerrain = _staging._terrain.TryGetValue(
|
||||
_prefix,
|
||||
out var terrain);
|
||||
Prepared = new PreparedCellGraphLandblock(
|
||||
_prefix,
|
||||
_removeIds,
|
||||
_envCells,
|
||||
hasTerrain,
|
||||
terrain,
|
||||
_staging.CurrCell?.Id ?? 0u);
|
||||
_phase = 2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal PreparedCellGraphLandblock? Prepared { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_enumerator?.Dispose();
|
||||
_enumerator = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PreparedCellGraphLandblock(
|
||||
uint LandblockPrefix,
|
||||
KeyValuePair<uint, EnvCell>[] EnvCells,
|
||||
IReadOnlyList<uint> EnvCellIdsToRemove,
|
||||
IReadOnlyList<KeyValuePair<uint, EnvCell>> EnvCells,
|
||||
bool HasTerrain,
|
||||
TerrainSurface? Terrain,
|
||||
Vector3 Origin,
|
||||
CellGraphTerrain? Terrain,
|
||||
uint CurrentCellId);
|
||||
|
||||
internal sealed record CellGraphTerrain(
|
||||
TerrainSurface Terrain,
|
||||
Vector3 Origin);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,77 @@ internal interface IHeadlessCollisionNeighborhood
|
|||
bool IsReady(uint fullCellId);
|
||||
}
|
||||
|
||||
internal static class HeadlessCollisionGenerationTransaction
|
||||
{
|
||||
internal static RuntimeCollisionGenerationCommit Execute(
|
||||
RuntimePhysicsState physics,
|
||||
uint landblockId,
|
||||
Action<RuntimeCollisionAdmission>? afterAdmission,
|
||||
Action<RuntimeCollisionAdmission,
|
||||
PreparedLandblockCollisionGeneration> stage)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(physics);
|
||||
ArgumentNullException.ThrowIfNull(stage);
|
||||
|
||||
RuntimeCollisionAdmission admission =
|
||||
physics.BeginCollisionAdmission(landblockId);
|
||||
PreparedLandblockCollisionGeneration? prepared = null;
|
||||
bool committed = false;
|
||||
try
|
||||
{
|
||||
// This hook exists so the exact post-admission/pre-prepare failure
|
||||
// boundary remains covered. Production does not install one.
|
||||
afterAdmission?.Invoke(admission);
|
||||
prepared = physics.PrepareCollisionGeneration(admission);
|
||||
stage(admission, prepared);
|
||||
|
||||
RuntimeCollisionOwnerCaptureStep ownerCapture;
|
||||
do
|
||||
{
|
||||
ownerCapture = physics.AdvanceCollisionRetainedOwnerCapture(
|
||||
admission,
|
||||
prepared);
|
||||
}
|
||||
while (!ownerCapture.Completed);
|
||||
foreach (uint ownerId in prepared.RetainedOwnerIds)
|
||||
{
|
||||
physics.RefreshCollisionRetainedOwner(
|
||||
admission,
|
||||
prepared,
|
||||
ownerId);
|
||||
}
|
||||
RuntimeCollisionSealStep seal;
|
||||
do
|
||||
{
|
||||
seal = physics.AdvanceCollisionGenerationSeal(
|
||||
admission,
|
||||
prepared);
|
||||
}
|
||||
while (!seal.Completed && !seal.Restarted);
|
||||
if (!seal.Completed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Headless collision owner set changed during synchronous sealing.");
|
||||
}
|
||||
|
||||
RuntimeCollisionGenerationCommit result =
|
||||
physics.CommitCollisionGeneration(admission, prepared);
|
||||
if (!result.Committed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Headless collision generation changed during synchronous publication.");
|
||||
}
|
||||
committed = true;
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!committed)
|
||||
physics.CancelCollisionGeneration(admission, prepared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-session mutable collision publication over process-shared immutable
|
||||
/// DAT and pak inputs. Every session retains its own engine, data cache,
|
||||
|
|
@ -172,81 +243,58 @@ internal sealed class HeadlessCollisionNeighborhood
|
|||
_content.PreparedCollision,
|
||||
landblock);
|
||||
|
||||
RuntimePhysicsState physics =
|
||||
_runtime.EntityObjects.Physics;
|
||||
RuntimeCollisionAdmission admission =
|
||||
physics.BeginCollisionAdmission(landblockId);
|
||||
using PreparedLandblockCollisionGeneration prepared =
|
||||
physics.PrepareCollisionGeneration(admission);
|
||||
prepared.SetAssetClosure(
|
||||
[.. collisions.GfxObjIds],
|
||||
[.. collisions.SetupIds]);
|
||||
PhysicsDataCache cache = prepared.DataCache;
|
||||
TerrainSurface terrain =
|
||||
LandblockPhysicsContentBuilder.BuildTerrainSurface(
|
||||
landblock,
|
||||
_content.HeightTable.AsSpan());
|
||||
var cellSurfaces = new List<CellSurface>();
|
||||
var portalPlanes = new List<PortalPlane>();
|
||||
LandblockPhysicsContentBuilder.PublishPreparedCells(
|
||||
cache,
|
||||
landblock,
|
||||
collisions,
|
||||
origin,
|
||||
cellSurfaces,
|
||||
portalPlanes);
|
||||
LandblockPhysicsContentBuilder.CacheBuildings(
|
||||
cache,
|
||||
landblock,
|
||||
terrain,
|
||||
origin);
|
||||
LandblockPhysicsContentBuilder.CachePreparedObjects(
|
||||
cache,
|
||||
collisions);
|
||||
|
||||
try
|
||||
{
|
||||
physics.StageCollisionAssets(
|
||||
admission,
|
||||
prepared,
|
||||
new RuntimeLandblockCollisionAssets(
|
||||
landblockId,
|
||||
terrain,
|
||||
cellSurfaces,
|
||||
portalPlanes,
|
||||
origin.X,
|
||||
origin.Y,
|
||||
currentCellId));
|
||||
_ = LandblockPhysicsContentBuilder
|
||||
.PublishStaticCollision(
|
||||
prepared.Engine,
|
||||
RuntimePhysicsState physics = _runtime.EntityObjects.Physics;
|
||||
_ = HeadlessCollisionGenerationTransaction.Execute(
|
||||
physics,
|
||||
landblockId,
|
||||
afterAdmission: null,
|
||||
(admission, prepared) =>
|
||||
{
|
||||
prepared.SetAssetClosure(
|
||||
[.. collisions.GfxObjIds],
|
||||
[.. collisions.SetupIds]);
|
||||
PhysicsDataCache cache = prepared.DataCache;
|
||||
TerrainSurface terrain =
|
||||
LandblockPhysicsContentBuilder.BuildTerrainSurface(
|
||||
landblock,
|
||||
_content.HeightTable.AsSpan());
|
||||
var cellSurfaces = new List<CellSurface>();
|
||||
var portalPlanes = new List<PortalPlane>();
|
||||
LandblockPhysicsContentBuilder.PublishPreparedCells(
|
||||
cache,
|
||||
landblock,
|
||||
collisions,
|
||||
origin,
|
||||
cellSurfaces,
|
||||
portalPlanes);
|
||||
LandblockPhysicsContentBuilder.CacheBuildings(
|
||||
cache,
|
||||
landblock,
|
||||
terrain,
|
||||
origin);
|
||||
foreach (uint ownerId in physics.CaptureCollisionDynamicOwners(
|
||||
admission,
|
||||
prepared))
|
||||
{
|
||||
physics.RefreshCollisionDynamicOwner(
|
||||
LandblockPhysicsContentBuilder.CachePreparedObjects(
|
||||
cache,
|
||||
collisions);
|
||||
physics.StageCollisionAssets(
|
||||
admission,
|
||||
prepared,
|
||||
ownerId);
|
||||
}
|
||||
RuntimeCollisionGenerationCommit commit =
|
||||
physics.CommitCollisionGeneration(admission, prepared);
|
||||
if (!commit.Committed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Headless collision generation changed during synchronous publication.");
|
||||
}
|
||||
_resident.Add(CanonicalLandblock(landblockId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
_ = physics.WithdrawCollision(landblockId);
|
||||
throw;
|
||||
}
|
||||
new RuntimeLandblockCollisionAssets(
|
||||
landblockId,
|
||||
terrain,
|
||||
cellSurfaces,
|
||||
portalPlanes,
|
||||
origin.X,
|
||||
origin.Y,
|
||||
currentCellId));
|
||||
_ = LandblockPhysicsContentBuilder
|
||||
.PublishStaticCollision(
|
||||
prepared.Engine,
|
||||
cache,
|
||||
landblock,
|
||||
collisions,
|
||||
origin);
|
||||
});
|
||||
_resident.Add(CanonicalLandblock(landblockId));
|
||||
}
|
||||
|
||||
private void RetireAll()
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ public readonly record struct RuntimeCollisionAcknowledgement(
|
|||
|
||||
public readonly record struct RuntimeCollisionGenerationCommit(
|
||||
RuntimeCollisionAcknowledgement Acknowledgement,
|
||||
uint[] DirtyDynamicOwnerIds)
|
||||
uint[] DirtyRetainedOwnerIds)
|
||||
{
|
||||
public bool Committed => Acknowledgement.Ready;
|
||||
}
|
||||
|
|
@ -87,7 +87,11 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
|
|||
{
|
||||
private readonly RuntimePhysicsState _owner;
|
||||
private readonly RuntimeCollisionAdmission _admission;
|
||||
private readonly Dictionary<uint, ulong> _dynamicOwnerVersions = new();
|
||||
private readonly Dictionary<uint, ulong> _retainedOwnerVersions = new();
|
||||
private readonly List<uint> _retainedOwnerIds = new();
|
||||
private ShadowObjectRegistry.RetainedRefloodOwnerScan? _retainedOwnerScan;
|
||||
private PhysicsEngine.LandblockReplacementBuilder? _sealBuilder;
|
||||
private PhysicsEngine.PreparedPhysicsEngineLandblock? _sealedReplacement;
|
||||
private bool _disposed;
|
||||
|
||||
internal PreparedLandblockCollisionGeneration(
|
||||
|
|
@ -106,9 +110,13 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
|
|||
internal PhysicsEngine Engine { get; }
|
||||
internal uint[] GfxObjectIds { get; private set; } = Array.Empty<uint>();
|
||||
internal uint[] SetupIds { get; private set; } = Array.Empty<uint>();
|
||||
internal IReadOnlyDictionary<uint, ulong> DynamicOwnerVersions =>
|
||||
_dynamicOwnerVersions;
|
||||
internal IReadOnlyDictionary<uint, ulong> RetainedOwnerVersions =>
|
||||
_retainedOwnerVersions;
|
||||
internal bool IsDisposed => _disposed;
|
||||
internal bool RetainedOwnerCaptureComplete { get; private set; }
|
||||
internal ulong RetainedOwnerCaptureMutationVersion { get; private set; }
|
||||
internal bool IsSealed => _sealedReplacement is not null;
|
||||
internal ulong SealedShadowMutationVersion { get; private set; }
|
||||
|
||||
internal bool Matches(
|
||||
RuntimePhysicsState owner,
|
||||
|
|
@ -123,26 +131,155 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
|
|||
SetupIds = setupIds ?? throw new ArgumentNullException(nameof(setupIds));
|
||||
}
|
||||
|
||||
internal void RefreshDynamicOwner(uint ownerId)
|
||||
internal void RefreshRetainedOwner(uint ownerId)
|
||||
{
|
||||
EnsureUsable();
|
||||
bool retained = Engine.ShadowObjects.RefreshDynamicOwnerFrom(
|
||||
bool retained = Engine.ShadowObjects.RefreshRetainedOwnerFrom(
|
||||
_owner.Engine.ShadowObjects,
|
||||
ownerId,
|
||||
_admission.LandblockId,
|
||||
out ulong version);
|
||||
if (retained)
|
||||
_dynamicOwnerVersions[ownerId] = version;
|
||||
_retainedOwnerVersions[ownerId] = version;
|
||||
else
|
||||
_dynamicOwnerVersions.Remove(ownerId);
|
||||
_retainedOwnerVersions.Remove(ownerId);
|
||||
}
|
||||
|
||||
internal uint[] FindDirtyDynamicOwners()
|
||||
internal RuntimeCollisionOwnerCaptureStep AdvanceRetainedOwnerCapture()
|
||||
{
|
||||
EnsureUsable();
|
||||
return _owner.Engine.ShadowObjects.FindDirtyDynamicOwners(
|
||||
if (RetainedOwnerCaptureComplete)
|
||||
{
|
||||
return new RuntimeCollisionOwnerCaptureStep(
|
||||
Completed: true,
|
||||
Restarted: false,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u);
|
||||
}
|
||||
_retainedOwnerScan ??= _owner.Engine.ShadowObjects
|
||||
.CreateRetainedRefloodOwnerScan(_admission.LandblockId);
|
||||
ShadowObjectRegistry.RetainedRefloodOwnerScanStep step =
|
||||
_retainedOwnerScan.Advance();
|
||||
if (step.Completed && !step.Stable)
|
||||
{
|
||||
ResetRetainedOwnerCapture();
|
||||
return new RuntimeCollisionOwnerCaptureStep(
|
||||
Completed: false,
|
||||
Restarted: true,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u);
|
||||
}
|
||||
if (step.HasOwner)
|
||||
_retainedOwnerIds.Add(step.OwnerId);
|
||||
if (step.Completed)
|
||||
{
|
||||
_retainedOwnerScan.Dispose();
|
||||
_retainedOwnerScan = null;
|
||||
RetainedOwnerCaptureMutationVersion = step.SourceMutationVersion;
|
||||
RetainedOwnerCaptureComplete = true;
|
||||
}
|
||||
return new RuntimeCollisionOwnerCaptureStep(
|
||||
RetainedOwnerCaptureComplete,
|
||||
Restarted: false,
|
||||
step.HasOwner,
|
||||
step.OwnerId);
|
||||
}
|
||||
|
||||
internal IReadOnlyList<uint> RetainedOwnerIds
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureUsable();
|
||||
if (!RetainedOwnerCaptureComplete)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Retained collision-owner capture is incomplete.");
|
||||
}
|
||||
return _retainedOwnerIds;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ResetRetainedOwnerCapture()
|
||||
{
|
||||
EnsureUsable();
|
||||
_retainedOwnerScan?.Dispose();
|
||||
_retainedOwnerScan = null;
|
||||
_retainedOwnerIds.Clear();
|
||||
_retainedOwnerVersions.Clear();
|
||||
RetainedOwnerCaptureComplete = false;
|
||||
RetainedOwnerCaptureMutationVersion = 0UL;
|
||||
_sealedReplacement = null;
|
||||
_sealBuilder?.Dispose();
|
||||
_sealBuilder = null;
|
||||
SealedShadowMutationVersion = 0UL;
|
||||
}
|
||||
|
||||
internal RuntimeCollisionSealStep AdvanceSeal()
|
||||
{
|
||||
EnsureUsable();
|
||||
if (!RetainedOwnerCaptureComplete)
|
||||
{
|
||||
return new RuntimeCollisionSealStep(
|
||||
Completed: false,
|
||||
Restarted: true,
|
||||
WorkUnits: 0);
|
||||
}
|
||||
if (_owner.Engine.ShadowObjects.MutationVersion
|
||||
!= RetainedOwnerCaptureMutationVersion)
|
||||
{
|
||||
ResetRetainedOwnerCapture();
|
||||
return new RuntimeCollisionSealStep(
|
||||
Completed: false,
|
||||
Restarted: true,
|
||||
WorkUnits: 0);
|
||||
}
|
||||
if (_retainedOwnerVersions.Count != _retainedOwnerIds.Count)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Every retained collision owner must refresh before sealing.");
|
||||
}
|
||||
|
||||
_sealBuilder ??= _owner.Engine.CreateLandblockReplacementBuilder(
|
||||
Engine,
|
||||
_admission.LandblockId,
|
||||
_dynamicOwnerVersions);
|
||||
GfxObjectIds,
|
||||
SetupIds,
|
||||
_retainedOwnerVersions);
|
||||
int before = _sealBuilder.WorkUnits;
|
||||
bool completed = _sealBuilder.Advance();
|
||||
int workUnits = _sealBuilder.WorkUnits - before;
|
||||
if (!completed)
|
||||
{
|
||||
return new RuntimeCollisionSealStep(
|
||||
Completed: false,
|
||||
Restarted: false,
|
||||
workUnits);
|
||||
}
|
||||
if (!_sealBuilder.IsStable || _sealBuilder.Prepared is null)
|
||||
{
|
||||
ResetRetainedOwnerCapture();
|
||||
return new RuntimeCollisionSealStep(
|
||||
Completed: false,
|
||||
Restarted: true,
|
||||
workUnits);
|
||||
}
|
||||
_sealedReplacement = _sealBuilder.Prepared;
|
||||
_sealBuilder.Dispose();
|
||||
_sealBuilder = null;
|
||||
SealedShadowMutationVersion =
|
||||
_owner.Engine.ShadowObjects.MutationVersion;
|
||||
return new RuntimeCollisionSealStep(
|
||||
Completed: true,
|
||||
Restarted: false,
|
||||
workUnits);
|
||||
}
|
||||
|
||||
internal PhysicsEngine.PreparedPhysicsEngineLandblock TakeSealedReplacement()
|
||||
{
|
||||
EnsureUsable();
|
||||
return _sealedReplacement
|
||||
?? throw new InvalidOperationException(
|
||||
"Collision generation must be sealed before activation.");
|
||||
}
|
||||
|
||||
internal void MarkCommitted()
|
||||
|
|
@ -156,7 +293,14 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
|
|||
if (_disposed)
|
||||
return;
|
||||
Engine.Clear();
|
||||
_dynamicOwnerVersions.Clear();
|
||||
_retainedOwnerScan?.Dispose();
|
||||
_retainedOwnerScan = null;
|
||||
_retainedOwnerIds.Clear();
|
||||
_retainedOwnerVersions.Clear();
|
||||
_sealBuilder?.Dispose();
|
||||
_sealBuilder = null;
|
||||
_sealedReplacement = null;
|
||||
SealedShadowMutationVersion = 0UL;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +311,17 @@ internal sealed class PreparedLandblockCollisionGeneration : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
internal readonly record struct RuntimeCollisionOwnerCaptureStep(
|
||||
bool Completed,
|
||||
bool Restarted,
|
||||
bool HasOwner,
|
||||
uint OwnerId);
|
||||
|
||||
internal readonly record struct RuntimeCollisionSealStep(
|
||||
bool Completed,
|
||||
bool Restarted,
|
||||
int WorkUnits);
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-free mutable physics world for one Runtime/session owner.
|
||||
/// Immutable prepared collision inputs may be supplied by a graphical or
|
||||
|
|
@ -1001,6 +1156,43 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
stagingEngine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels only the named unpublished generation. The currently active
|
||||
/// collision world is never withdrawn. A stale receipt may dispose its
|
||||
/// own staging storage but cannot invalidate a newer admission.
|
||||
/// </summary>
|
||||
internal void CancelCollisionGeneration(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration? prepared = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
EnsureCollisionMutationThread();
|
||||
ArgumentNullException.ThrowIfNull(admission);
|
||||
if (!ReferenceEquals(admission.Owner, this))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Collision admission belongs to another Runtime.",
|
||||
nameof(admission));
|
||||
}
|
||||
if (prepared is not null && !prepared.Matches(this, admission))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Prepared collision generation belongs to another admission.",
|
||||
nameof(prepared));
|
||||
}
|
||||
|
||||
prepared?.Dispose();
|
||||
if (_collisionAdmissions.TryGetValue(
|
||||
admission.LandblockId,
|
||||
out RuntimeCollisionAdmission? current)
|
||||
&& ReferenceEquals(current, admission))
|
||||
{
|
||||
_collisionAdmissions.Remove(admission.LandblockId);
|
||||
_collisionGenerations[admission.LandblockId] = checked(
|
||||
admission.Generation + 1UL);
|
||||
}
|
||||
}
|
||||
|
||||
internal void StageCollisionAssets(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared,
|
||||
|
|
@ -1043,18 +1235,17 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
admission.AssetsPrepared = true;
|
||||
}
|
||||
|
||||
internal uint[] CaptureCollisionDynamicOwners(
|
||||
internal RuntimeCollisionOwnerCaptureStep AdvanceCollisionRetainedOwnerCapture(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
return Engine.ShadowObjects.CaptureDynamicRefloodOwnersForLandblock(
|
||||
admission.LandblockId);
|
||||
return prepared.AdvanceRetainedOwnerCapture();
|
||||
}
|
||||
|
||||
internal void RefreshCollisionDynamicOwner(
|
||||
internal void RefreshCollisionRetainedOwner(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared,
|
||||
uint ownerId)
|
||||
|
|
@ -1062,7 +1253,32 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
prepared.RefreshDynamicOwner(ownerId);
|
||||
prepared.RefreshRetainedOwner(ownerId);
|
||||
}
|
||||
|
||||
internal void RestartCollisionRetainedOwnerCapture(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
prepared.ResetRetainedOwnerCapture();
|
||||
}
|
||||
|
||||
internal RuntimeCollisionSealStep AdvanceCollisionGenerationSeal(
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
EnsureCollisionMutationThread();
|
||||
ValidatePreparedGeneration(admission, prepared);
|
||||
if (!admission.AssetsPrepared)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision generation cannot seal before its assets are prepared.");
|
||||
}
|
||||
return prepared.AdvanceSeal();
|
||||
}
|
||||
|
||||
internal RuntimeCollisionGenerationCommit CommitCollisionGeneration(
|
||||
|
|
@ -1083,38 +1299,25 @@ public sealed class RuntimePhysicsState : IDisposable
|
|||
"Collision generation has already completed.");
|
||||
}
|
||||
|
||||
uint[] dirtyOwners = prepared.FindDirtyDynamicOwners();
|
||||
if (dirtyOwners.Length != 0)
|
||||
if (!prepared.IsSealed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision generation cannot activate before sealing.");
|
||||
}
|
||||
if (Engine.ShadowObjects.MutationVersion
|
||||
!= prepared.SealedShadowMutationVersion)
|
||||
{
|
||||
prepared.ResetRetainedOwnerCapture();
|
||||
return new RuntimeCollisionGenerationCommit(
|
||||
new RuntimeCollisionAcknowledgement(
|
||||
admission.LandblockId,
|
||||
admission.Generation,
|
||||
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||
Ready: false),
|
||||
dirtyOwners);
|
||||
Array.Empty<uint>());
|
||||
}
|
||||
|
||||
PhysicsEngine.PreparedPhysicsEngineLandblock replacement =
|
||||
Engine.PrepareLandblockReplacement(
|
||||
prepared.Engine,
|
||||
admission.LandblockId,
|
||||
prepared.GfxObjectIds,
|
||||
prepared.SetupIds,
|
||||
prepared.DynamicOwnerVersions);
|
||||
if (!Engine.ValidateLandblockReplacement(replacement))
|
||||
{
|
||||
dirtyOwners = prepared.FindDirtyDynamicOwners();
|
||||
return new RuntimeCollisionGenerationCommit(
|
||||
new RuntimeCollisionAcknowledgement(
|
||||
admission.LandblockId,
|
||||
admission.Generation,
|
||||
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||
Ready: false),
|
||||
dirtyOwners);
|
||||
}
|
||||
|
||||
Engine.CommitLandblockReplacement(replacement);
|
||||
Engine.CommitLandblockReplacement(prepared.TakeSealedReplacement());
|
||||
admission.Completed = true;
|
||||
_collisionAdmissions.Remove(admission.LandblockId);
|
||||
prepared.MarkCommitted();
|
||||
|
|
|
|||
|
|
@ -976,6 +976,43 @@ public sealed class LandblockPhysicsPublisherTests
|
|||
new Dictionary<uint, GfxObj>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReloadedSeamAtomicallyRestoresAdjacentStaticAndClearsRepairMarker()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
WorldEntity neighbor = CylinderEntity(
|
||||
0x80AAB401u,
|
||||
new Vector3(192.25f, 12f, 0f));
|
||||
Publish(fixture.Publisher, Build(FirstLandblock));
|
||||
Publish(fixture.Publisher, Build(AdjacentLandblock, [neighbor]));
|
||||
Assert.True(fixture.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
||||
neighbor.Id,
|
||||
FirstLandblock));
|
||||
|
||||
fixture.Publisher.RemoveLandblock(FirstLandblock);
|
||||
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.WithdrawnPrefixMarkerCount);
|
||||
Assert.False(fixture.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
||||
neighbor.Id,
|
||||
FirstLandblock));
|
||||
LandblockPhysicsPublication pending = Begin(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock));
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.WithdrawnPrefixMarkerCount);
|
||||
|
||||
fixture.Publisher.CompletePublication(pending);
|
||||
|
||||
Assert.Equal(0, fixture.Engine.ShadowObjects.WithdrawnPrefixMarkerCount);
|
||||
Assert.True(fixture.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
||||
neighbor.Id,
|
||||
FirstLandblock));
|
||||
Assert.True(fixture.Engine.ShadowObjects.HasOwnerRowsInLandblock(
|
||||
neighbor.Id,
|
||||
AdjacentLandblock));
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
}
|
||||
|
||||
private static LandblockCollisionBuild FlatCellClosure(
|
||||
PhysicsDatBundle bundle,
|
||||
uint envCellId)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using AcDream.Headless.Platform;
|
|||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
|
|
@ -373,6 +374,72 @@ public sealed class HeadlessSessionHostTests
|
|||
Assert.Equal(2, collision.CenterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollisionTransactionCancelsPostAdmissionFaultWithoutWithdrawingActiveWorld()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimePhysicsState physics = lifetime.Physics;
|
||||
const uint landblockId = 0xA9B4FFFFu;
|
||||
_ = HeadlessCollisionGenerationTransaction.Execute(
|
||||
physics,
|
||||
landblockId,
|
||||
afterAdmission: null,
|
||||
(admission, prepared) =>
|
||||
physics.StageCollisionAssets(
|
||||
admission,
|
||||
prepared,
|
||||
CollisionAssets(landblockId, 10f)));
|
||||
|
||||
Assert.Throws<FixtureCollisionPublicationException>(() =>
|
||||
HeadlessCollisionGenerationTransaction.Execute(
|
||||
physics,
|
||||
landblockId,
|
||||
_ => throw new FixtureCollisionPublicationException(),
|
||||
(_, _) => throw new InvalidOperationException(
|
||||
"Staging must not run after the injected admission fault.")));
|
||||
|
||||
Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
|
||||
Assert.Equal(1, ownership.LandblockCount);
|
||||
Assert.Equal(0, ownership.CollisionAdmissionCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimePhysicsState physics = lifetime.Physics;
|
||||
const uint landblockId = 0xA9B4FFFFu;
|
||||
_ = HeadlessCollisionGenerationTransaction.Execute(
|
||||
physics,
|
||||
landblockId,
|
||||
afterAdmission: null,
|
||||
(admission, prepared) =>
|
||||
physics.StageCollisionAssets(
|
||||
admission,
|
||||
prepared,
|
||||
CollisionAssets(landblockId, 10f)));
|
||||
|
||||
Assert.Throws<FixtureCollisionPublicationException>(() =>
|
||||
HeadlessCollisionGenerationTransaction.Execute(
|
||||
physics,
|
||||
landblockId,
|
||||
afterAdmission: null,
|
||||
(admission, prepared) =>
|
||||
{
|
||||
physics.StageCollisionAssets(
|
||||
admission,
|
||||
prepared,
|
||||
CollisionAssets(landblockId, 25f));
|
||||
throw new FixtureCollisionPublicationException();
|
||||
}));
|
||||
|
||||
Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
|
||||
Assert.Equal(1, ownership.LandblockCount);
|
||||
Assert.Equal(0, ownership.CollisionAdmissionCount);
|
||||
}
|
||||
|
||||
private static HeadlessSessionDescriptor Descriptor(
|
||||
HeadlessCredentialProviderKind provider =
|
||||
HeadlessCredentialProviderKind.Environment,
|
||||
|
|
@ -422,6 +489,25 @@ public sealed class HeadlessSessionHostTests
|
|||
runtime.MovementOwner.Controller = controller;
|
||||
}
|
||||
|
||||
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
||||
uint landblockId,
|
||||
float terrainHeight)
|
||||
{
|
||||
var heights = new byte[81];
|
||||
var table = new float[256];
|
||||
table[0] = terrainHeight;
|
||||
return new RuntimeLandblockCollisionAssets(
|
||||
landblockId,
|
||||
new TerrainSurface(heights, table),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f,
|
||||
0u);
|
||||
}
|
||||
|
||||
private sealed class FixtureCollisionPublicationException : Exception;
|
||||
|
||||
private static void AddFlatLandblock(PhysicsEngine engine)
|
||||
{
|
||||
var heights = new byte[81];
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ public sealed class RuntimePhysicsStateTests
|
|||
prepared,
|
||||
CollisionAssets(0xA9B4FFFFu));
|
||||
RuntimeCollisionGenerationCommit commit =
|
||||
first.Physics.CommitCollisionGeneration(newer, prepared);
|
||||
CommitPrepared(first.Physics, newer, prepared);
|
||||
RuntimeCollisionAcknowledgement completed = commit.Acknowledgement;
|
||||
|
||||
Assert.True(commit.Committed);
|
||||
|
|
@ -332,7 +332,7 @@ public sealed class RuntimePhysicsStateTests
|
|||
0,
|
||||
first.Physics.CaptureOwnership().CollisionAdmissionCount);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
first.Physics.CommitCollisionGeneration(newer, prepared));
|
||||
CommitPrepared(first.Physics, newer, prepared));
|
||||
|
||||
RuntimeCollisionAcknowledgement withdrawn =
|
||||
first.Physics.WithdrawCollision(0xA9B4FFFFu);
|
||||
|
|
@ -355,7 +355,7 @@ public sealed class RuntimePhysicsStateTests
|
|||
firstAdmission,
|
||||
first,
|
||||
CollisionAssets(0xA9B4FFFFu, terrainHeight: 10f));
|
||||
Assert.True(physics.CommitCollisionGeneration(
|
||||
Assert.True(CommitPrepared(physics,
|
||||
firstAdmission,
|
||||
first).Committed);
|
||||
}
|
||||
|
|
@ -375,7 +375,7 @@ public sealed class RuntimePhysicsStateTests
|
|||
Assert.Equal(0, notifications);
|
||||
|
||||
RuntimeCollisionGenerationCommit committed =
|
||||
physics.CommitCollisionGeneration(
|
||||
CommitPrepared(physics,
|
||||
replacementAdmission,
|
||||
replacement);
|
||||
|
||||
|
|
@ -401,7 +401,11 @@ public sealed class RuntimePhysicsStateTests
|
|||
RuntimeCollisionAdmission current =
|
||||
physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
physics.CommitCollisionGeneration(stale, preparedStale));
|
||||
CommitPrepared(physics, stale, preparedStale));
|
||||
physics.CancelCollisionGeneration(stale, preparedStale);
|
||||
Assert.Equal(
|
||||
1,
|
||||
physics.CaptureOwnership().CollisionAdmissionCount);
|
||||
Assert.False(physics.Engine.IsLandblockTerrainResident(0xA9B4FFFFu));
|
||||
|
||||
using PreparedLandblockCollisionGeneration preparedCurrent =
|
||||
|
|
@ -410,14 +414,14 @@ public sealed class RuntimePhysicsStateTests
|
|||
current,
|
||||
preparedCurrent,
|
||||
CollisionAssets(0xA9B4FFFFu, terrainHeight: 20f));
|
||||
Assert.True(physics.CommitCollisionGeneration(
|
||||
Assert.True(CommitPrepared(physics,
|
||||
current,
|
||||
preparedCurrent).Committed);
|
||||
Assert.Equal(20f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovingDynamicOwnerDuringStagingMustRefreshBeforeCommit()
|
||||
public void WithdrawnOwnerStateChangeRejectsSealUntilRefreshed()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimePhysicsState physics = lifetime.Physics;
|
||||
|
|
@ -430,7 +434,7 @@ public sealed class RuntimePhysicsStateTests
|
|||
initialAdmission,
|
||||
initial,
|
||||
CollisionAssets(0x0101FFFFu, terrainHeight: 5f));
|
||||
Assert.True(physics.CommitCollisionGeneration(
|
||||
Assert.True(CommitPrepared(physics,
|
||||
initialAdmission,
|
||||
initial).Committed);
|
||||
}
|
||||
|
|
@ -445,6 +449,9 @@ public sealed class RuntimePhysicsStateTests
|
|||
0x0101FFFFu,
|
||||
seedCellId: 0x01010001u,
|
||||
isStatic: false);
|
||||
physics.Engine.ShadowObjects.RemoveLandblock(0x0101FFFFu);
|
||||
Assert.Equal(0, physics.Engine.ShadowObjects.TotalRegistered);
|
||||
Assert.Equal(1, physics.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
|
||||
RuntimeCollisionAdmission admission =
|
||||
physics.BeginCollisionAdmission(0x0101FFFFu);
|
||||
|
|
@ -454,33 +461,125 @@ public sealed class RuntimePhysicsStateTests
|
|||
admission,
|
||||
prepared,
|
||||
CollisionAssets(0x0101FFFFu, terrainHeight: 15f));
|
||||
uint owner = Assert.Single(physics.CaptureCollisionDynamicOwners(
|
||||
admission,
|
||||
prepared));
|
||||
physics.RefreshCollisionDynamicOwner(admission, prepared, owner);
|
||||
physics.Engine.ShadowObjects.UpdatePosition(
|
||||
owner,
|
||||
new Vector3(30f, 10f, 0f),
|
||||
Quaternion.Identity,
|
||||
0f,
|
||||
0f,
|
||||
0x0101FFFFu,
|
||||
seedCellId: 0x01010009u);
|
||||
uint owner = Assert.Single(SealPrepared(physics, admission, prepared));
|
||||
physics.Engine.ShadowObjects.UpdatePhysicsState(owner, 0x14u);
|
||||
|
||||
RuntimeCollisionGenerationCommit rejected =
|
||||
physics.CommitCollisionGeneration(admission, prepared);
|
||||
Assert.False(rejected.Committed);
|
||||
Assert.Equal(owner, Assert.Single(rejected.DirtyDynamicOwnerIds));
|
||||
Assert.Equal(5f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||
|
||||
physics.RefreshCollisionDynamicOwner(admission, prepared, owner);
|
||||
Assert.Equal(owner, Assert.Single(SealPrepared(
|
||||
physics,
|
||||
admission,
|
||||
prepared)));
|
||||
Assert.True(physics.CommitCollisionGeneration(
|
||||
admission,
|
||||
prepared).Committed);
|
||||
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||
Assert.Contains(
|
||||
physics.Engine.ShadowObjects.GetObjectsInCell(0x01010009u),
|
||||
entry => entry.EntityId == owner);
|
||||
ShadowEntry restored = Assert.Single(
|
||||
physics.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Equal(owner, restored.EntityId);
|
||||
Assert.Equal(0x14u, restored.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DenseReplacementSealsOneWorkUnitPerStepAndActivatesWithoutAllocation()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimePhysicsState physics = lifetime.Physics;
|
||||
const uint landblockId = 0x0101FFFFu;
|
||||
RuntimeCollisionAdmission initialAdmission =
|
||||
physics.BeginCollisionAdmission(landblockId);
|
||||
using (PreparedLandblockCollisionGeneration initial =
|
||||
physics.PrepareCollisionGeneration(initialAdmission))
|
||||
{
|
||||
physics.StageCollisionAssets(
|
||||
initialAdmission,
|
||||
initial,
|
||||
CollisionAssets(landblockId, terrainHeight: 5f));
|
||||
Assert.True(CommitPrepared(
|
||||
physics,
|
||||
initialAdmission,
|
||||
initial).Committed);
|
||||
}
|
||||
|
||||
const int ownerCount = 256;
|
||||
for (uint index = 0; index < ownerCount; index++)
|
||||
{
|
||||
physics.Engine.ShadowObjects.Register(
|
||||
entityId: 1000u + index,
|
||||
gfxObjId: 0x01000001u,
|
||||
worldPos: new Vector3(
|
||||
8f + (index % 16u) * 0.25f,
|
||||
8f + (index / 16u) * 0.25f,
|
||||
0f),
|
||||
rotation: Quaternion.Identity,
|
||||
radius: 0.5f,
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f,
|
||||
landblockId,
|
||||
seedCellId: 0x01010001u,
|
||||
isStatic: false);
|
||||
}
|
||||
|
||||
RuntimeCollisionAdmission admission =
|
||||
physics.BeginCollisionAdmission(landblockId);
|
||||
using PreparedLandblockCollisionGeneration prepared =
|
||||
physics.PrepareCollisionGeneration(admission);
|
||||
physics.StageCollisionAssets(
|
||||
admission,
|
||||
prepared,
|
||||
CollisionAssets(landblockId, terrainHeight: 15f));
|
||||
|
||||
int captureSteps = 0;
|
||||
RuntimeCollisionOwnerCaptureStep capture;
|
||||
do
|
||||
{
|
||||
capture = physics.AdvanceCollisionRetainedOwnerCapture(
|
||||
admission,
|
||||
prepared);
|
||||
captureSteps++;
|
||||
}
|
||||
while (!capture.Completed);
|
||||
Assert.True(captureSteps >= ownerCount);
|
||||
Assert.Equal(ownerCount, prepared.RetainedOwnerIds.Count);
|
||||
foreach (uint ownerId in prepared.RetainedOwnerIds)
|
||||
{
|
||||
physics.RefreshCollisionRetainedOwner(
|
||||
admission,
|
||||
prepared,
|
||||
ownerId);
|
||||
}
|
||||
|
||||
int sealSteps = 0;
|
||||
int workUnits = 0;
|
||||
RuntimeCollisionSealStep seal;
|
||||
do
|
||||
{
|
||||
seal = physics.AdvanceCollisionGenerationSeal(
|
||||
admission,
|
||||
prepared);
|
||||
sealSteps++;
|
||||
Assert.InRange(seal.WorkUnits, 0, 1);
|
||||
workUnits += seal.WorkUnits;
|
||||
}
|
||||
while (!seal.Completed);
|
||||
Assert.False(seal.Restarted);
|
||||
Assert.True(sealSteps > ownerCount);
|
||||
Assert.True(workUnits > ownerCount);
|
||||
|
||||
_ = GC.GetAllocatedBytesForCurrentThread();
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
RuntimeCollisionGenerationCommit commit =
|
||||
physics.CommitCollisionGeneration(admission, prepared);
|
||||
long allocated =
|
||||
GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.True(commit.Committed);
|
||||
Assert.Equal(0L, allocated);
|
||||
Assert.Equal(15f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||
Assert.Equal(ownerCount, physics.Engine.ShadowObjects.TotalRegistered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -496,7 +595,7 @@ public sealed class RuntimePhysicsStateTests
|
|||
admission,
|
||||
prepared,
|
||||
CollisionAssets(0x0101FFFFu, terrainHeight: 12f));
|
||||
Assert.Empty(physics.CaptureCollisionDynamicOwners(admission, prepared));
|
||||
Assert.Empty(SealPrepared(physics, admission, prepared));
|
||||
|
||||
physics.Engine.ShadowObjects.Register(
|
||||
77u,
|
||||
|
|
@ -512,16 +611,17 @@ public sealed class RuntimePhysicsStateTests
|
|||
RuntimeCollisionGenerationCommit spawned =
|
||||
physics.CommitCollisionGeneration(admission, prepared);
|
||||
Assert.False(spawned.Committed);
|
||||
Assert.Equal(77u, Assert.Single(spawned.DirtyDynamicOwnerIds));
|
||||
|
||||
physics.RefreshCollisionDynamicOwner(admission, prepared, 77u);
|
||||
Assert.Equal(77u, Assert.Single(SealPrepared(
|
||||
physics,
|
||||
admission,
|
||||
prepared)));
|
||||
physics.Engine.ShadowObjects.Deregister(77u);
|
||||
RuntimeCollisionGenerationCommit deleted =
|
||||
physics.CommitCollisionGeneration(admission, prepared);
|
||||
Assert.False(deleted.Committed);
|
||||
Assert.Equal(77u, Assert.Single(deleted.DirtyDynamicOwnerIds));
|
||||
|
||||
physics.RefreshCollisionDynamicOwner(admission, prepared, 77u);
|
||||
Assert.Empty(SealPrepared(physics, admission, prepared));
|
||||
Assert.True(physics.CommitCollisionGeneration(
|
||||
admission,
|
||||
prepared).Committed);
|
||||
|
|
@ -546,7 +646,7 @@ public sealed class RuntimePhysicsStateTests
|
|||
admission,
|
||||
prepared,
|
||||
CollisionAssets(0x0101FFFFu));
|
||||
_ = lifetime.Physics.CommitCollisionGeneration(admission, prepared);
|
||||
_ = CommitPrepared(lifetime.Physics, admission, prepared);
|
||||
lifetime.Physics.Engine.ShadowObjects.Register(
|
||||
entityId: record.LocalEntityId!.Value,
|
||||
gfxObjId: 0x01000001u,
|
||||
|
|
@ -930,6 +1030,49 @@ public sealed class RuntimePhysicsStateTests
|
|||
Assert.Equal(record.SpatialAuthorityVersion, observed.SpatialAuthorityVersion);
|
||||
}
|
||||
|
||||
private static RuntimeCollisionGenerationCommit CommitPrepared(
|
||||
RuntimePhysicsState physics,
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
_ = SealPrepared(physics, admission, prepared);
|
||||
return physics.CommitCollisionGeneration(admission, prepared);
|
||||
}
|
||||
|
||||
private static uint[] SealPrepared(
|
||||
RuntimePhysicsState physics,
|
||||
RuntimeCollisionAdmission admission,
|
||||
PreparedLandblockCollisionGeneration prepared)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
while (!physics.AdvanceCollisionRetainedOwnerCapture(
|
||||
admission,
|
||||
prepared).Completed)
|
||||
{
|
||||
}
|
||||
foreach (uint ownerId in prepared.RetainedOwnerIds)
|
||||
{
|
||||
physics.RefreshCollisionRetainedOwner(
|
||||
admission,
|
||||
prepared,
|
||||
ownerId);
|
||||
}
|
||||
RuntimeCollisionSealStep seal;
|
||||
do
|
||||
{
|
||||
seal = physics.AdvanceCollisionGenerationSeal(
|
||||
admission,
|
||||
prepared);
|
||||
Assert.InRange(seal.WorkUnits, 0, 1);
|
||||
}
|
||||
while (!seal.Completed && !seal.Restarted);
|
||||
if (seal.Completed)
|
||||
break;
|
||||
}
|
||||
return [.. prepared.RetainedOwnerIds];
|
||||
}
|
||||
|
||||
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
||||
uint landblockId,
|
||||
float terrainHeight = 0f)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue