wip(physics): collision O(changed) delta-commit (O1-O3) - ON HOLD, feel-test failed
Publication-throughput rework per the D2 design (docs/research/ 2026-08-02-collision-throughput-handoff/design-note.md): O1 per-prefix installed-key ledgers replacing the seal's full-map scans; O2 per- landblock delta commit (LandblockReplacementApplyCursor against the active root) replacing whole-world TransferTo; O3 empty staging root, commit-time reflood (CObjCell::init_objects 0x0052B420 -> recalc_cross_cells 0x00515A30), journal/peer-rebase machinery deleted (~1,900 lines net). Automated gates green: Runtime 999, Core physics 2,135, App 4,039/3, Headless 79, complete solution 10,812/0/4; lifecycle gate PASS (connected-world-gate-20260802-193029). Soak 194423: publication-side acceptance fully met (37 -> 4 failures, all convergence dims zero, loadedLandblocks baseline-identical, waitCue 6/9 -> 1/9). COMMITTED AS WIP ON USER DIRECTION - NOT ACCEPTED. The user feel-test FAILED on this tree: monsters still pop into existence at close range, monsters spawned mid-air far ahead, static placements visibly wrong, plus 243x "Landblock already has a full retirement receipt" InvalidOperationException catch-retry loop during origin recenter (launch-feeltest-oclone.log). The 4 remaining soak failures (pendingLandblockRetirements 131/122 at the Caul->Sawato stops) and the implementer's "exposed pre-existing" classification are under re-judgment against that loop. Dual reviews were dispatched and then stopped mid-flight on user direction; NO review has passed this commit. Full problem inventory + next-agent instructions: docs/research/2026-08-02-collision-throughput-handoff/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c52ce14a07
commit
71604331cf
13 changed files with 6410 additions and 1894 deletions
|
|
@ -249,20 +249,22 @@ public sealed class PhysicsEngine
|
|||
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.
|
||||
/// Creates the empty off-side staging root for one landblock collision
|
||||
/// generation. O3 (2026-08-02): admission no longer materializes a clone
|
||||
/// of the resident world — the staging root holds ONLY the target
|
||||
/// landblock's authored content and the commit installs it into the
|
||||
/// active root as a per-landblock delta whose owner refloods run against
|
||||
/// the live world (retail <c>CObjCell::init_objects</c> 0x0052B420 →
|
||||
/// <c>CPhysicsObj::recalc_cross_cells</c> 0x00515A30).
|
||||
/// </summary>
|
||||
internal CollisionStagingBuilder CreateCollisionStagingBuilder(
|
||||
uint targetLandblockId)
|
||||
{
|
||||
_ = targetLandblockId;
|
||||
PhysicsDataCache activeCache = DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Active collision engine has no data cache.");
|
||||
return new CollisionStagingBuilder(
|
||||
this,
|
||||
activeCache,
|
||||
targetLandblockId & 0xFFFF0000u);
|
||||
return new CollisionStagingBuilder(this, activeCache);
|
||||
}
|
||||
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
|
|
@ -301,6 +303,20 @@ public sealed class PhysicsEngine
|
|||
expectedRetainedOwners));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes one sealed landblock replacement into the ACTIVE collision
|
||||
/// root as a per-landblock delta drained in this one synchronous
|
||||
/// update-thread call — the O2 (2026-08-02) restoration of be94bc9b's
|
||||
/// O(changed) commit, replacing the whole-root
|
||||
/// <c>CollisionWorldStateSlot.TransferTo</c> swap. Retail hydrates one
|
||||
/// cell synchronously and refloods the objects associated with it
|
||||
/// (<c>CObjCell::init_objects</c> 0x0052B420 →
|
||||
/// <c>CPhysicsObj::recalc_cross_cells</c> 0x00515A30); the streaming
|
||||
/// analogue is one landblock delta applied atomically with respect to
|
||||
/// every reader (the runtime is single-threaded and the caller holds the
|
||||
/// prefix quiescence permission). Owner rows install from the sealed
|
||||
/// staging registry, which the seal keeps exactly current.
|
||||
/// </summary>
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedPhysicsEngineLandblock replacement)
|
||||
{
|
||||
|
|
@ -310,14 +326,59 @@ public sealed class PhysicsEngine
|
|||
PhysicsDataCache stagingCache = replacement.Staging.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Staging collision engine has no data cache.");
|
||||
uint activeCurrentCellId = activeCache.CellGraph.CurrCell?.Id ?? 0u;
|
||||
stagingCache.CollisionWorld.TransferTo(activeCache.CollisionWorld);
|
||||
if ((activeCurrentCellId & 0xFFFF0000u)
|
||||
== (replacement.LandblockId & 0xFFFF0000u))
|
||||
ShadowObjectRegistry stagingShadows = replacement.Staging.ShadowObjects;
|
||||
CollisionWorldState active = activeCache.CollisionWorld.Current;
|
||||
|
||||
// Rows in retired target cells belong exclusively to owners in the
|
||||
// sealed owner list (every owner with target-prefix rows is captured
|
||||
// there), so dropping the retired cells' row lists first can never
|
||||
// discard a row the owner installs below.
|
||||
PreparedPhysicsDataCacheLandblock data = replacement.DataCache;
|
||||
for (int index = 0; index < data.CellIdsToRemove.Count; index++)
|
||||
active.ShadowCells.Remove(data.CellIdsToRemove[index]);
|
||||
IReadOnlyList<uint> envCellRemovals =
|
||||
data.CellGraph.EnvCellIdsToRemove;
|
||||
for (int index = 0; index < envCellRemovals.Count; index++)
|
||||
active.ShadowCells.Remove(envCellRemovals[index]);
|
||||
|
||||
using (LandblockReplacementApplyCursor cursor =
|
||||
CreateLandblockReplacementApplyCursor(replacement))
|
||||
{
|
||||
activeCache.CellGraph.CurrCell =
|
||||
activeCache.CellGraph.GetVisible(activeCurrentCellId);
|
||||
while (true)
|
||||
{
|
||||
LandblockReplacementApplyStep step = cursor.Advance();
|
||||
if (step.HasOwner)
|
||||
{
|
||||
// O3 (2026-08-02): retail's per-cell hydration suffix —
|
||||
// adopt each staged owner and recalculate its cross-cells
|
||||
// against the live post-delta world, retire the outgoing
|
||||
// generation's authored statics, and re-run the flood for
|
||||
// retained live owners touching the replaced landblock
|
||||
// (CObjCell::init_objects 0x0052B420 →
|
||||
// CPhysicsObj::recalc_cross_cells 0x00515A30).
|
||||
ShadowObjects.ApplyCommittedOwnerReplacement(
|
||||
stagingShadows,
|
||||
step.OwnerId,
|
||||
replacement.LandblockId);
|
||||
}
|
||||
if (step.Completed)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Retail init_objects refloods every object associated with the
|
||||
// hydrated cell at hydration time. Owners that became associated with
|
||||
// the target after the sealed capture (a mover entering the prefix
|
||||
// mid-publication) are in the live prefix-owner slots but not the
|
||||
// sealed list; recalculate their cross-cells here too.
|
||||
ShadowObjects.RefloodPrefixOwnersAfterReplacement(
|
||||
replacement.LandblockId,
|
||||
replacement.Shadows.OwnerIds);
|
||||
|
||||
// The staging root no longer becomes the active root, but a committed
|
||||
// preparation must still lose its private world exactly as TransferTo
|
||||
// revoked it.
|
||||
stagingCache.CollisionWorld.Revoke();
|
||||
}
|
||||
|
||||
internal LandblockReplacementApplyCursor
|
||||
|
|
@ -331,239 +392,12 @@ public sealed class PhysicsEngine
|
|||
bool HasOwner,
|
||||
uint OwnerId);
|
||||
|
||||
internal LandblockRetirementCursor CreateLandblockRetirementCursor(
|
||||
PhysicsEngine authoritative,
|
||||
uint landblockId,
|
||||
bool withdraw) => new(
|
||||
this,
|
||||
authoritative,
|
||||
landblockId,
|
||||
withdraw);
|
||||
|
||||
/// <summary>
|
||||
/// Applies one demotion/withdrawal to an off-side root without a whole-
|
||||
/// world synchronous scan. Every advance inspects or mutates at most one
|
||||
/// stable owner slot, dictionary leaf, or authored outdoor cell.
|
||||
/// </summary>
|
||||
internal sealed class LandblockRetirementCursor : IDisposable
|
||||
{
|
||||
private readonly PhysicsEngine _destinationEngine;
|
||||
private readonly PhysicsDataCache _destinationCache;
|
||||
private readonly CollisionWorldState _destination;
|
||||
private readonly CollisionWorldState _authoritative;
|
||||
private readonly uint _canonical;
|
||||
private readonly uint _prefix;
|
||||
private readonly bool _withdraw;
|
||||
private readonly List<uint> _ownerSlots;
|
||||
private readonly int _ownerSlotLimit;
|
||||
private readonly LandblockPhysics? _demotedLandblock;
|
||||
private readonly CellGraphTerrain? _demotedTerrain;
|
||||
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>?
|
||||
_flatCells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvCells;
|
||||
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildings;
|
||||
private IEnumerator<KeyValuePair<uint, EnvCell>>? _envCells;
|
||||
private int _ownerIndex;
|
||||
private int _outdoorIndex;
|
||||
private int _phase;
|
||||
|
||||
internal LandblockRetirementCursor(
|
||||
PhysicsEngine destination,
|
||||
PhysicsEngine authoritative,
|
||||
uint landblockId,
|
||||
bool withdraw)
|
||||
{
|
||||
_destinationEngine = destination;
|
||||
_destinationCache = destination.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Collision engine has no data cache.");
|
||||
_destination = destination._collisionWorld.Capture();
|
||||
_authoritative = authoritative._collisionWorld.Capture();
|
||||
_canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
_prefix = landblockId & 0xFFFF0000u;
|
||||
_withdraw = withdraw;
|
||||
_ownerSlots = _destination.ShadowOwnerSlots;
|
||||
_ownerSlotLimit = _ownerSlots.Count;
|
||||
if (!withdraw)
|
||||
{
|
||||
_authoritative.Landblocks.TryGetValue(
|
||||
_canonical,
|
||||
out _demotedLandblock);
|
||||
_authoritative.Terrain.TryGetValue(
|
||||
_prefix,
|
||||
out _demotedTerrain);
|
||||
}
|
||||
}
|
||||
|
||||
internal uint LandblockId => _canonical;
|
||||
|
||||
internal LandblockRetirementStep Advance()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (_ownerIndex < _ownerSlotLimit)
|
||||
{
|
||||
uint ownerId = _ownerSlots[_ownerIndex++];
|
||||
if (ownerId != 0u)
|
||||
{
|
||||
_destinationEngine.ShadowObjects
|
||||
.RetireOwnerFromLandblock(
|
||||
ownerId,
|
||||
_canonical);
|
||||
}
|
||||
return Worked();
|
||||
}
|
||||
_phase++;
|
||||
continue;
|
||||
case 1:
|
||||
_cells ??= _destination.CellStruct.GetEnumerator();
|
||||
if (RemoveOneInPrefix(_cells, _destination.CellStruct))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _cells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 2:
|
||||
_flatCells ??= _destination.FlatCellStruct.GetEnumerator();
|
||||
if (RemoveOneInPrefix(
|
||||
_flatCells,
|
||||
_destination.FlatCellStruct))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 3:
|
||||
_flatEnvCells ??= _destination.FlatEnvCell.GetEnumerator();
|
||||
if (RemoveOneInPrefix(
|
||||
_flatEnvCells,
|
||||
_destination.FlatEnvCell))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 4:
|
||||
_buildings ??= _destination.Buildings.GetEnumerator();
|
||||
if (RemoveOneInPrefix(
|
||||
_buildings,
|
||||
_destination.Buildings))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _buildings);
|
||||
_phase++;
|
||||
continue;
|
||||
case 5:
|
||||
_envCells ??= _destination.EnvCells.GetEnumerator();
|
||||
if (RemoveOneInPrefix(_envCells, _destination.EnvCells))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _envCells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 6:
|
||||
if (_outdoorIndex < 0x40)
|
||||
{
|
||||
uint id = _prefix | (uint)++_outdoorIndex;
|
||||
_destination.ShadowCells.Remove(id);
|
||||
if (_withdraw)
|
||||
{
|
||||
_destination.OutdoorCells.TryRemove(id, out _);
|
||||
}
|
||||
else if (_authoritative.OutdoorCells.TryGetValue(
|
||||
id,
|
||||
out ObjCell? outdoor))
|
||||
{
|
||||
_destination.OutdoorCells[id] = outdoor;
|
||||
}
|
||||
return Worked();
|
||||
}
|
||||
_phase++;
|
||||
continue;
|
||||
case 7:
|
||||
if (_withdraw)
|
||||
{
|
||||
_destinationEngine._landblocks.Remove(_canonical);
|
||||
_destinationEngine.RemoveLandblockSlot(_canonical);
|
||||
_destination.Terrain.TryRemove(_prefix, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_demotedLandblock is not null)
|
||||
{
|
||||
_destinationEngine._landblocks[_canonical] =
|
||||
_demotedLandblock;
|
||||
_destinationEngine.EnsureLandblockSlot(_canonical);
|
||||
}
|
||||
if (_demotedTerrain is not null)
|
||||
_destination.Terrain[_prefix] = _demotedTerrain;
|
||||
}
|
||||
_phase++;
|
||||
return Worked();
|
||||
case 8:
|
||||
uint currentCellId =
|
||||
_destinationCache.CellGraph.CurrCell?.Id ?? 0u;
|
||||
if ((currentCellId & 0xFFFF0000u) == _prefix
|
||||
&& (_withdraw
|
||||
|| (currentCellId & 0xFFFFu) >= 0x0100u))
|
||||
{
|
||||
_destinationCache.CellGraph.CurrCell = null;
|
||||
}
|
||||
_phase++;
|
||||
return new LandblockRetirementStep(
|
||||
Completed: true,
|
||||
Worked: false);
|
||||
default:
|
||||
return new LandblockRetirementStep(
|
||||
Completed: true,
|
||||
Worked: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LandblockRetirementStep Worked() => new(
|
||||
Completed: false,
|
||||
Worked: true);
|
||||
|
||||
private bool RemoveOneInPrefix<T>(
|
||||
IEnumerator<KeyValuePair<uint, T>> source,
|
||||
IDictionary<uint, T> destination)
|
||||
{
|
||||
if (!source.MoveNext())
|
||||
return false;
|
||||
uint id = source.Current.Key;
|
||||
if ((id & 0xFFFF0000u) == _prefix)
|
||||
{
|
||||
destination.Remove(id);
|
||||
_destination.ShadowCells.Remove(id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void DisposeEnumerator<T>(
|
||||
ref IEnumerator<KeyValuePair<uint, T>>? enumerator)
|
||||
{
|
||||
enumerator?.Dispose();
|
||||
enumerator = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeEnumerator(ref _cells);
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
DisposeEnumerator(ref _buildings);
|
||||
DisposeEnumerator(ref _envCells);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct LandblockRetirementStep(
|
||||
bool Completed,
|
||||
bool Worked);
|
||||
|
||||
/// <summary>
|
||||
/// Applies one already-committed landblock delta to a later off-side root.
|
||||
/// Each advance mutates at most one dictionary leaf, one synthesized
|
||||
/// outdoor cell, or one logical shadow owner.
|
||||
/// Applies one sealed landblock delta to the active collision root. Each
|
||||
/// advance mutates at most one dictionary leaf, one synthesized outdoor
|
||||
/// cell, or yields one logical shadow owner to the committing caller.
|
||||
/// <see cref="CommitLandblockReplacement"/> drains it in one synchronous
|
||||
/// update-thread call.
|
||||
/// </summary>
|
||||
internal sealed class LandblockReplacementApplyCursor : IDisposable
|
||||
{
|
||||
|
|
@ -597,48 +431,88 @@ public sealed class PhysicsEngine
|
|||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (RemoveOne(_destination.CellStruct, data.CellIdsToRemove))
|
||||
if (_index < data.CellIdsToRemove.Count)
|
||||
{
|
||||
_destination.RemoveCellStruct(
|
||||
data.CellIdsToRemove[_index++]);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 1:
|
||||
if (InstallOne(_destination.CellStruct, data.Cells))
|
||||
if (_index < data.Cells.Count)
|
||||
{
|
||||
KeyValuePair<uint, CellPhysics> pair =
|
||||
data.Cells[_index++];
|
||||
_destination.SetCellStruct(pair.Key, pair.Value);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 2:
|
||||
if (RemoveOne(_destination.FlatCellStruct, data.FlatCellIdsToRemove))
|
||||
if (_index < data.FlatCellIdsToRemove.Count)
|
||||
{
|
||||
_destination.RemoveFlatCellStruct(
|
||||
data.FlatCellIdsToRemove[_index++]);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 3:
|
||||
if (InstallOne(_destination.FlatCellStruct, data.FlatCells))
|
||||
if (_index < data.FlatCells.Count)
|
||||
{
|
||||
KeyValuePair<uint, FlatCellStructureCollisionAsset>
|
||||
pair = data.FlatCells[_index++];
|
||||
_destination.SetFlatCellStruct(pair.Key, pair.Value);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 4:
|
||||
if (RemoveOne(_destination.FlatEnvCell, data.FlatEnvCellIdsToRemove))
|
||||
if (_index < data.FlatEnvCellIdsToRemove.Count)
|
||||
{
|
||||
_destination.RemoveFlatEnvCell(
|
||||
data.FlatEnvCellIdsToRemove[_index++]);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 5:
|
||||
if (InstallOne(_destination.FlatEnvCell, data.FlatEnvCells))
|
||||
if (_index < data.FlatEnvCells.Count)
|
||||
{
|
||||
KeyValuePair<uint, FlatEnvCellTopology> pair =
|
||||
data.FlatEnvCells[_index++];
|
||||
_destination.SetFlatEnvCell(pair.Key, pair.Value);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 6:
|
||||
if (RemoveOne(_destination.Buildings, data.BuildingIdsToRemove))
|
||||
if (_index < data.BuildingIdsToRemove.Count)
|
||||
{
|
||||
_destination.RemoveBuilding(
|
||||
data.BuildingIdsToRemove[_index++]);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 7:
|
||||
if (InstallOne(_destination.Buildings, data.Buildings))
|
||||
if (_index < data.Buildings.Count)
|
||||
{
|
||||
KeyValuePair<uint, BuildingPhysics> pair =
|
||||
data.Buildings[_index++];
|
||||
_destination.SetBuilding(pair.Key, pair.Value);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 8:
|
||||
if (RemoveOne(_destination.EnvCells, graph.EnvCellIdsToRemove))
|
||||
if (_index < graph.EnvCellIdsToRemove.Count)
|
||||
{
|
||||
_destination.RemoveEnvCell(
|
||||
graph.EnvCellIdsToRemove[_index++]);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 9:
|
||||
|
|
@ -689,8 +563,13 @@ public sealed class PhysicsEngine
|
|||
NextPhase();
|
||||
continue;
|
||||
case 10:
|
||||
if (InstallOne(_destination.EnvCells, graph.EnvCells))
|
||||
if (_index < graph.EnvCells.Count)
|
||||
{
|
||||
KeyValuePair<uint, EnvCell> pair =
|
||||
graph.EnvCells[_index++];
|
||||
_destination.SetEnvCell(pair.Key, pair.Value);
|
||||
return Worked();
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 11:
|
||||
|
|
@ -750,67 +629,30 @@ public sealed class PhysicsEngine
|
|||
_index = 0;
|
||||
}
|
||||
|
||||
private bool RemoveOne<T>(
|
||||
IDictionary<uint, T> destination,
|
||||
IReadOnlyList<uint> ids)
|
||||
{
|
||||
if (_index >= ids.Count)
|
||||
return false;
|
||||
destination.Remove(ids[_index++]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool InstallOne<T>(
|
||||
IDictionary<uint, T> destination,
|
||||
IReadOnlyList<KeyValuePair<uint, T>> entries)
|
||||
{
|
||||
if (_index >= entries.Count)
|
||||
return false;
|
||||
KeyValuePair<uint, T> pair = entries[_index++];
|
||||
destination[pair.Key] = pair.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retained one-leaf-at-a-time materializer for an off-side collision
|
||||
/// generation. Construction captures the current root reference only; no
|
||||
/// resident dictionary is copied until <see cref="Advance"/>. Runtime's
|
||||
/// owner journal reconciles mutations that occur while this cursor walks.
|
||||
/// O3 (2026-08-02): the empty off-side staging root for one landblock
|
||||
/// collision generation. The pre-O3 builder materialized a whole-world
|
||||
/// clone one leaf per host step; that clone existed only so the old
|
||||
/// whole-root activation swap and the staged retained-owner refloods had
|
||||
/// a complete world to stand on. With the per-landblock delta commit and
|
||||
/// commit-time refloods against the live world (retail
|
||||
/// <c>CObjCell::init_objects</c> 0x0052B420 →
|
||||
/// <c>CPhysicsObj::recalc_cross_cells</c> 0x00515A30), admission is O(1)
|
||||
/// and the staging root holds only the target landblock's authored
|
||||
/// content. Immutable GfxObj/Setup catalogs still read through to the
|
||||
/// active cache via the staging cache's read fallback.
|
||||
/// </summary>
|
||||
internal sealed class CollisionStagingBuilder : IDisposable
|
||||
{
|
||||
private readonly PhysicsEngine _active;
|
||||
private readonly CollisionWorldState _source;
|
||||
private readonly CollisionWorldState _destination;
|
||||
private readonly ShadowObjectRegistry _sourceShadows;
|
||||
private readonly uint _targetPrefix;
|
||||
private readonly HashSet<uint> _suppressedPrefixes = new();
|
||||
private readonly int _landblockSlotLimit;
|
||||
private readonly int _ownerSlotLimit;
|
||||
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>? _flatCells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvCells;
|
||||
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildings;
|
||||
private IEnumerator<KeyValuePair<uint, EnvCell>>? _envCells;
|
||||
private IEnumerator<KeyValuePair<uint, CellGraphTerrain>>? _terrain;
|
||||
private IEnumerator<KeyValuePair<uint, ObjCell>>? _outdoorCells;
|
||||
private int _landblockIndex;
|
||||
private int _ownerIndex;
|
||||
private int _phase;
|
||||
|
||||
internal CollisionStagingBuilder(
|
||||
PhysicsEngine active,
|
||||
PhysicsDataCache activeCache,
|
||||
uint targetPrefix)
|
||||
PhysicsDataCache activeCache)
|
||||
{
|
||||
_active = active;
|
||||
_targetPrefix = targetPrefix;
|
||||
_source = active._collisionWorld.Capture();
|
||||
var stagingSlot = new CollisionWorldStateSlot();
|
||||
StagingCache = activeCache.CreateEmptyCollisionStaging(stagingSlot);
|
||||
StagingEngine = new PhysicsEngine
|
||||
|
|
@ -818,170 +660,13 @@ public sealed class PhysicsEngine
|
|||
DataCache = StagingCache,
|
||||
Objects = active.Objects,
|
||||
};
|
||||
_destination = stagingSlot.Capture();
|
||||
_sourceShadows = new ShadowObjectRegistry(
|
||||
new CollisionWorldStateSlot(_source));
|
||||
_landblockSlotLimit = _source.LandblockSlots.Count;
|
||||
_ownerSlotLimit = _source.ShadowOwnerSlots.Count;
|
||||
}
|
||||
|
||||
internal PhysicsDataCache StagingCache { get; }
|
||||
internal PhysicsEngine StagingEngine { get; }
|
||||
internal int WorkUnits { get; private set; }
|
||||
internal bool Completed => _phase == 9;
|
||||
|
||||
/// <summary>
|
||||
/// Prevent a landblock retired after this cursor captured its source
|
||||
/// root from being copied back into the draft by a later phase.
|
||||
/// Already-copied leaves are retired by the caller before cloning
|
||||
/// resumes; this tombstone covers every leaf not visited yet.
|
||||
/// </summary>
|
||||
internal void SuppressLandblock(uint landblockId) =>
|
||||
_suppressedPrefixes.Add(landblockId & 0xFFFF0000u);
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (_landblockIndex < _landblockSlotLimit)
|
||||
{
|
||||
uint id = _source.LandblockSlots[_landblockIndex++];
|
||||
if (id != 0u
|
||||
&& (id & 0xFFFF0000u) != _targetPrefix
|
||||
&& !_suppressedPrefixes.Contains(
|
||||
id & 0xFFFF0000u)
|
||||
&& _source.Landblocks.TryGetValue(
|
||||
id,
|
||||
out LandblockPhysics? landblock))
|
||||
{
|
||||
StagingEngine.InstallLandblockClone(id, landblock);
|
||||
}
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_phase++;
|
||||
return false;
|
||||
case 1:
|
||||
_cells ??= _source.CellStruct.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_cells, _destination.CellStruct))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _cells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 2:
|
||||
_flatCells ??= _source.FlatCellStruct.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_flatCells, _destination.FlatCellStruct))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 3:
|
||||
_flatEnvCells ??= _source.FlatEnvCell.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_flatEnvCells, _destination.FlatEnvCell))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 4:
|
||||
_buildings ??= _source.Buildings.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_buildings, _destination.Buildings))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _buildings);
|
||||
_phase++;
|
||||
return false;
|
||||
case 5:
|
||||
_envCells ??= _source.EnvCells.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_envCells, _destination.EnvCells))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _envCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 6:
|
||||
_terrain ??= _source.Terrain.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_terrain, _destination.Terrain))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _terrain);
|
||||
_phase++;
|
||||
return false;
|
||||
case 7:
|
||||
_outdoorCells ??= _source.OutdoorCells.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_outdoorCells, _destination.OutdoorCells))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _outdoorCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 8:
|
||||
if (_ownerIndex < _ownerSlotLimit)
|
||||
{
|
||||
uint ownerId = _source.ShadowOwnerSlots[_ownerIndex++];
|
||||
if (ownerId != 0u
|
||||
&& !_sourceShadows.IsStaticOwnerRootedIn(
|
||||
ownerId,
|
||||
_targetPrefix)
|
||||
&& !IsSuppressedStaticOwner(ownerId)
|
||||
&& !StagingEngine.ShadowObjects.HasLogicalOwner(
|
||||
ownerId))
|
||||
{
|
||||
StagingEngine.ShadowObjects.MirrorOwnerFrom(
|
||||
_sourceShadows,
|
||||
ownerId);
|
||||
}
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
uint currentCellId = _active.DataCache?.CellGraph.CurrCell?.Id ?? 0u;
|
||||
StagingCache.CellGraph.CurrCell =
|
||||
StagingCache.CellGraph.GetVisible(currentCellId);
|
||||
_phase++;
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CountOne()
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CopyOneOutsideTarget<T>(
|
||||
IEnumerator<KeyValuePair<uint, T>> source,
|
||||
IDictionary<uint, T> destination)
|
||||
{
|
||||
if (!source.MoveNext())
|
||||
return false;
|
||||
KeyValuePair<uint, T> pair = source.Current;
|
||||
uint prefix = pair.Key & 0xFFFF0000u;
|
||||
if (prefix != _targetPrefix
|
||||
&& !_suppressedPrefixes.Contains(prefix))
|
||||
destination[pair.Key] = pair.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsSuppressedStaticOwner(uint ownerId)
|
||||
=> _sourceShadows.TryGetStaticOwnerRootPrefix(
|
||||
ownerId,
|
||||
out uint prefix)
|
||||
&& _suppressedPrefixes.Contains(prefix);
|
||||
|
||||
private static void DisposeEnumerator<T>(
|
||||
ref IEnumerator<KeyValuePair<uint, T>>? enumerator)
|
||||
{
|
||||
enumerator?.Dispose();
|
||||
enumerator = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeEnumerator(ref _cells);
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
DisposeEnumerator(ref _buildings);
|
||||
DisposeEnumerator(ref _envCells);
|
||||
DisposeEnumerator(ref _terrain);
|
||||
DisposeEnumerator(ref _outdoorCells);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue