fix(physics): activate collision generations atomically
This commit is contained in:
parent
3e0f3b6206
commit
be94bc9b06
18 changed files with 1402 additions and 80 deletions
|
|
@ -485,8 +485,21 @@ What exists and is active:
|
||||||
collision assets loaded from the validated prepared package. Production
|
collision assets loaded from the validated prepared package. Production
|
||||||
retains no parsed DAT collision graph; graph construction is restricted to
|
retains no parsed DAT collision graph; graph construction is restricted to
|
||||||
bake/equivalence tools and explicit test oracles.
|
bake/equivalence tools and explicit test oracles.
|
||||||
- `ShadowObjectRegistry` gives movement a broadphase over nearby objects and
|
- Landblock collision activation is generation-owned by
|
||||||
buildings.
|
`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.
|
||||||
|
- `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
|
||||||
|
reflood suffix.
|
||||||
- `TerrainSurface` uses triangle-aware terrain contact; older "bilinear terrain
|
- `TerrainSurface` uses triangle-aware terrain contact; older "bilinear terrain
|
||||||
Z" descriptions are historical B.3 language, not current architecture.
|
Z" descriptions are historical B.3 language, not current architecture.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-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-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-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 | Per-LANDBLOCK shadow re-flood on hydration vs retail per-CELL `recalc_cross_cells` | `src/AcDream.Core/Physics/ShadowObjectRegistry.cs:339` | The streaming unit IS the landblock; one hook per hydration event covers both race directions (entity-before-cells, cells-after-spawn) | Any cell-hydration path that doesn't raise the landblock hook leaves an entity's shadow set stale — walk-through / missing collisions in just-streamed cells | `CObjCell::init_objects` → `recalc_cross_cells`, 0x0052b420 / 0x00515a30 |
|
| ~~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-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-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-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 |
|
| 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 |
|
||||||
|
|
|
||||||
72
docs/research/2026-07-31-atomic-collision-generation.md
Normal file
72
docs/research/2026-07-31-atomic-collision-generation.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Atomic collision-generation activation (Slice 3B)
|
||||||
|
|
||||||
|
## Retail anchor
|
||||||
|
|
||||||
|
Retail hydrates a cell synchronously. `CObjCell::init_objects`
|
||||||
|
(`0x0052B420`) visits objects associated with that cell and invokes
|
||||||
|
`CPhysicsObj::recalc_cross_cells` (`0x00515A30`). The final position path also
|
||||||
|
replaces shadows as one `SetPositionInternal` operation (`0x00515330`). Retail
|
||||||
|
therefore never exposes a world where the new cell exists but the objects that
|
||||||
|
overlap it still have their old cross-cell set.
|
||||||
|
|
||||||
|
Acdream streams a landblock over several update frames. Literal per-cell
|
||||||
|
mutation during those frames was not equivalent: the active `PhysicsDataCache`,
|
||||||
|
`CellGraph`, `PhysicsEngine`, buildings, static shadows, and retained-object
|
||||||
|
refloods changed at different cursors. Collision queries could observe a mixed
|
||||||
|
generation, and correctness depended on a later optional landblock callback.
|
||||||
|
|
||||||
|
## Ported adaptation
|
||||||
|
|
||||||
|
The asynchronous unit is now one Runtime-owned collision generation:
|
||||||
|
|
||||||
|
1. `BeginCollisionAdmission` issues the exact Runtime/landblock generation.
|
||||||
|
2. `PrepareCollisionGeneration` clones the bounded resident spatial records
|
||||||
|
into a private cache, graph, engine, and shadow registry. Global immutable
|
||||||
|
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.
|
||||||
|
|
||||||
|
The stable borrowed `PhysicsEngine` and `PhysicsDataCache` object identities do
|
||||||
|
not change. Presentation and no-window hosts use the same Runtime transaction.
|
||||||
|
Network workers still enqueue immutable messages and cannot mutate collision or
|
||||||
|
shadow state.
|
||||||
|
|
||||||
|
## Failure and lifetime rules
|
||||||
|
|
||||||
|
- A newer admission invalidates an older prepared generation.
|
||||||
|
- Demotion, withdrawal, reset, and disposal invalidate the admission before
|
||||||
|
changing the active generation.
|
||||||
|
- Disposing a stale/cancelled prepared generation clears only its private
|
||||||
|
engine/cache/shadows.
|
||||||
|
- The prior complete generation remains queryable throughout preparation.
|
||||||
|
- The commit notification is the future lost-cell-registry seam. Slice 3B does
|
||||||
|
not implement `GotoLostCell` or change `SetPosition` recovery behavior.
|
||||||
|
|
||||||
|
## Deterministic evidence
|
||||||
|
|
||||||
|
The focused Runtime/App tests pin:
|
||||||
|
|
||||||
|
- previous terrain/cells/buildings/statics remain visible until commit;
|
||||||
|
- exactly one notification after a successful complete activation;
|
||||||
|
- 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;
|
||||||
|
- spawn and deletion during staging both reject stale activation;
|
||||||
|
- graphical and no-window publishers use the same Runtime transaction;
|
||||||
|
- removal and terminal teardown converge the active ownership ledger.
|
||||||
|
|
||||||
|
This retires divergence row AD-6. The remaining lost-cell state-machine work is
|
||||||
|
deliberately outside this slice.
|
||||||
|
|
@ -10,6 +10,14 @@ as "delete everything and start over." A partial retail transition port exists:
|
||||||
- `TransitionTypes` carries the active `SpherePath`, `CollisionInfo`,
|
- `TransitionTypes` carries the active `SpherePath`, `CollisionInfo`,
|
||||||
transition, step, contact, and partial slide logic.
|
transition, step, contact, and partial slide logic.
|
||||||
- `PhysicsDataCache` loads GfxObj, Setup, and CellStruct physics data.
|
- `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
|
||||||
|
`docs/research/2026-07-31-atomic-collision-generation.md`.
|
||||||
- `ShadowObjectRegistry` gives the resolver a broadphase over nearby objects.
|
- `ShadowObjectRegistry` gives the resolver a broadphase over nearby objects.
|
||||||
- `TerrainSurface` uses triangle-aware terrain contact.
|
- `TerrainSurface` uses triangle-aware terrain contact.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.Streaming;
|
||||||
/// publication. The render publisher commits buildings and EnvCells between
|
/// publication. The render publisher commits buildings and EnvCells between
|
||||||
/// these stages without recomputing the captured origin.
|
/// these stages without recomputing the captured origin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LandblockPhysicsPublication
|
public sealed class LandblockPhysicsPublication : IDisposable
|
||||||
{
|
{
|
||||||
internal LandblockPhysicsPublication(
|
internal LandblockPhysicsPublication(
|
||||||
object owner,
|
object owner,
|
||||||
|
|
@ -21,7 +21,8 @@ public sealed class LandblockPhysicsPublication
|
||||||
uint currentCellId,
|
uint currentCellId,
|
||||||
BuildingInfo[] buildings,
|
BuildingInfo[] buildings,
|
||||||
uint[] priorStaticOwnerIds,
|
uint[] priorStaticOwnerIds,
|
||||||
RuntimeCollisionAdmission collisionAdmission)
|
RuntimeCollisionAdmission collisionAdmission,
|
||||||
|
PreparedLandblockCollisionGeneration preparedGeneration)
|
||||||
{
|
{
|
||||||
Owner = owner;
|
Owner = owner;
|
||||||
Build = build;
|
Build = build;
|
||||||
|
|
@ -30,6 +31,7 @@ public sealed class LandblockPhysicsPublication
|
||||||
Buildings = buildings;
|
Buildings = buildings;
|
||||||
PriorStaticOwnerIds = priorStaticOwnerIds;
|
PriorStaticOwnerIds = priorStaticOwnerIds;
|
||||||
CollisionAdmission = collisionAdmission;
|
CollisionAdmission = collisionAdmission;
|
||||||
|
PreparedGeneration = preparedGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object Owner { get; }
|
internal object Owner { get; }
|
||||||
|
|
@ -38,6 +40,9 @@ public sealed class LandblockPhysicsPublication
|
||||||
internal BuildingInfo[] Buildings { get; }
|
internal BuildingInfo[] Buildings { get; }
|
||||||
internal uint[] PriorStaticOwnerIds { get; }
|
internal uint[] PriorStaticOwnerIds { get; }
|
||||||
internal RuntimeCollisionAdmission CollisionAdmission { get; }
|
internal RuntimeCollisionAdmission CollisionAdmission { get; }
|
||||||
|
internal PreparedLandblockCollisionGeneration PreparedGeneration { get; }
|
||||||
|
internal PhysicsDataCache StagingCache => PreparedGeneration.DataCache;
|
||||||
|
internal PhysicsEngine StagingEngine => PreparedGeneration.Engine;
|
||||||
internal SortedSet<uint> GfxObjectIdSet { get; } = new();
|
internal SortedSet<uint> GfxObjectIdSet { get; } = new();
|
||||||
internal uint[] GfxObjectIds { get; set; } = Array.Empty<uint>();
|
internal uint[] GfxObjectIds { get; set; } = Array.Empty<uint>();
|
||||||
internal int PreparationCursor { get; set; }
|
internal int PreparationCursor { get; set; }
|
||||||
|
|
@ -64,6 +69,12 @@ public sealed class LandblockPhysicsPublication
|
||||||
internal bool BeginCommitted { get; set; }
|
internal bool BeginCommitted { get; set; }
|
||||||
internal bool CompletionCommitted { get; set; }
|
internal bool CompletionCommitted { get; set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!CompletionCommitted)
|
||||||
|
PreparedGeneration.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public uint LandblockId => Build.Landblock.LandblockId;
|
public uint LandblockId => Build.Landblock.LandblockId;
|
||||||
public Vector3 Origin { get; }
|
public Vector3 Origin { get; }
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +215,8 @@ public sealed class LandblockPhysicsPublisher
|
||||||
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||||
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
||||||
?? Array.Empty<BuildingInfo>();
|
?? Array.Empty<BuildingInfo>();
|
||||||
|
RuntimeCollisionAdmission collisionAdmission =
|
||||||
|
_physics.BeginCollisionAdmission(build.Landblock.LandblockId);
|
||||||
var publication = new LandblockPhysicsPublication(
|
var publication = new LandblockPhysicsPublication(
|
||||||
_receiptOwner,
|
_receiptOwner,
|
||||||
build,
|
build,
|
||||||
|
|
@ -212,11 +225,14 @@ public sealed class LandblockPhysicsPublisher
|
||||||
buildings,
|
buildings,
|
||||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||||
build.Landblock.LandblockId),
|
build.Landblock.LandblockId),
|
||||||
_physics.BeginCollisionAdmission(
|
collisionAdmission,
|
||||||
build.Landblock.LandblockId));
|
_physics.PrepareCollisionGeneration(collisionAdmission));
|
||||||
publication.SetupObjectIds = build.Collisions is { } collisions
|
publication.SetupObjectIds = build.Collisions is { } collisions
|
||||||
? [.. collisions.SetupIds]
|
? [.. collisions.SetupIds]
|
||||||
: datBundle.Setups.Keys.Order().ToArray();
|
: datBundle.Setups.Keys.Order().ToArray();
|
||||||
|
publication.PreparedGeneration.SetAssetClosure(
|
||||||
|
publication.GfxObjectIds,
|
||||||
|
publication.SetupObjectIds);
|
||||||
return publication;
|
return publication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,6 +253,9 @@ public sealed class LandblockPhysicsPublisher
|
||||||
if (publication.Build.Collisions is { } collisions)
|
if (publication.Build.Collisions is { } collisions)
|
||||||
{
|
{
|
||||||
publication.GfxObjectIds = [.. collisions.GfxObjIds];
|
publication.GfxObjectIds = [.. collisions.GfxObjIds];
|
||||||
|
publication.PreparedGeneration.SetAssetClosure(
|
||||||
|
publication.GfxObjectIds,
|
||||||
|
publication.SetupObjectIds);
|
||||||
publication.PreparationCursor = entities.Count;
|
publication.PreparationCursor = entities.Count;
|
||||||
publication.PreparationCommitted = true;
|
publication.PreparationCommitted = true;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -256,6 +275,9 @@ public sealed class LandblockPhysicsPublisher
|
||||||
}
|
}
|
||||||
|
|
||||||
publication.GfxObjectIds = publication.GfxObjectIdSet.ToArray();
|
publication.GfxObjectIds = publication.GfxObjectIdSet.ToArray();
|
||||||
|
publication.PreparedGeneration.SetAssetClosure(
|
||||||
|
publication.GfxObjectIds,
|
||||||
|
publication.SetupObjectIds);
|
||||||
publication.PreparationCommitted = true;
|
publication.PreparationCommitted = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -299,9 +321,9 @@ public sealed class LandblockPhysicsPublisher
|
||||||
// CacheCellStruct/CacheBuilding use first-wins semantics within one
|
// CacheCellStruct/CacheBuilding use first-wins semantics within one
|
||||||
// publication, so the replacement pass starts with one exact
|
// publication, so the replacement pass starts with one exact
|
||||||
// landblock-scoped withdrawal.
|
// landblock-scoped withdrawal.
|
||||||
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
|
publication.StagingCache.RemoveCellsForLandblock(landblock.LandblockId);
|
||||||
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
|
publication.StagingCache.RemoveBuildingsForLandblock(landblock.LandblockId);
|
||||||
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
|
publication.StagingCache.CellGraph.RemoveEnvCellsForLandblock(
|
||||||
landblock.LandblockId);
|
landblock.LandblockId);
|
||||||
publication.PriorCacheRemoved = true;
|
publication.PriorCacheRemoved = true;
|
||||||
}
|
}
|
||||||
|
|
@ -330,6 +352,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
PublishBuilding(
|
PublishBuilding(
|
||||||
landblock,
|
landblock,
|
||||||
datBundle,
|
datBundle,
|
||||||
|
publication.StagingCache,
|
||||||
publication.TerrainSurface,
|
publication.TerrainSurface,
|
||||||
origin,
|
origin,
|
||||||
publication.Buildings[publication.BuildingCursor]);
|
publication.Buildings[publication.BuildingCursor]);
|
||||||
|
|
@ -337,8 +360,9 @@ public sealed class LandblockPhysicsPublisher
|
||||||
}
|
}
|
||||||
else if (!publication.BaseCommitted)
|
else if (!publication.BaseCommitted)
|
||||||
{
|
{
|
||||||
_physics.AdmitCollisionAssets(
|
_physics.StageCollisionAssets(
|
||||||
publication.CollisionAdmission,
|
publication.CollisionAdmission,
|
||||||
|
publication.PreparedGeneration,
|
||||||
new RuntimeLandblockCollisionAssets(
|
new RuntimeLandblockCollisionAssets(
|
||||||
landblock.LandblockId,
|
landblock.LandblockId,
|
||||||
publication.TerrainSurface,
|
publication.TerrainSurface,
|
||||||
|
|
@ -409,7 +433,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
gfxObjectId,
|
gfxObjectId,
|
||||||
out FlatGfxObjCollisionAsset? prepared) == true)
|
out FlatGfxObjCollisionAsset? prepared) == true)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheGfxObj(gfxObjectId, prepared);
|
publication.StagingCache.CacheGfxObj(gfxObjectId, prepared);
|
||||||
}
|
}
|
||||||
else if (datBundle.GfxObjs.TryGetValue(
|
else if (datBundle.GfxObjs.TryGetValue(
|
||||||
gfxObjectId,
|
gfxObjectId,
|
||||||
|
|
@ -417,7 +441,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
{
|
{
|
||||||
// Graph-oracle fixture seam. Production near builds always
|
// Graph-oracle fixture seam. Production near builds always
|
||||||
// carry the strict prepared closure.
|
// carry the strict prepared closure.
|
||||||
_physicsDataCache.CacheGfxObj(gfxObjectId, source);
|
publication.StagingCache.CacheGfxObj(gfxObjectId, source);
|
||||||
}
|
}
|
||||||
publication.GfxCursor++;
|
publication.GfxCursor++;
|
||||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||||
|
|
@ -430,19 +454,19 @@ public sealed class LandblockPhysicsPublisher
|
||||||
setupId,
|
setupId,
|
||||||
out FlatSetupCollision? prepared) == true)
|
out FlatSetupCollision? prepared) == true)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheSetup(setupId, prepared);
|
publication.StagingCache.CacheSetup(setupId, prepared);
|
||||||
}
|
}
|
||||||
else if (datBundle.Setups.TryGetValue(setupId, out var source))
|
else if (datBundle.Setups.TryGetValue(setupId, out var source))
|
||||||
{
|
{
|
||||||
// Graph-oracle fixture seam only.
|
// Graph-oracle fixture seam only.
|
||||||
_physicsDataCache.CacheSetup(setupId, source);
|
publication.StagingCache.CacheSetup(setupId, source);
|
||||||
}
|
}
|
||||||
publication.SetupCursor++;
|
publication.SetupCursor++;
|
||||||
}
|
}
|
||||||
else if (publication.PriorStaticCursor
|
else if (publication.PriorStaticCursor
|
||||||
< publication.PriorStaticOwnerIds.Length)
|
< publication.PriorStaticOwnerIds.Length)
|
||||||
{
|
{
|
||||||
_physicsEngine.ShadowObjects.DeregisterStaticOwnerForLandblock(
|
publication.StagingEngine.ShadowObjects.DeregisterStaticOwnerForLandblock(
|
||||||
publication.PriorStaticOwnerIds[
|
publication.PriorStaticOwnerIds[
|
||||||
publication.PriorStaticCursor],
|
publication.PriorStaticCursor],
|
||||||
landblock.LandblockId);
|
landblock.LandblockId);
|
||||||
|
|
@ -458,15 +482,17 @@ public sealed class LandblockPhysicsPublisher
|
||||||
else if (publication.RefloodOwnerIds is null)
|
else if (publication.RefloodOwnerIds is null)
|
||||||
{
|
{
|
||||||
publication.RefloodOwnerIds =
|
publication.RefloodOwnerIds =
|
||||||
_physicsEngine.ShadowObjects
|
_physics.CaptureCollisionDynamicOwners(
|
||||||
.CaptureRefloodOwnersForLandblock(landblock.LandblockId);
|
publication.CollisionAdmission,
|
||||||
|
publication.PreparedGeneration);
|
||||||
}
|
}
|
||||||
else if (publication.RefloodCursor
|
else if (publication.RefloodCursor
|
||||||
< publication.RefloodOwnerIds.Length)
|
< publication.RefloodOwnerIds.Length)
|
||||||
{
|
{
|
||||||
_physicsEngine.ShadowObjects.RefloodOwnerForLandblock(
|
_physics.RefreshCollisionDynamicOwner(
|
||||||
publication.RefloodOwnerIds[publication.RefloodCursor],
|
publication.CollisionAdmission,
|
||||||
landblock.LandblockId);
|
publication.PreparedGeneration,
|
||||||
|
publication.RefloodOwnerIds[publication.RefloodCursor]);
|
||||||
publication.RefloodCursor++;
|
publication.RefloodCursor++;
|
||||||
}
|
}
|
||||||
else if (!publication.RefloodCommitted)
|
else if (!publication.RefloodCommitted)
|
||||||
|
|
@ -478,14 +504,24 @@ public sealed class LandblockPhysicsPublisher
|
||||||
$"lb 0x{landblock.LandblockId:X8}: scenery tried={publication.SceneryTried} " +
|
$"lb 0x{landblock.LandblockId:X8}: scenery tried={publication.SceneryTried} " +
|
||||||
$"(outdoorNone={publication.NoCollisionCount})");
|
$"(outdoorNone={publication.NoCollisionCount})");
|
||||||
}
|
}
|
||||||
LogMissingSceneryBounds(landblock);
|
LogMissingSceneryBounds(landblock, publication.StagingCache);
|
||||||
_refloodCount++;
|
|
||||||
publication.RefloodCommitted = true;
|
publication.RefloodCommitted = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_physics.CompleteCollisionAdmission(
|
RuntimeCollisionGenerationCommit commit =
|
||||||
publication.CollisionAdmission);
|
_physics.CommitCollisionGeneration(
|
||||||
|
publication.CollisionAdmission,
|
||||||
|
publication.PreparedGeneration);
|
||||||
|
if (!commit.Committed)
|
||||||
|
{
|
||||||
|
publication.RefloodOwnerIds = commit.DirtyDynamicOwnerIds;
|
||||||
|
publication.RefloodCursor = 0;
|
||||||
|
publication.RefloodCommitted = false;
|
||||||
|
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_refloodCount++;
|
||||||
_staticBspOwnerCount += publication.BspOwnerCount;
|
_staticBspOwnerCount += publication.BspOwnerCount;
|
||||||
_staticCylinderOwnerCount += publication.CylinderOwnerCount;
|
_staticCylinderOwnerCount += publication.CylinderOwnerCount;
|
||||||
publication.CompletionCommitted = true;
|
publication.CompletionCommitted = true;
|
||||||
|
|
@ -545,7 +581,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
Matrix4x4.CreateFromQuaternion(rotation)
|
Matrix4x4.CreateFromQuaternion(rotation)
|
||||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||||
|
|
||||||
_physicsDataCache.CacheCellStruct(
|
publication.StagingCache.CacheCellStruct(
|
||||||
envCellId,
|
envCellId,
|
||||||
envCell,
|
envCell,
|
||||||
physicsCellTransform,
|
physicsCellTransform,
|
||||||
|
|
@ -623,7 +659,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
Matrix4x4 physicsCellTransform =
|
Matrix4x4 physicsCellTransform =
|
||||||
Matrix4x4.CreateFromQuaternion(rotation)
|
Matrix4x4.CreateFromQuaternion(rotation)
|
||||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||||
_physicsDataCache.CacheCellStruct(
|
publication.StagingCache.CacheCellStruct(
|
||||||
envCellId,
|
envCellId,
|
||||||
envCell,
|
envCell,
|
||||||
cellStruct,
|
cellStruct,
|
||||||
|
|
@ -687,6 +723,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
private void PublishBuilding(
|
private void PublishBuilding(
|
||||||
LoadedLandblock landblock,
|
LoadedLandblock landblock,
|
||||||
PhysicsDatBundle datBundle,
|
PhysicsDatBundle datBundle,
|
||||||
|
PhysicsDataCache cache,
|
||||||
TerrainSurface terrainSurface,
|
TerrainSurface terrainSurface,
|
||||||
Vector3 origin,
|
Vector3 origin,
|
||||||
BuildingInfo building)
|
BuildingInfo building)
|
||||||
|
|
@ -720,7 +757,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
? setup.Parts[0]
|
? setup.Parts[0]
|
||||||
: 0u;
|
: 0u;
|
||||||
}
|
}
|
||||||
_physicsDataCache.CacheBuilding(
|
cache.CacheBuilding(
|
||||||
landcellId,
|
landcellId,
|
||||||
portals,
|
portals,
|
||||||
buildingTransform,
|
buildingTransform,
|
||||||
|
|
@ -752,11 +789,11 @@ public sealed class LandblockPhysicsPublisher
|
||||||
ShadowShapeBuilder.FromLandblockBspParts(
|
ShadowShapeBuilder.FromLandblockBspParts(
|
||||||
entity.MeshRefs,
|
entity.MeshRefs,
|
||||||
entity.IsBuildingShell,
|
entity.IsBuildingShell,
|
||||||
_physicsDataCache.GetGfxObj);
|
publication.StagingCache.GetGfxObj);
|
||||||
entityBspCount = bspShapes.Count;
|
entityBspCount = bspShapes.Count;
|
||||||
if (entityBspCount > 0)
|
if (entityBspCount > 0)
|
||||||
{
|
{
|
||||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
publication.StagingEngine.ShadowObjects.RegisterMultiPart(
|
||||||
entity.Id,
|
entity.Id,
|
||||||
entity.Position,
|
entity.Position,
|
||||||
entity.Rotation,
|
entity.Rotation,
|
||||||
|
|
@ -772,9 +809,9 @@ public sealed class LandblockPhysicsPublisher
|
||||||
}
|
}
|
||||||
|
|
||||||
FlatSetupCollision? setup =
|
FlatSetupCollision? setup =
|
||||||
_physicsDataCache.GetFlatSetup(entity.SourceGfxObjOrSetupId);
|
publication.StagingCache.GetFlatSetup(entity.SourceGfxObjOrSetupId);
|
||||||
if (setup is null
|
if (setup is null
|
||||||
&& _physicsDataCache.GetSetup(
|
&& publication.StagingCache.GetSetup(
|
||||||
entity.SourceGfxObjOrSetupId) is { } graphSetup)
|
entity.SourceGfxObjOrSetupId) is { } graphSetup)
|
||||||
{
|
{
|
||||||
// Graph-oracle fixture seam only. Production Setup publication is
|
// Graph-oracle fixture seam only. Production Setup publication is
|
||||||
|
|
@ -858,7 +895,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
|
|
||||||
if (setupShapes.Count > 0)
|
if (setupShapes.Count > 0)
|
||||||
{
|
{
|
||||||
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
publication.StagingEngine.ShadowObjects.RegisterMultiPart(
|
||||||
entity.Id,
|
entity.Id,
|
||||||
entity.Position,
|
entity.Position,
|
||||||
entity.Rotation,
|
entity.Rotation,
|
||||||
|
|
@ -917,7 +954,9 @@ public sealed class LandblockPhysicsPublisher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogMissingSceneryBounds(LoadedLandblock landblock)
|
private static void LogMissingSceneryBounds(
|
||||||
|
LoadedLandblock landblock,
|
||||||
|
PhysicsDataCache cache)
|
||||||
{
|
{
|
||||||
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
|
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
@ -933,7 +972,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
foreach (MeshRef meshRef in entity.MeshRefs)
|
foreach (MeshRef meshRef in entity.MeshRefs)
|
||||||
{
|
{
|
||||||
GfxObjVisualBounds? bounds =
|
GfxObjVisualBounds? bounds =
|
||||||
_physicsDataCache.GetVisualBounds(meshRef.GfxObjId);
|
cache.GetVisualBounds(meshRef.GfxObjId);
|
||||||
if (bounds is not null && bounds.Radius > 0f)
|
if (bounds is not null && bounds.Radius > 0f)
|
||||||
{
|
{
|
||||||
hasBounds = true;
|
hasBounds = true;
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,18 @@ public sealed class LandblockPresentationPipeline
|
||||||
public IReadOnlyList<LandblockStreamResult> GetPendingPublicationResults() =>
|
public IReadOnlyList<LandblockStreamResult> GetPendingPublicationResults() =>
|
||||||
_publications.Keys.ToArray();
|
_publications.Keys.ToArray();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancels retained publication receipts during a generation reset. A
|
||||||
|
/// collision receipt owns only its private staging world until activation,
|
||||||
|
/// so cancellation cannot withdraw or partially replace the active world.
|
||||||
|
/// </summary>
|
||||||
|
internal void CancelPendingPublications()
|
||||||
|
{
|
||||||
|
foreach (PublicationTransaction transaction in _publications.Values)
|
||||||
|
transaction.PhysicsPublication?.Dispose();
|
||||||
|
_publications.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
public void ResumePublication(LandblockStreamResult result)
|
public void ResumePublication(LandblockStreamResult result)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(result);
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ public sealed class StreamingController
|
||||||
public bool GenerationAdvanced;
|
public bool GenerationAdvanced;
|
||||||
public bool PendingLoadsCleared;
|
public bool PendingLoadsCleared;
|
||||||
public bool CompletionQueueCleared;
|
public bool CompletionQueueCleared;
|
||||||
|
public bool PendingPublicationsCleared;
|
||||||
public bool RegionCleared;
|
public bool RegionCleared;
|
||||||
public bool SpatialGenerationDetached;
|
public bool SpatialGenerationDetached;
|
||||||
public bool PreparationCommitted;
|
public bool PreparationCommitted;
|
||||||
|
|
@ -45,6 +46,7 @@ public sealed class StreamingController
|
||||||
public bool GenerationAdvanced;
|
public bool GenerationAdvanced;
|
||||||
public bool PendingLoadsCleared;
|
public bool PendingLoadsCleared;
|
||||||
public bool CompletionQueueCleared;
|
public bool CompletionQueueCleared;
|
||||||
|
public bool PendingPublicationsCleared;
|
||||||
public bool RegionCleared;
|
public bool RegionCleared;
|
||||||
public List<uint>? ResidentIds;
|
public List<uint>? ResidentIds;
|
||||||
public IEnumerator<uint>? ResidentEnumerator;
|
public IEnumerator<uint>? ResidentEnumerator;
|
||||||
|
|
@ -1358,6 +1360,22 @@ public sealed class StreamingController
|
||||||
}
|
}
|
||||||
if (!transaction.RegionCleared)
|
if (!transaction.RegionCleared)
|
||||||
{
|
{
|
||||||
|
if (!transaction.PendingPublicationsCleared)
|
||||||
|
{
|
||||||
|
if (!TryRunStreamingWork(
|
||||||
|
meter,
|
||||||
|
new StreamingWorkCost(EntityOperations: 1),
|
||||||
|
"recenter-cancel-publications",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_presentation.CancelPendingPublications();
|
||||||
|
transaction.PendingPublicationsCleared = true;
|
||||||
|
return true;
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!TryRunStreamingWork(
|
if (!TryRunStreamingWork(
|
||||||
meter,
|
meter,
|
||||||
new StreamingWorkCost(EntityOperations: 1),
|
new StreamingWorkCost(EntityOperations: 1),
|
||||||
|
|
@ -1499,6 +1517,22 @@ public sealed class StreamingController
|
||||||
|
|
||||||
if (!transaction.RegionCleared)
|
if (!transaction.RegionCleared)
|
||||||
{
|
{
|
||||||
|
if (!transaction.PendingPublicationsCleared)
|
||||||
|
{
|
||||||
|
if (!TryRunStreamingWork(
|
||||||
|
meter,
|
||||||
|
new StreamingWorkCost(EntityOperations: 1),
|
||||||
|
"reload-cancel-publications",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_presentation.CancelPendingPublications();
|
||||||
|
transaction.PendingPublicationsCleared = true;
|
||||||
|
return true;
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!TryRunStreamingWork(
|
if (!TryRunStreamingWork(
|
||||||
meter,
|
meter,
|
||||||
new StreamingWorkCost(EntityOperations: 1),
|
new StreamingWorkCost(EntityOperations: 1),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@
|
||||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||||
<_Parameter1>AcDream.Core.Tests</_Parameter1>
|
<_Parameter1>AcDream.Core.Tests</_Parameter1>
|
||||||
</AssemblyAttribute>
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||||
|
<_Parameter1>AcDream.Runtime</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using DatReaderWriter.Types;
|
||||||
using Plane = System.Numerics.Plane;
|
using Plane = System.Numerics.Plane;
|
||||||
using UcgEnvCell = AcDream.Core.World.Cells.EnvCell;
|
using UcgEnvCell = AcDream.Core.World.Cells.EnvCell;
|
||||||
using UcgCellGraph = AcDream.Core.World.Cells.CellGraph;
|
using UcgCellGraph = AcDream.Core.World.Cells.CellGraph;
|
||||||
|
using PreparedCellGraphLandblock = AcDream.Core.World.Cells.PreparedCellGraphLandblock;
|
||||||
|
|
||||||
namespace AcDream.Core.Physics;
|
namespace AcDream.Core.Physics;
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ namespace AcDream.Core.Physics;
|
||||||
public sealed class PhysicsDataCache
|
public sealed class PhysicsDataCache
|
||||||
{
|
{
|
||||||
private readonly bool _requirePreparedCollision;
|
private readonly bool _requirePreparedCollision;
|
||||||
|
private PhysicsDataCache? _readFallback;
|
||||||
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
||||||
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
||||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||||
|
|
@ -92,7 +94,112 @@ public sealed class PhysicsDataCache
|
||||||
/// (<c>TryGetTerrainOrigin</c>, read by <c>CellTransit</c>'s pick + transit
|
/// (<c>TryGetTerrainOrigin</c>, read by <c>CellTransit</c>'s pick + transit
|
||||||
/// paths). No longer inert.
|
/// paths). No longer inert.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UcgCellGraph CellGraph { get; } = new();
|
public UcgCellGraph CellGraph { get; private set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the currently committed immutable collision records into an
|
||||||
|
/// off-side cache. Streaming may replace one landblock in this copy over
|
||||||
|
/// many frames without exposing a partially withdrawn cell graph to live
|
||||||
|
/// physics queries.
|
||||||
|
/// </summary>
|
||||||
|
internal PhysicsDataCache CreateCollisionStagingCopy()
|
||||||
|
{
|
||||||
|
var copy = new PhysicsDataCache(_requirePreparedCollision)
|
||||||
|
{
|
||||||
|
CollisionTraversalMode = CollisionTraversalMode,
|
||||||
|
CellGraph = CellGraph.CreateCollisionStagingCopy(),
|
||||||
|
_readFallback = this,
|
||||||
|
};
|
||||||
|
// Global immutable GfxObj/Setup records are not copied wholesale.
|
||||||
|
// The accepted build's exact closure is staged cursor-by-cursor below;
|
||||||
|
// copying the process-retained asset catalog here would turn every
|
||||||
|
// landblock publication into an unbounded frame spike.
|
||||||
|
CopyDictionary(_cellStruct, copy._cellStruct);
|
||||||
|
CopyDictionary(_flatCellStruct, copy._flatCellStruct);
|
||||||
|
CopyDictionary(_flatEnvCell, copy._flatEnvCell);
|
||||||
|
CopyDictionary(_buildings, copy._buildings);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal PreparedPhysicsDataCacheLandblock PrepareLandblockReplacement(
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CommitLandblockReplacement(
|
||||||
|
PreparedPhysicsDataCacheLandblock replacement)
|
||||||
|
{
|
||||||
|
RemoveCellsForLandblock(replacement.LandblockPrefix);
|
||||||
|
RemoveBuildingsForLandblock(replacement.LandblockPrefix);
|
||||||
|
CommitEntries(_gfxObj, replacement.GfxObjects, replace: false);
|
||||||
|
CommitEntries(_visualBounds, replacement.VisualBounds, replace: false);
|
||||||
|
CommitEntries(_flatGfxObj, replacement.FlatGfxObjects, replace: false);
|
||||||
|
CommitEntries(_setup, replacement.Setups, replace: false);
|
||||||
|
CommitEntries(_flatSetup, replacement.FlatSetups, replace: false);
|
||||||
|
CommitEntries(_cellStruct, replacement.Cells, replace: true);
|
||||||
|
CommitEntries(_flatCellStruct, replacement.FlatCells, replace: true);
|
||||||
|
CommitEntries(_flatEnvCell, replacement.FlatEnvCells, replace: true);
|
||||||
|
CommitEntries(_buildings, replacement.Buildings, replace: true);
|
||||||
|
CellGraph.CommitLandblockReplacement(replacement.CellGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyDictionary<T>(
|
||||||
|
ConcurrentDictionary<uint, T> source,
|
||||||
|
ConcurrentDictionary<uint, T> destination)
|
||||||
|
{
|
||||||
|
foreach ((uint id, T value) in source)
|
||||||
|
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,
|
||||||
|
bool replace)
|
||||||
|
{
|
||||||
|
foreach ((uint id, T value) in entries)
|
||||||
|
{
|
||||||
|
if (replace)
|
||||||
|
destination[id] = value;
|
||||||
|
else
|
||||||
|
destination.TryAdd(id, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extract and cache the physics BSP + polygon data from a GfxObj,
|
/// Extract and cache the physics BSP + polygon data from a GfxObj,
|
||||||
|
|
@ -237,7 +344,9 @@ public sealed class PhysicsDataCache
|
||||||
/// Get the cached visual AABB for a GfxObj, or null if not cached.
|
/// Get the cached visual AABB for a GfxObj, or null if not cached.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GfxObjVisualBounds? GetVisualBounds(uint gfxObjId) =>
|
public GfxObjVisualBounds? GetVisualBounds(uint gfxObjId) =>
|
||||||
_visualBounds.TryGetValue(gfxObjId, out var vb) ? vb : null;
|
_visualBounds.TryGetValue(gfxObjId, out var vb)
|
||||||
|
? vb
|
||||||
|
: _readFallback?.GetVisualBounds(gfxObjId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compute a tight axis-aligned bounding box over all vertices in the mesh.
|
/// Compute a tight axis-aligned bounding box over all vertices in the mesh.
|
||||||
|
|
@ -756,14 +865,24 @@ public sealed class PhysicsDataCache
|
||||||
$"Production {kind} 0x{sourceId:X8} has no prepared collision asset. " +
|
$"Production {kind} 0x{sourceId:X8} has no prepared collision asset. " +
|
||||||
"Gameplay must not extract or fall back to a parsed DAT graph.");
|
"Gameplay must not extract or fall back to a parsed DAT graph.");
|
||||||
|
|
||||||
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
|
public GfxObjPhysics? GetGfxObj(uint id) =>
|
||||||
|
_gfxObj.TryGetValue(id, out var p)
|
||||||
|
? p
|
||||||
|
: _readFallback?.GetGfxObj(id);
|
||||||
|
|
||||||
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
public SetupPhysics? GetSetup(uint id) =>
|
||||||
|
_setup.TryGetValue(id, out var p)
|
||||||
|
? p
|
||||||
|
: _readFallback?.GetSetup(id);
|
||||||
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
||||||
public FlatGfxObjCollisionAsset? GetFlatGfxObj(uint id) =>
|
public FlatGfxObjCollisionAsset? GetFlatGfxObj(uint id) =>
|
||||||
_flatGfxObj.TryGetValue(id, out var value) ? value : null;
|
_flatGfxObj.TryGetValue(id, out var value)
|
||||||
|
? value
|
||||||
|
: _readFallback?.GetFlatGfxObj(id);
|
||||||
public FlatSetupCollision? GetFlatSetup(uint id) =>
|
public FlatSetupCollision? GetFlatSetup(uint id) =>
|
||||||
_flatSetup.TryGetValue(id, out var value) ? value : null;
|
_flatSetup.TryGetValue(id, out var value)
|
||||||
|
? value
|
||||||
|
: _readFallback?.GetFlatSetup(id);
|
||||||
public FlatCellStructureCollisionAsset? GetFlatCellStruct(uint id) =>
|
public FlatCellStructureCollisionAsset? GetFlatCellStruct(uint id) =>
|
||||||
_flatCellStruct.TryGetValue(id, out var value) ? value : null;
|
_flatCellStruct.TryGetValue(id, out var value) ? value : null;
|
||||||
public FlatEnvCellTopology? GetFlatEnvCell(uint id) =>
|
public FlatEnvCellTopology? GetFlatEnvCell(uint id) =>
|
||||||
|
|
@ -926,6 +1045,19 @@ public sealed class PhysicsDataCache
|
||||||
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) => _buildings[landcellId] = b;
|
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) => _buildings[landcellId] = b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
PreparedCellGraphLandblock CellGraph);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visual AABB of a GfxObj mesh — populated for every cached GfxObj regardless
|
/// Visual AABB of a GfxObj mesh — populated for every cached GfxObj regardless
|
||||||
/// of whether it has physics data. Used as a collision fallback shape for
|
/// of whether it has physics data. Used as a collision fallback shape for
|
||||||
|
|
|
||||||
|
|
@ -153,13 +153,106 @@ public sealed class PhysicsEngine
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ClientObjectTable? Objects { get; set; }
|
public ClientObjectTable? Objects { get; set; }
|
||||||
|
|
||||||
private sealed record LandblockPhysics(
|
internal sealed record LandblockPhysics(
|
||||||
TerrainSurface Terrain,
|
TerrainSurface Terrain,
|
||||||
IReadOnlyList<CellSurface> Cells,
|
IReadOnlyList<CellSurface> Cells,
|
||||||
IReadOnlyList<PortalPlane> Portals,
|
IReadOnlyList<PortalPlane> Portals,
|
||||||
float WorldOffsetX,
|
float WorldOffsetX,
|
||||||
float WorldOffsetY);
|
float WorldOffsetY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an off-side collision world from the last complete generation.
|
||||||
|
/// Streaming modifies this copy only; the active engine and its borrowed
|
||||||
|
/// cache/registry identities remain stable until Runtime commits.
|
||||||
|
/// </summary>
|
||||||
|
internal PhysicsEngine CreateCollisionStagingCopy(
|
||||||
|
PhysicsDataCache stagingCache)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(stagingCache);
|
||||||
|
var staging = new PhysicsEngine
|
||||||
|
{
|
||||||
|
DataCache = stagingCache,
|
||||||
|
Objects = Objects,
|
||||||
|
};
|
||||||
|
foreach ((uint id, LandblockPhysics landblock) in _landblocks)
|
||||||
|
staging._landblocks[id] = landblock;
|
||||||
|
staging.ShadowObjects.CopyCollisionStateFrom(
|
||||||
|
ShadowObjects,
|
||||||
|
stagingCache);
|
||||||
|
return staging;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal PreparedPhysicsEngineLandblock PrepareLandblockReplacement(
|
||||||
|
PhysicsEngine staging,
|
||||||
|
uint landblockId,
|
||||||
|
ReadOnlySpan<uint> gfxObjectIds,
|
||||||
|
ReadOnlySpan<uint> setupIds,
|
||||||
|
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(staging);
|
||||||
|
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
|
if (!staging._landblocks.TryGetValue(
|
||||||
|
canonical,
|
||||||
|
out LandblockPhysics? landblock))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Staging collision generation has no landblock 0x{canonical:X8}.");
|
||||||
|
}
|
||||||
|
PhysicsDataCache stagingCache = staging.DataCache
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Staging collision engine has no data cache.");
|
||||||
|
return new PreparedPhysicsEngineLandblock(
|
||||||
|
canonical,
|
||||||
|
landblock,
|
||||||
|
stagingCache.PrepareLandblockReplacement(
|
||||||
|
canonical,
|
||||||
|
gfxObjectIds,
|
||||||
|
setupIds),
|
||||||
|
ShadowObjects.PrepareLandblockReplacement(
|
||||||
|
staging.ShadowObjects,
|
||||||
|
canonical,
|
||||||
|
expectedDynamicVersions));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
_landblocks[replacement.LandblockId] = replacement.Landblock;
|
||||||
|
ShadowObjects.CommitLandblockReplacement(replacement.Shadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PreparedPhysicsEngineLandblock
|
||||||
|
{
|
||||||
|
internal PreparedPhysicsEngineLandblock(
|
||||||
|
uint landblockId,
|
||||||
|
LandblockPhysics landblock,
|
||||||
|
PreparedPhysicsDataCacheLandblock dataCache,
|
||||||
|
ShadowObjectRegistry.PreparedLandblockShadowReplacement shadows)
|
||||||
|
{
|
||||||
|
LandblockId = landblockId;
|
||||||
|
Landblock = landblock;
|
||||||
|
DataCache = dataCache;
|
||||||
|
Shadows = shadows;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal uint LandblockId { get; }
|
||||||
|
internal LandblockPhysics Landblock { get; }
|
||||||
|
internal PreparedPhysicsDataCacheLandblock DataCache { get; }
|
||||||
|
internal ShadowObjectRegistry.PreparedLandblockShadowReplacement Shadows { get; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a landblock with its terrain surface, indoor cells, portal
|
/// Register a landblock with its terrain surface, indoor cells, portal
|
||||||
/// planes, and world-space origin offset.
|
/// planes, and world-space origin offset.
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,9 @@ public sealed class ShadowObjectRegistry
|
||||||
/// is the streaming-side trigger.
|
/// is the streaming-side trigger.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
|
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
|
||||||
|
private readonly Dictionary<uint, ulong> _ownerVersions = new();
|
||||||
|
|
||||||
private sealed record RegistrationRecord(
|
internal sealed record RegistrationRecord(
|
||||||
uint SeedCellId,
|
uint SeedCellId,
|
||||||
Vector3 EntityWorldPos,
|
Vector3 EntityWorldPos,
|
||||||
Quaternion EntityWorldRot,
|
Quaternion EntityWorldRot,
|
||||||
|
|
@ -67,6 +68,16 @@ public sealed class ShadowObjectRegistry
|
||||||
float CylHeight,
|
float CylHeight,
|
||||||
float Scale);
|
float Scale);
|
||||||
|
|
||||||
|
internal ulong GetOwnerVersion(uint entityId) =>
|
||||||
|
_ownerVersions.TryGetValue(entityId, out ulong version)
|
||||||
|
? version
|
||||||
|
: 0UL;
|
||||||
|
|
||||||
|
private void BumpOwnerVersion(uint entityId)
|
||||||
|
{
|
||||||
|
_ownerVersions[entityId] = checked(GetOwnerVersion(entityId) + 1UL);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The flood's data source (cells, buildings, terrain origins). Wired by
|
/// The flood's data source (cells, buildings, terrain origins). Wired by
|
||||||
/// <see cref="PhysicsEngine"/> when its own <c>DataCache</c> is set.
|
/// <see cref="PhysicsEngine"/> when its own <c>DataCache</c> is set.
|
||||||
|
|
@ -135,6 +146,7 @@ public sealed class ShadowObjectRegistry
|
||||||
_entityReg[entityId] = new RegistrationRecord(
|
_entityReg[entityId] = new RegistrationRecord(
|
||||||
seed, worldPos, rotation, state, flags, isStatic,
|
seed, worldPos, rotation, state, flags, isStatic,
|
||||||
IsMultiPart: false, gfxObjId, radius, collisionType, cylHeight, scale);
|
IsMultiPart: false, gfxObjId, radius, collisionType, cylHeight, scale);
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -214,6 +226,7 @@ public sealed class ShadowObjectRegistry
|
||||||
seed, entityWorldPos, entityWorldRot, state, flags, isStatic,
|
seed, entityWorldPos, entityWorldRot, state, flags, isStatic,
|
||||||
IsMultiPart: true, GfxObjId: 0u, Radius: 0f,
|
IsMultiPart: true, GfxObjId: 0u, Radius: 0f,
|
||||||
CollisionType: ShadowCollisionType.BSP, CylHeight: 0f, Scale: 1f);
|
CollisionType: ShadowCollisionType.BSP, CylHeight: 0f, Scale: 1f);
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -271,7 +284,10 @@ public sealed class ShadowObjectRegistry
|
||||||
};
|
};
|
||||||
|
|
||||||
if (suspended || !_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
if (suspended || !_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
||||||
|
{
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (uint cellId in cells)
|
foreach (uint cellId in cells)
|
||||||
{
|
{
|
||||||
|
|
@ -300,6 +316,7 @@ public sealed class ShadowObjectRegistry
|
||||||
foreach (uint cellId in cells)
|
foreach (uint cellId in cells)
|
||||||
AddEntryToCell(entry, cellId);
|
AddEntryToCell(entry, cellId);
|
||||||
}
|
}
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -441,6 +458,7 @@ public sealed class ShadowObjectRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
_suspendedEntities.Add(entityId);
|
_suspendedEntities.Add(entityId);
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -624,11 +642,16 @@ public sealed class ShadowObjectRegistry
|
||||||
|
|
||||||
if (_entityReg.TryGetValue(entityId, out var reg))
|
if (_entityReg.TryGetValue(entityId, out var reg))
|
||||||
_entityReg[entityId] = reg with { State = newState };
|
_entityReg[entityId] = reg with { State = newState };
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Remove an entity from all cells it was registered in.</summary>
|
/// <summary>Remove an entity from all cells it was registered in.</summary>
|
||||||
public void Deregister(uint entityId)
|
public void Deregister(uint entityId)
|
||||||
{
|
{
|
||||||
|
bool existed = _entityReg.ContainsKey(entityId)
|
||||||
|
|| _entityToCells.ContainsKey(entityId)
|
||||||
|
|| _entityShapes.ContainsKey(entityId)
|
||||||
|
|| _suspendedEntities.Contains(entityId);
|
||||||
if (_entityToCells.TryGetValue(entityId, out var cellIds))
|
if (_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||||
{
|
{
|
||||||
foreach (var cellId in cellIds)
|
foreach (var cellId in cellIds)
|
||||||
|
|
@ -642,6 +665,8 @@ public sealed class ShadowObjectRegistry
|
||||||
_entityReg.Remove(entityId);
|
_entityReg.Remove(entityId);
|
||||||
_suspendedEntities.Remove(entityId);
|
_suspendedEntities.Remove(entityId);
|
||||||
_withdrawnPrefixesByOwner.Remove(entityId);
|
_withdrawnPrefixesByOwner.Remove(entityId);
|
||||||
|
if (existed)
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -708,11 +733,13 @@ public sealed class ShadowObjectRegistry
|
||||||
{
|
{
|
||||||
uint lbPrefix = landblockId & 0xFFFF0000u;
|
uint lbPrefix = landblockId & 0xFFFF0000u;
|
||||||
var toRemove = new List<uint>();
|
var toRemove = new List<uint>();
|
||||||
|
var touchedOwners = new HashSet<uint>();
|
||||||
|
|
||||||
foreach (var (entityId, cells) in _entityToCells)
|
foreach (var (entityId, cells) in _entityToCells)
|
||||||
{
|
{
|
||||||
if (!cells.Exists(cell => (cell & 0xFFFF0000u) == lbPrefix))
|
if (!cells.Exists(cell => (cell & 0xFFFF0000u) == lbPrefix))
|
||||||
continue;
|
continue;
|
||||||
|
touchedOwners.Add(entityId);
|
||||||
if (!_withdrawnPrefixesByOwner.TryGetValue(entityId, out var withdrawn))
|
if (!_withdrawnPrefixesByOwner.TryGetValue(entityId, out var withdrawn))
|
||||||
{
|
{
|
||||||
withdrawn = new HashSet<uint>();
|
withdrawn = new HashSet<uint>();
|
||||||
|
|
@ -753,6 +780,8 @@ public sealed class ShadowObjectRegistry
|
||||||
_withdrawnPrefixesByOwner.Remove(eid);
|
_withdrawnPrefixesByOwner.Remove(eid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
foreach (uint entityId in touchedOwners)
|
||||||
|
BumpOwnerVersion(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -795,6 +824,315 @@ public sealed class ShadowObjectRegistry
|
||||||
/// <summary>Suspended logical registrations awaiting spatial re-entry.</summary>
|
/// <summary>Suspended logical registrations awaiting spatial re-entry.</summary>
|
||||||
public int SuspendedRegistrationCount => _suspendedEntities.Count;
|
public int SuspendedRegistrationCount => _suspendedEntities.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the committed registry into an off-side collision generation.
|
||||||
|
/// All mutable lists and sets are cloned; immutable registration and shape
|
||||||
|
/// payloads may be shared.
|
||||||
|
/// </summary>
|
||||||
|
internal void CopyCollisionStateFrom(
|
||||||
|
ShadowObjectRegistry source,
|
||||||
|
PhysicsDataCache stagingCache)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(stagingCache);
|
||||||
|
Clear();
|
||||||
|
DataCache = stagingCache;
|
||||||
|
foreach ((uint cellId, List<ShadowEntry> entries) in source._cells)
|
||||||
|
_cells[cellId] = new List<ShadowEntry>(entries);
|
||||||
|
foreach ((uint ownerId, List<uint> cells) in source._entityToCells)
|
||||||
|
_entityToCells[ownerId] = new List<uint>(cells);
|
||||||
|
foreach (uint ownerId in source._suspendedEntities)
|
||||||
|
_suspendedEntities.Add(ownerId);
|
||||||
|
foreach ((uint ownerId, HashSet<uint> prefixes) in
|
||||||
|
source._withdrawnPrefixesByOwner)
|
||||||
|
{
|
||||||
|
_withdrawnPrefixesByOwner[ownerId] = new HashSet<uint>(prefixes);
|
||||||
|
}
|
||||||
|
foreach ((uint ownerId, IReadOnlyList<ShadowShape> shapes) in
|
||||||
|
source._entityShapes)
|
||||||
|
{
|
||||||
|
_entityShapes[ownerId] = shapes;
|
||||||
|
}
|
||||||
|
foreach ((uint ownerId, RegistrationRecord registration) in
|
||||||
|
source._entityReg)
|
||||||
|
{
|
||||||
|
_entityReg[ownerId] = registration;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes one staging owner from the exact active payload, then floods
|
||||||
|
/// it against the staging generation's complete cell graph. The returned
|
||||||
|
/// source version is the commit-time freshness token.
|
||||||
|
/// </summary>
|
||||||
|
internal bool RefreshDynamicOwnerFrom(
|
||||||
|
ShadowObjectRegistry source,
|
||||||
|
uint entityId,
|
||||||
|
uint landblockId,
|
||||||
|
out ulong sourceVersion)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
Deregister(entityId);
|
||||||
|
sourceVersion = source.GetOwnerVersion(entityId);
|
||||||
|
if (!source._entityReg.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out RegistrationRecord? registration)
|
||||||
|
|| registration.IsStatic
|
||||||
|
|| source._suspendedEntities.Contains(entityId)
|
||||||
|
|| !source.OwnerTouchesLandblock(entityId, landblockId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registration.IsMultiPart
|
||||||
|
&& source._entityShapes.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out IReadOnlyList<ShadowShape>? shapes))
|
||||||
|
{
|
||||||
|
RegisterMultiPart(
|
||||||
|
entityId,
|
||||||
|
registration.EntityWorldPos,
|
||||||
|
registration.EntityWorldRot,
|
||||||
|
shapes,
|
||||||
|
registration.State,
|
||||||
|
registration.Flags,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
landblockId,
|
||||||
|
registration.SeedCellId,
|
||||||
|
isStatic: false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Register(
|
||||||
|
entityId,
|
||||||
|
registration.GfxObjId,
|
||||||
|
registration.EntityWorldPos,
|
||||||
|
registration.EntityWorldRot,
|
||||||
|
registration.Radius,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
landblockId,
|
||||||
|
registration.CollisionType,
|
||||||
|
registration.CylHeight,
|
||||||
|
registration.Scale,
|
||||||
|
registration.State,
|
||||||
|
registration.Flags,
|
||||||
|
registration.SeedCellId,
|
||||||
|
isStatic: false);
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
ShadowObjectRegistry staging,
|
||||||
|
uint landblockId,
|
||||||
|
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(staging);
|
||||||
|
uint[] dirty = FindDirtyDynamicOwners(
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool OwnerTouchesLandblock(uint entityId, uint landblockId)
|
||||||
|
{
|
||||||
|
uint prefix = landblockId & 0xFFFF0000u;
|
||||||
|
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? record))
|
||||||
|
return false;
|
||||||
|
if ((record.SeedCellId & 0xFFFF0000u) == prefix)
|
||||||
|
return true;
|
||||||
|
if (_entityToCells.TryGetValue(entityId, out List<uint>? cells)
|
||||||
|
&& cells.Exists(cell => (cell & 0xFFFF0000u) == prefix))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return _withdrawnPrefixesByOwner.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out HashSet<uint>? withdrawn)
|
||||||
|
&& withdrawn.Contains(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryCaptureOwnerState(
|
||||||
|
uint entityId,
|
||||||
|
out PreparedShadowOwnerState? state)
|
||||||
|
{
|
||||||
|
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? registration))
|
||||||
|
{
|
||||||
|
state = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_entityToCells.TryGetValue(entityId, out List<uint>? cells);
|
||||||
|
_entityShapes.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out IReadOnlyList<ShadowShape>? shapes);
|
||||||
|
_withdrawnPrefixesByOwner.TryGetValue(
|
||||||
|
entityId,
|
||||||
|
out HashSet<uint>? withdrawn);
|
||||||
|
var rows = new List<PreparedShadowCellRows>();
|
||||||
|
if (cells is not null)
|
||||||
|
{
|
||||||
|
foreach (uint cellId in cells)
|
||||||
|
{
|
||||||
|
if (_cells.TryGetValue(cellId, out List<ShadowEntry>? entries))
|
||||||
|
{
|
||||||
|
rows.Add(new PreparedShadowCellRows(
|
||||||
|
cellId,
|
||||||
|
entries.Where(entry => entry.EntityId == entityId)
|
||||||
|
.ToArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = new PreparedShadowOwnerState(
|
||||||
|
entityId,
|
||||||
|
registration,
|
||||||
|
shapes,
|
||||||
|
cells?.ToArray() ?? Array.Empty<uint>(),
|
||||||
|
rows.ToArray(),
|
||||||
|
_suspendedEntities.Contains(entityId),
|
||||||
|
withdrawn?.ToArray() ?? Array.Empty<uint>());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InstallOwnerState(PreparedShadowOwnerState state)
|
||||||
|
{
|
||||||
|
_entityReg[state.EntityId] = state.Registration;
|
||||||
|
if (state.Shapes is not null)
|
||||||
|
_entityShapes[state.EntityId] = state.Shapes;
|
||||||
|
if (state.Suspended)
|
||||||
|
_suspendedEntities.Add(state.EntityId);
|
||||||
|
if (state.WithdrawnPrefixes.Length != 0)
|
||||||
|
{
|
||||||
|
_withdrawnPrefixesByOwner[state.EntityId] =
|
||||||
|
new HashSet<uint>(state.WithdrawnPrefixes);
|
||||||
|
}
|
||||||
|
if (state.CellIds.Length != 0)
|
||||||
|
_entityToCells[state.EntityId] = new List<uint>(state.CellIds);
|
||||||
|
foreach (PreparedShadowCellRows row in state.Rows)
|
||||||
|
{
|
||||||
|
foreach (ShadowEntry entry in row.Entries)
|
||||||
|
AddEntryToCell(entry, row.CellId);
|
||||||
|
}
|
||||||
|
BumpOwnerVersion(state.EntityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PreparedLandblockShadowReplacement
|
||||||
|
{
|
||||||
|
internal PreparedLandblockShadowReplacement(
|
||||||
|
uint landblockPrefix,
|
||||||
|
uint[] ownerIds,
|
||||||
|
PreparedShadowOwnerState[] ownerStates,
|
||||||
|
Dictionary<uint, ulong> dynamicVersions)
|
||||||
|
{
|
||||||
|
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 sealed record PreparedShadowOwnerState(
|
||||||
|
uint EntityId,
|
||||||
|
RegistrationRecord Registration,
|
||||||
|
IReadOnlyList<ShadowShape>? Shapes,
|
||||||
|
uint[] CellIds,
|
||||||
|
PreparedShadowCellRows[] Rows,
|
||||||
|
bool Suspended,
|
||||||
|
uint[] WithdrawnPrefixes);
|
||||||
|
|
||||||
|
internal sealed record PreparedShadowCellRows(
|
||||||
|
uint CellId,
|
||||||
|
ShadowEntry[] Entries);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retires the complete logical registry at terminal physics-engine
|
/// Retires the complete logical registry at terminal physics-engine
|
||||||
/// disposal, including suspended live registrations that own no cell row.
|
/// disposal, including suspended live registrations that own no cell row.
|
||||||
|
|
@ -807,6 +1145,7 @@ public sealed class ShadowObjectRegistry
|
||||||
_withdrawnPrefixesByOwner.Clear();
|
_withdrawnPrefixesByOwner.Clear();
|
||||||
_entityShapes.Clear();
|
_entityShapes.Clear();
|
||||||
_entityReg.Clear();
|
_entityReg.Clear();
|
||||||
|
_ownerVersions.Clear();
|
||||||
_fallback = null;
|
_fallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,4 +125,76 @@ public sealed class CellGraph
|
||||||
return stab;
|
return stab;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an immutable-reference snapshot for collision-generation
|
||||||
|
/// preparation. EnvCell and TerrainSurface records are immutable after
|
||||||
|
/// publication, so copying the registries is sufficient; the active graph
|
||||||
|
/// remains untouched while the staging graph is rebuilt.
|
||||||
|
/// </summary>
|
||||||
|
internal CellGraph CreateCollisionStagingCopy()
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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 void CommitLandblockReplacement(
|
||||||
|
PreparedCellGraphLandblock replacement)
|
||||||
|
{
|
||||||
|
uint currentCellId = CurrCell?.Id ?? 0u;
|
||||||
|
RemoveLandblock(replacement.LandblockPrefix);
|
||||||
|
if (replacement.HasTerrain)
|
||||||
|
{
|
||||||
|
_terrain[replacement.LandblockPrefix] = (
|
||||||
|
replacement.Terrain!,
|
||||||
|
replacement.Origin);
|
||||||
|
}
|
||||||
|
foreach ((uint id, EnvCell cell) in replacement.EnvCells)
|
||||||
|
_envCells[id] = cell;
|
||||||
|
|
||||||
|
uint desiredCurrentCellId =
|
||||||
|
(currentCellId & 0xFFFF0000u) == replacement.LandblockPrefix
|
||||||
|
? currentCellId
|
||||||
|
: currentCellId == 0u
|
||||||
|
&& (replacement.CurrentCellId & 0xFFFF0000u)
|
||||||
|
== replacement.LandblockPrefix
|
||||||
|
? replacement.CurrentCellId
|
||||||
|
: 0u;
|
||||||
|
if (desiredCurrentCellId != 0u)
|
||||||
|
CurrCell = GetVisible(desiredCurrentCellId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed record PreparedCellGraphLandblock(
|
||||||
|
uint LandblockPrefix,
|
||||||
|
KeyValuePair<uint, EnvCell>[] EnvCells,
|
||||||
|
bool HasTerrain,
|
||||||
|
TerrainSurface? Terrain,
|
||||||
|
Vector3 Origin,
|
||||||
|
uint CurrentCellId);
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,14 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
|
|
||||||
RuntimePhysicsState physics =
|
RuntimePhysicsState physics =
|
||||||
_runtime.EntityObjects.Physics;
|
_runtime.EntityObjects.Physics;
|
||||||
PhysicsDataCache cache = physics.DataCache;
|
RuntimeCollisionAdmission admission =
|
||||||
|
physics.BeginCollisionAdmission(landblockId);
|
||||||
|
using PreparedLandblockCollisionGeneration prepared =
|
||||||
|
physics.PrepareCollisionGeneration(admission);
|
||||||
|
prepared.SetAssetClosure(
|
||||||
|
[.. collisions.GfxObjIds],
|
||||||
|
[.. collisions.SetupIds]);
|
||||||
|
PhysicsDataCache cache = prepared.DataCache;
|
||||||
TerrainSurface terrain =
|
TerrainSurface terrain =
|
||||||
LandblockPhysicsContentBuilder.BuildTerrainSurface(
|
LandblockPhysicsContentBuilder.BuildTerrainSurface(
|
||||||
landblock,
|
landblock,
|
||||||
|
|
@ -197,12 +204,11 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
cache,
|
cache,
|
||||||
collisions);
|
collisions);
|
||||||
|
|
||||||
RuntimeCollisionAdmission admission =
|
|
||||||
physics.BeginCollisionAdmission(landblockId);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
physics.AdmitCollisionAssets(
|
physics.StageCollisionAssets(
|
||||||
admission,
|
admission,
|
||||||
|
prepared,
|
||||||
new RuntimeLandblockCollisionAssets(
|
new RuntimeLandblockCollisionAssets(
|
||||||
landblockId,
|
landblockId,
|
||||||
terrain,
|
terrain,
|
||||||
|
|
@ -213,12 +219,27 @@ internal sealed class HeadlessCollisionNeighborhood
|
||||||
currentCellId));
|
currentCellId));
|
||||||
_ = LandblockPhysicsContentBuilder
|
_ = LandblockPhysicsContentBuilder
|
||||||
.PublishStaticCollision(
|
.PublishStaticCollision(
|
||||||
physics.Engine,
|
prepared.Engine,
|
||||||
cache,
|
cache,
|
||||||
landblock,
|
landblock,
|
||||||
collisions,
|
collisions,
|
||||||
origin);
|
origin);
|
||||||
_ = physics.CompleteCollisionAdmission(admission);
|
foreach (uint ownerId in physics.CaptureCollisionDynamicOwners(
|
||||||
|
admission,
|
||||||
|
prepared))
|
||||||
|
{
|
||||||
|
physics.RefreshCollisionDynamicOwner(
|
||||||
|
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));
|
_resident.Add(CanonicalLandblock(landblockId));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
<InternalsVisibleTo Include="AcDream.App" />
|
<InternalsVisibleTo Include="AcDream.App" />
|
||||||
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
||||||
<InternalsVisibleTo Include="AcDream.Core.Tests" />
|
<InternalsVisibleTo Include="AcDream.Core.Tests" />
|
||||||
|
<InternalsVisibleTo Include="acdream-headless" />
|
||||||
|
<InternalsVisibleTo Include="AcDream.Headless.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ public sealed class RuntimeCollisionAdmission
|
||||||
}
|
}
|
||||||
|
|
||||||
internal RuntimePhysicsState Owner { get; }
|
internal RuntimePhysicsState Owner { get; }
|
||||||
internal bool AssetsAdmitted { get; set; }
|
internal bool AssetsPrepared { get; set; }
|
||||||
internal bool Completed { get; set; }
|
internal bool Completed { get; set; }
|
||||||
public uint LandblockId { get; }
|
public uint LandblockId { get; }
|
||||||
public ulong Generation { get; }
|
public ulong Generation { get; }
|
||||||
|
|
@ -63,7 +63,109 @@ public sealed class RuntimeCollisionAdmission
|
||||||
public readonly record struct RuntimeCollisionAcknowledgement(
|
public readonly record struct RuntimeCollisionAcknowledgement(
|
||||||
uint LandblockId,
|
uint LandblockId,
|
||||||
ulong Generation,
|
ulong Generation,
|
||||||
bool WasResident);
|
bool WasResident,
|
||||||
|
bool Ready);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCollisionGenerationCommit(
|
||||||
|
RuntimeCollisionAcknowledgement Acknowledgement,
|
||||||
|
uint[] DirtyDynamicOwnerIds)
|
||||||
|
{
|
||||||
|
public bool Committed => Acknowledgement.Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCollisionGenerationCommitted(
|
||||||
|
uint LandblockId,
|
||||||
|
ulong Generation,
|
||||||
|
bool Ready);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One off-side collision generation. It owns a private cache, cell graph,
|
||||||
|
/// engine, and shadow registry cloned from the previous complete generation.
|
||||||
|
/// Hosts may populate it incrementally, but only Runtime can activate it.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PreparedLandblockCollisionGeneration : IDisposable
|
||||||
|
{
|
||||||
|
private readonly RuntimePhysicsState _owner;
|
||||||
|
private readonly RuntimeCollisionAdmission _admission;
|
||||||
|
private readonly Dictionary<uint, ulong> _dynamicOwnerVersions = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
internal PreparedLandblockCollisionGeneration(
|
||||||
|
RuntimePhysicsState owner,
|
||||||
|
RuntimeCollisionAdmission admission,
|
||||||
|
PhysicsDataCache dataCache,
|
||||||
|
PhysicsEngine engine)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_admission = admission;
|
||||||
|
DataCache = dataCache;
|
||||||
|
Engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal PhysicsDataCache DataCache { get; }
|
||||||
|
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 bool IsDisposed => _disposed;
|
||||||
|
|
||||||
|
internal bool Matches(
|
||||||
|
RuntimePhysicsState owner,
|
||||||
|
RuntimeCollisionAdmission admission) =>
|
||||||
|
ReferenceEquals(_owner, owner)
|
||||||
|
&& ReferenceEquals(_admission, admission);
|
||||||
|
|
||||||
|
internal void SetAssetClosure(uint[] gfxObjectIds, uint[] setupIds)
|
||||||
|
{
|
||||||
|
EnsureUsable();
|
||||||
|
GfxObjectIds = gfxObjectIds ?? throw new ArgumentNullException(nameof(gfxObjectIds));
|
||||||
|
SetupIds = setupIds ?? throw new ArgumentNullException(nameof(setupIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RefreshDynamicOwner(uint ownerId)
|
||||||
|
{
|
||||||
|
EnsureUsable();
|
||||||
|
bool retained = Engine.ShadowObjects.RefreshDynamicOwnerFrom(
|
||||||
|
_owner.Engine.ShadowObjects,
|
||||||
|
ownerId,
|
||||||
|
_admission.LandblockId,
|
||||||
|
out ulong version);
|
||||||
|
if (retained)
|
||||||
|
_dynamicOwnerVersions[ownerId] = version;
|
||||||
|
else
|
||||||
|
_dynamicOwnerVersions.Remove(ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal uint[] FindDirtyDynamicOwners()
|
||||||
|
{
|
||||||
|
EnsureUsable();
|
||||||
|
return _owner.Engine.ShadowObjects.FindDirtyDynamicOwners(
|
||||||
|
_admission.LandblockId,
|
||||||
|
_dynamicOwnerVersions);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void MarkCommitted()
|
||||||
|
{
|
||||||
|
EnsureUsable();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
Engine.Clear();
|
||||||
|
_dynamicOwnerVersions.Clear();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureUsable()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
throw new ObjectDisposedException(nameof(PreparedLandblockCollisionGeneration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Presentation-free mutable physics world for one Runtime/session owner.
|
/// Presentation-free mutable physics world for one Runtime/session owner.
|
||||||
|
|
@ -83,9 +185,12 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
private readonly Dictionary<uint, ulong> _collisionGenerations = new();
|
private readonly Dictionary<uint, ulong> _collisionGenerations = new();
|
||||||
private readonly Dictionary<uint, RuntimeCollisionAdmission>
|
private readonly Dictionary<uint, RuntimeCollisionAdmission>
|
||||||
_collisionAdmissions = new();
|
_collisionAdmissions = new();
|
||||||
|
private int _collisionMutationThreadId;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public event Action<RuntimePhysicsCellCommit>? CellCommitted;
|
public event Action<RuntimePhysicsCellCommit>? CellCommitted;
|
||||||
|
public event Action<RuntimeCollisionGenerationCommitted>?
|
||||||
|
CollisionGenerationCommitted;
|
||||||
|
|
||||||
internal RuntimePhysicsState(
|
internal RuntimePhysicsState(
|
||||||
RuntimeEntityDirectory entities,
|
RuntimeEntityDirectory entities,
|
||||||
|
|
@ -865,6 +970,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
uint landblockId)
|
uint landblockId)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
|
EnsureCollisionMutationThread();
|
||||||
uint canonical = CanonicalLandblock(landblockId);
|
uint canonical = CanonicalLandblock(landblockId);
|
||||||
ulong generation = _collisionGenerations.TryGetValue(
|
ulong generation = _collisionGenerations.TryGetValue(
|
||||||
canonical,
|
canonical,
|
||||||
|
|
@ -880,11 +986,29 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
return admission;
|
return admission;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AdmitCollisionAssets(
|
internal PreparedLandblockCollisionGeneration PrepareCollisionGeneration(
|
||||||
|
RuntimeCollisionAdmission admission)
|
||||||
|
{
|
||||||
|
ValidateAdmission(admission);
|
||||||
|
EnsureCollisionMutationThread();
|
||||||
|
PhysicsDataCache stagingCache = DataCache.CreateCollisionStagingCopy();
|
||||||
|
PhysicsEngine stagingEngine =
|
||||||
|
Engine.CreateCollisionStagingCopy(stagingCache);
|
||||||
|
return new PreparedLandblockCollisionGeneration(
|
||||||
|
this,
|
||||||
|
admission,
|
||||||
|
stagingCache,
|
||||||
|
stagingEngine);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void StageCollisionAssets(
|
||||||
RuntimeCollisionAdmission admission,
|
RuntimeCollisionAdmission admission,
|
||||||
|
PreparedLandblockCollisionGeneration prepared,
|
||||||
RuntimeLandblockCollisionAssets assets)
|
RuntimeLandblockCollisionAssets assets)
|
||||||
{
|
{
|
||||||
ValidateAdmission(admission);
|
ValidateAdmission(admission);
|
||||||
|
EnsureCollisionMutationThread();
|
||||||
|
ValidatePreparedGeneration(admission, prepared);
|
||||||
ArgumentNullException.ThrowIfNull(assets);
|
ArgumentNullException.ThrowIfNull(assets);
|
||||||
if (CanonicalLandblock(assets.LandblockId)
|
if (CanonicalLandblock(assets.LandblockId)
|
||||||
!= admission.LandblockId)
|
!= admission.LandblockId)
|
||||||
|
|
@ -898,13 +1022,13 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"A completed collision admission cannot publish more assets.");
|
"A completed collision admission cannot publish more assets.");
|
||||||
}
|
}
|
||||||
if (admission.AssetsAdmitted)
|
if (admission.AssetsPrepared)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Collision assets were already admitted by this receipt.");
|
"Collision assets were already prepared by this receipt.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine.AddLandblock(
|
prepared.Engine.AddLandblock(
|
||||||
admission.LandblockId,
|
admission.LandblockId,
|
||||||
assets.Terrain,
|
assets.Terrain,
|
||||||
assets.CellSurfaces,
|
assets.CellSurfaces,
|
||||||
|
|
@ -914,38 +1038,106 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
if ((assets.CurrentCellId & 0xFFFF0000u)
|
if ((assets.CurrentCellId & 0xFFFF0000u)
|
||||||
== (admission.LandblockId & 0xFFFF0000u))
|
== (admission.LandblockId & 0xFFFF0000u))
|
||||||
{
|
{
|
||||||
Engine.UpdatePlayerCurrCell(assets.CurrentCellId);
|
prepared.Engine.UpdatePlayerCurrCell(assets.CurrentCellId);
|
||||||
}
|
}
|
||||||
admission.AssetsAdmitted = true;
|
admission.AssetsPrepared = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeCollisionAcknowledgement CompleteCollisionAdmission(
|
internal uint[] CaptureCollisionDynamicOwners(
|
||||||
RuntimeCollisionAdmission admission)
|
RuntimeCollisionAdmission admission,
|
||||||
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
{
|
{
|
||||||
ValidateAdmission(admission);
|
ValidateAdmission(admission);
|
||||||
if (!admission.AssetsAdmitted)
|
EnsureCollisionMutationThread();
|
||||||
|
ValidatePreparedGeneration(admission, prepared);
|
||||||
|
return Engine.ShadowObjects.CaptureDynamicRefloodOwnersForLandblock(
|
||||||
|
admission.LandblockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RefreshCollisionDynamicOwner(
|
||||||
|
RuntimeCollisionAdmission admission,
|
||||||
|
PreparedLandblockCollisionGeneration prepared,
|
||||||
|
uint ownerId)
|
||||||
|
{
|
||||||
|
ValidateAdmission(admission);
|
||||||
|
EnsureCollisionMutationThread();
|
||||||
|
ValidatePreparedGeneration(admission, prepared);
|
||||||
|
prepared.RefreshDynamicOwner(ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RuntimeCollisionGenerationCommit CommitCollisionGeneration(
|
||||||
|
RuntimeCollisionAdmission admission,
|
||||||
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
|
{
|
||||||
|
ValidateAdmission(admission);
|
||||||
|
EnsureCollisionMutationThread();
|
||||||
|
ValidatePreparedGeneration(admission, prepared);
|
||||||
|
if (!admission.AssetsPrepared)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Collision admission cannot complete before its assets publish.");
|
"Collision generation cannot commit before its assets are prepared.");
|
||||||
}
|
}
|
||||||
if (admission.Completed)
|
if (admission.Completed)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Collision admission has already completed.");
|
"Collision generation has already completed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint[] dirtyOwners = prepared.FindDirtyDynamicOwners();
|
||||||
|
if (dirtyOwners.Length != 0)
|
||||||
|
{
|
||||||
|
return new RuntimeCollisionGenerationCommit(
|
||||||
|
new RuntimeCollisionAcknowledgement(
|
||||||
|
admission.LandblockId,
|
||||||
|
admission.Generation,
|
||||||
|
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||||
|
Ready: false),
|
||||||
|
dirtyOwners);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
admission.Completed = true;
|
admission.Completed = true;
|
||||||
_collisionAdmissions.Remove(admission.LandblockId);
|
_collisionAdmissions.Remove(admission.LandblockId);
|
||||||
return new RuntimeCollisionAcknowledgement(
|
prepared.MarkCommitted();
|
||||||
|
var acknowledgement = new RuntimeCollisionAcknowledgement(
|
||||||
admission.LandblockId,
|
admission.LandblockId,
|
||||||
admission.Generation,
|
admission.Generation,
|
||||||
Engine.IsLandblockTerrainResident(admission.LandblockId));
|
Engine.IsLandblockTerrainResident(admission.LandblockId),
|
||||||
|
Ready: Engine.IsLandblockTerrainResident(admission.LandblockId));
|
||||||
|
PublishCollisionGenerationCommitted(
|
||||||
|
new RuntimeCollisionGenerationCommitted(
|
||||||
|
acknowledgement.LandblockId,
|
||||||
|
acknowledgement.Generation,
|
||||||
|
acknowledgement.Ready));
|
||||||
|
return new RuntimeCollisionGenerationCommit(
|
||||||
|
acknowledgement,
|
||||||
|
Array.Empty<uint>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeCollisionAcknowledgement DemoteCollisionToTerrain(
|
public RuntimeCollisionAcknowledgement DemoteCollisionToTerrain(
|
||||||
uint landblockId)
|
uint landblockId)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
|
EnsureCollisionMutationThread();
|
||||||
uint canonical = CanonicalLandblock(landblockId);
|
uint canonical = CanonicalLandblock(landblockId);
|
||||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
||||||
InvalidateCollisionAdmission(canonical);
|
InvalidateCollisionAdmission(canonical);
|
||||||
|
|
@ -953,13 +1145,15 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
return new RuntimeCollisionAcknowledgement(
|
return new RuntimeCollisionAcknowledgement(
|
||||||
canonical,
|
canonical,
|
||||||
_collisionGenerations[canonical],
|
_collisionGenerations[canonical],
|
||||||
resident);
|
resident,
|
||||||
|
Ready: Engine.IsLandblockTerrainResident(canonical));
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeCollisionAcknowledgement WithdrawCollision(
|
public RuntimeCollisionAcknowledgement WithdrawCollision(
|
||||||
uint landblockId)
|
uint landblockId)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
|
EnsureCollisionMutationThread();
|
||||||
uint canonical = CanonicalLandblock(landblockId);
|
uint canonical = CanonicalLandblock(landblockId);
|
||||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
||||||
InvalidateCollisionAdmission(canonical);
|
InvalidateCollisionAdmission(canonical);
|
||||||
|
|
@ -967,7 +1161,8 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
return new RuntimeCollisionAcknowledgement(
|
return new RuntimeCollisionAcknowledgement(
|
||||||
canonical,
|
canonical,
|
||||||
_collisionGenerations[canonical],
|
_collisionGenerations[canonical],
|
||||||
resident);
|
resident,
|
||||||
|
Ready: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -981,6 +1176,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_collisionAdmissions.Clear();
|
_collisionAdmissions.Clear();
|
||||||
_collisionGenerations.Clear();
|
_collisionGenerations.Clear();
|
||||||
CellCommitted = null;
|
CellCommitted = null;
|
||||||
|
CollisionGenerationCommitted = null;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1053,6 +1249,20 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
private void EnsureNotDisposed() =>
|
private void EnsureNotDisposed() =>
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
private void EnsureCollisionMutationThread()
|
||||||
|
{
|
||||||
|
int current = Environment.CurrentManagedThreadId;
|
||||||
|
int owner = Interlocked.CompareExchange(
|
||||||
|
ref _collisionMutationThreadId,
|
||||||
|
current,
|
||||||
|
0);
|
||||||
|
if (owner != 0 && owner != current)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Collision generations must be staged and committed on one update thread.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void EnsureCurrent(RuntimeEntityRecord record)
|
private void EnsureCurrent(RuntimeEntityRecord record)
|
||||||
{
|
{
|
||||||
if (!Entities.IsCurrent(record))
|
if (!Entities.IsCurrent(record))
|
||||||
|
|
@ -1081,6 +1291,39 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ValidatePreparedGeneration(
|
||||||
|
RuntimeCollisionAdmission admission,
|
||||||
|
PreparedLandblockCollisionGeneration prepared)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(prepared);
|
||||||
|
ObjectDisposedException.ThrowIf(prepared.IsDisposed, prepared);
|
||||||
|
if (!prepared.Matches(this, admission))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Prepared collision generation is stale or belongs to another admission.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishCollisionGenerationCommitted(
|
||||||
|
RuntimeCollisionGenerationCommitted committed)
|
||||||
|
{
|
||||||
|
Delegate[] observers = CollisionGenerationCommitted?
|
||||||
|
.GetInvocationList() ?? Array.Empty<Delegate>();
|
||||||
|
foreach (Delegate observer in observers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
((Action<RuntimeCollisionGenerationCommitted>)observer)(committed);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Trace.TraceError(
|
||||||
|
"Collision-generation commit observer failed after activation: {0}",
|
||||||
|
error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InvalidateCollisionAdmission(uint landblockId)
|
private void InvalidateCollisionAdmission(uint landblockId)
|
||||||
{
|
{
|
||||||
ulong generation = _collisionGenerations.TryGetValue(
|
ulong generation = _collisionGenerations.TryGetValue(
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,11 @@ public sealed class RuntimePhysicsOwnershipTests
|
||||||
collisionPublisher,
|
collisionPublisher,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"AdmitCollisionAssets(",
|
"StageCollisionAssets(",
|
||||||
|
collisionPublisher,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"CommitCollisionGeneration(",
|
||||||
collisionPublisher,
|
collisionPublisher,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,11 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
Assert.Equal(1, diagnostics.CellSurfaceCount);
|
Assert.Equal(1, diagnostics.CellSurfaceCount);
|
||||||
Assert.Equal(1, diagnostics.PortalPlaneCount);
|
Assert.Equal(1, diagnostics.PortalPlaneCount);
|
||||||
Assert.Equal(1, diagnostics.BuildingCount);
|
Assert.Equal(1, diagnostics.BuildingCount);
|
||||||
|
Assert.Null(fixture.Cache.CellGraph.GetVisible(envCellId));
|
||||||
|
Assert.Empty(fixture.Cache.BuildingIds);
|
||||||
|
|
||||||
|
fixture.Publisher.CompletePublication(receipt);
|
||||||
|
|
||||||
Assert.NotNull(fixture.Cache.CellGraph.GetVisible(envCellId));
|
Assert.NotNull(fixture.Cache.CellGraph.GetVisible(envCellId));
|
||||||
BuildingPhysics building = Assert.Single(
|
BuildingPhysics building = Assert.Single(
|
||||||
fixture.Cache.BuildingIds.Select(id => fixture.Cache.GetBuilding(id)!));
|
fixture.Cache.BuildingIds.Select(id => fixture.Cache.GetBuilding(id)!));
|
||||||
|
|
@ -502,6 +507,11 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
LandblockPhysicsPublication secondReceipt = Begin(
|
LandblockPhysicsPublication secondReceipt = Begin(
|
||||||
fixture.Publisher,
|
fixture.Publisher,
|
||||||
Build(FirstLandblock, [moved]));
|
Build(FirstLandblock, [moved]));
|
||||||
|
ShadowEntry duringPreparation = Assert.Single(
|
||||||
|
fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||||
|
Assert.Equal(
|
||||||
|
first.Position + new Vector3(0f, 0f, 0.6f),
|
||||||
|
duringPreparation.Position);
|
||||||
fixture.Publisher.CompletePublication(secondReceipt);
|
fixture.Publisher.CompletePublication(secondReceipt);
|
||||||
|
|
||||||
Assert.Equal(1, fixture.Engine.LandblockCount);
|
Assert.Equal(1, fixture.Engine.LandblockCount);
|
||||||
|
|
@ -513,6 +523,27 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
Assert.Equal(2, fixture.Publisher.Diagnostics.CompleteCount);
|
Assert.Equal(2, fixture.Publisher.Diagnostics.CompleteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CancelledPublicationDisposesOnlyItsPrivateCollisionGeneration()
|
||||||
|
{
|
||||||
|
var fixture = Fixture();
|
||||||
|
Publish(fixture.Publisher, Build(FirstLandblock));
|
||||||
|
Assert.True(fixture.Engine.IsLandblockTerrainResident(FirstLandblock));
|
||||||
|
|
||||||
|
LandblockPhysicsPublication pending = Begin(
|
||||||
|
fixture.Publisher,
|
||||||
|
Build(AdjacentLandblock));
|
||||||
|
Assert.False(pending.PreparedGeneration.IsDisposed);
|
||||||
|
|
||||||
|
pending.Dispose();
|
||||||
|
|
||||||
|
Assert.True(pending.PreparedGeneration.IsDisposed);
|
||||||
|
Assert.True(fixture.Engine.IsLandblockTerrainResident(FirstLandblock));
|
||||||
|
Assert.False(fixture.Engine.IsLandblockTerrainResident(AdjacentLandblock));
|
||||||
|
Assert.Throws<ObjectDisposedException>(() =>
|
||||||
|
fixture.Publisher.CompletePublication(pending));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NearReapply_RemovesOmittedStaticAcrossSeamAndPreservesNeighborOwner()
|
public void NearReapply_RemovesOmittedStaticAcrossSeamAndPreservesNeighborOwner()
|
||||||
{
|
{
|
||||||
|
|
@ -760,6 +791,17 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
Assert.DoesNotContain("ShadowObjects.RefloodLandblock", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("ShadowObjects.RefloodLandblock", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_physicsEngine.DemoteLandblockToTerrain", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_physicsEngine.DemoteLandblockToTerrain", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_physicsEngine.RemoveLandblock", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_physicsEngine.RemoveLandblock", source, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
string publisherSource = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Streaming",
|
||||||
|
"LandblockPhysicsPublisher.cs"));
|
||||||
|
Assert.DoesNotContain("_physicsDataCache.RemoveCellsForLandblock", publisherSource, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("_physicsDataCache.RemoveBuildingsForLandblock", publisherSource, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("_physicsEngine.ShadowObjects.Reflood", publisherSource, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("CommitCollisionGeneration(", publisherSource, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Publish(
|
private static void Publish(
|
||||||
|
|
|
||||||
|
|
@ -311,27 +311,28 @@ public sealed class RuntimePhysicsStateTests
|
||||||
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
first.Physics.AdmitCollisionAssets(
|
first.Physics.PrepareCollisionGeneration(admission));
|
||||||
admission,
|
|
||||||
CollisionAssets(0xA9B4FFFFu)));
|
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
second.Physics.AdmitCollisionAssets(
|
second.Physics.PrepareCollisionGeneration(newer));
|
||||||
newer,
|
|
||||||
CollisionAssets(0xA9B4FFFFu)));
|
|
||||||
|
|
||||||
first.Physics.AdmitCollisionAssets(
|
using PreparedLandblockCollisionGeneration prepared =
|
||||||
|
first.Physics.PrepareCollisionGeneration(newer);
|
||||||
|
first.Physics.StageCollisionAssets(
|
||||||
newer,
|
newer,
|
||||||
|
prepared,
|
||||||
CollisionAssets(0xA9B4FFFFu));
|
CollisionAssets(0xA9B4FFFFu));
|
||||||
RuntimeCollisionAcknowledgement completed =
|
RuntimeCollisionGenerationCommit commit =
|
||||||
first.Physics.CompleteCollisionAdmission(newer);
|
first.Physics.CommitCollisionGeneration(newer, prepared);
|
||||||
|
RuntimeCollisionAcknowledgement completed = commit.Acknowledgement;
|
||||||
|
|
||||||
|
Assert.True(commit.Committed);
|
||||||
Assert.True(completed.WasResident);
|
Assert.True(completed.WasResident);
|
||||||
Assert.Equal(1, first.Physics.Engine.LandblockCount);
|
Assert.Equal(1, first.Physics.Engine.LandblockCount);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
0,
|
0,
|
||||||
first.Physics.CaptureOwnership().CollisionAdmissionCount);
|
first.Physics.CaptureOwnership().CollisionAdmissionCount);
|
||||||
Assert.Throws<InvalidOperationException>(() =>
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
first.Physics.CompleteCollisionAdmission(newer));
|
first.Physics.CommitCollisionGeneration(newer, prepared));
|
||||||
|
|
||||||
RuntimeCollisionAcknowledgement withdrawn =
|
RuntimeCollisionAcknowledgement withdrawn =
|
||||||
first.Physics.WithdrawCollision(0xA9B4FFFFu);
|
first.Physics.WithdrawCollision(0xA9B4FFFFu);
|
||||||
|
|
@ -340,6 +341,193 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.Equal(0, first.Physics.Engine.LandblockCount);
|
Assert.Equal(0, first.Physics.Engine.LandblockCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollisionGenerationKeepsPreviousWorldVisibleUntilOneCommitNotification()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimePhysicsState physics = lifetime.Physics;
|
||||||
|
RuntimeCollisionAdmission firstAdmission =
|
||||||
|
physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||||
|
using (PreparedLandblockCollisionGeneration first =
|
||||||
|
physics.PrepareCollisionGeneration(firstAdmission))
|
||||||
|
{
|
||||||
|
physics.StageCollisionAssets(
|
||||||
|
firstAdmission,
|
||||||
|
first,
|
||||||
|
CollisionAssets(0xA9B4FFFFu, terrainHeight: 10f));
|
||||||
|
Assert.True(physics.CommitCollisionGeneration(
|
||||||
|
firstAdmission,
|
||||||
|
first).Committed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int notifications = 0;
|
||||||
|
physics.CollisionGenerationCommitted += _ => notifications++;
|
||||||
|
RuntimeCollisionAdmission replacementAdmission =
|
||||||
|
physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||||
|
using PreparedLandblockCollisionGeneration replacement =
|
||||||
|
physics.PrepareCollisionGeneration(replacementAdmission);
|
||||||
|
physics.StageCollisionAssets(
|
||||||
|
replacementAdmission,
|
||||||
|
replacement,
|
||||||
|
CollisionAssets(0xA9B4FFFFu, terrainHeight: 25f));
|
||||||
|
|
||||||
|
Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||||
|
Assert.Equal(0, notifications);
|
||||||
|
|
||||||
|
RuntimeCollisionGenerationCommit committed =
|
||||||
|
physics.CommitCollisionGeneration(
|
||||||
|
replacementAdmission,
|
||||||
|
replacement);
|
||||||
|
|
||||||
|
Assert.True(committed.Committed);
|
||||||
|
Assert.Equal(25f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||||
|
Assert.Equal(1, notifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollisionGenerationRejectsStaleReplacementWithoutMutatingActiveWorld()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimePhysicsState physics = lifetime.Physics;
|
||||||
|
RuntimeCollisionAdmission stale =
|
||||||
|
physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||||
|
using PreparedLandblockCollisionGeneration preparedStale =
|
||||||
|
physics.PrepareCollisionGeneration(stale);
|
||||||
|
physics.StageCollisionAssets(
|
||||||
|
stale,
|
||||||
|
preparedStale,
|
||||||
|
CollisionAssets(0xA9B4FFFFu, terrainHeight: 10f));
|
||||||
|
|
||||||
|
RuntimeCollisionAdmission current =
|
||||||
|
physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
physics.CommitCollisionGeneration(stale, preparedStale));
|
||||||
|
Assert.False(physics.Engine.IsLandblockTerrainResident(0xA9B4FFFFu));
|
||||||
|
|
||||||
|
using PreparedLandblockCollisionGeneration preparedCurrent =
|
||||||
|
physics.PrepareCollisionGeneration(current);
|
||||||
|
physics.StageCollisionAssets(
|
||||||
|
current,
|
||||||
|
preparedCurrent,
|
||||||
|
CollisionAssets(0xA9B4FFFFu, terrainHeight: 20f));
|
||||||
|
Assert.True(physics.CommitCollisionGeneration(
|
||||||
|
current,
|
||||||
|
preparedCurrent).Committed);
|
||||||
|
Assert.Equal(20f, physics.Engine.SampleTerrainZ(1f, 1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MovingDynamicOwnerDuringStagingMustRefreshBeforeCommit()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimePhysicsState physics = lifetime.Physics;
|
||||||
|
RuntimeCollisionAdmission initialAdmission =
|
||||||
|
physics.BeginCollisionAdmission(0x0101FFFFu);
|
||||||
|
using (PreparedLandblockCollisionGeneration initial =
|
||||||
|
physics.PrepareCollisionGeneration(initialAdmission))
|
||||||
|
{
|
||||||
|
physics.StageCollisionAssets(
|
||||||
|
initialAdmission,
|
||||||
|
initial,
|
||||||
|
CollisionAssets(0x0101FFFFu, terrainHeight: 5f));
|
||||||
|
Assert.True(physics.CommitCollisionGeneration(
|
||||||
|
initialAdmission,
|
||||||
|
initial).Committed);
|
||||||
|
}
|
||||||
|
physics.Engine.ShadowObjects.Register(
|
||||||
|
42u,
|
||||||
|
0x01000001u,
|
||||||
|
new Vector3(10f, 10f, 0f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
0.5f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0x0101FFFFu,
|
||||||
|
seedCellId: 0x01010001u,
|
||||||
|
isStatic: false);
|
||||||
|
|
||||||
|
RuntimeCollisionAdmission admission =
|
||||||
|
physics.BeginCollisionAdmission(0x0101FFFFu);
|
||||||
|
using PreparedLandblockCollisionGeneration prepared =
|
||||||
|
physics.PrepareCollisionGeneration(admission);
|
||||||
|
physics.StageCollisionAssets(
|
||||||
|
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);
|
||||||
|
|
||||||
|
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.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpawnAndDeleteDuringStagingAreBothGenerationGated()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimePhysicsState physics = lifetime.Physics;
|
||||||
|
RuntimeCollisionAdmission admission =
|
||||||
|
physics.BeginCollisionAdmission(0x0101FFFFu);
|
||||||
|
using PreparedLandblockCollisionGeneration prepared =
|
||||||
|
physics.PrepareCollisionGeneration(admission);
|
||||||
|
physics.StageCollisionAssets(
|
||||||
|
admission,
|
||||||
|
prepared,
|
||||||
|
CollisionAssets(0x0101FFFFu, terrainHeight: 12f));
|
||||||
|
Assert.Empty(physics.CaptureCollisionDynamicOwners(admission, prepared));
|
||||||
|
|
||||||
|
physics.Engine.ShadowObjects.Register(
|
||||||
|
77u,
|
||||||
|
0x01000001u,
|
||||||
|
new Vector3(10f, 10f, 0f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
0.5f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0x0101FFFFu,
|
||||||
|
seedCellId: 0x01010001u,
|
||||||
|
isStatic: false);
|
||||||
|
RuntimeCollisionGenerationCommit spawned =
|
||||||
|
physics.CommitCollisionGeneration(admission, prepared);
|
||||||
|
Assert.False(spawned.Committed);
|
||||||
|
Assert.Equal(77u, Assert.Single(spawned.DirtyDynamicOwnerIds));
|
||||||
|
|
||||||
|
physics.RefreshCollisionDynamicOwner(admission, prepared, 77u);
|
||||||
|
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.True(physics.CommitCollisionGeneration(
|
||||||
|
admission,
|
||||||
|
prepared).Committed);
|
||||||
|
Assert.Empty(physics.Engine.ShadowObjects.AllEntriesForDebug());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TerminalDisposalClearsLandblocksShadowsAndWorksets()
|
public void TerminalDisposalClearsLandblocksShadowsAndWorksets()
|
||||||
{
|
{
|
||||||
|
|
@ -352,10 +540,13 @@ public sealed class RuntimePhysicsStateTests
|
||||||
|
|
||||||
RuntimeCollisionAdmission admission =
|
RuntimeCollisionAdmission admission =
|
||||||
lifetime.Physics.BeginCollisionAdmission(0x0101FFFFu);
|
lifetime.Physics.BeginCollisionAdmission(0x0101FFFFu);
|
||||||
lifetime.Physics.AdmitCollisionAssets(
|
using PreparedLandblockCollisionGeneration prepared =
|
||||||
|
lifetime.Physics.PrepareCollisionGeneration(admission);
|
||||||
|
lifetime.Physics.StageCollisionAssets(
|
||||||
admission,
|
admission,
|
||||||
|
prepared,
|
||||||
CollisionAssets(0x0101FFFFu));
|
CollisionAssets(0x0101FFFFu));
|
||||||
_ = lifetime.Physics.CompleteCollisionAdmission(admission);
|
_ = lifetime.Physics.CommitCollisionGeneration(admission, prepared);
|
||||||
lifetime.Physics.Engine.ShadowObjects.Register(
|
lifetime.Physics.Engine.ShadowObjects.Register(
|
||||||
entityId: record.LocalEntityId!.Value,
|
entityId: record.LocalEntityId!.Value,
|
||||||
gfxObjId: 0x01000001u,
|
gfxObjId: 0x01000001u,
|
||||||
|
|
@ -740,10 +931,12 @@ public sealed class RuntimePhysicsStateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
||||||
uint landblockId)
|
uint landblockId,
|
||||||
|
float terrainHeight = 0f)
|
||||||
{
|
{
|
||||||
var heights = new byte[81];
|
var heights = new byte[81];
|
||||||
var table = new float[256];
|
var table = new float[256];
|
||||||
|
table[0] = terrainHeight;
|
||||||
return new RuntimeLandblockCollisionAssets(
|
return new RuntimeLandblockCollisionAssets(
|
||||||
landblockId,
|
landblockId,
|
||||||
new TerrainSurface(heights, table),
|
new TerrainSurface(heights, table),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue