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:
Erik 2026-08-02 20:06:59 +02:00
parent c52ce14a07
commit 71604331cf
13 changed files with 6410 additions and 1894 deletions

View file

@ -3,6 +3,81 @@ using AcDream.Core.World.Cells;
namespace AcDream.Core.Physics;
/// <summary>
/// Per-landblock installed-key ledger for one collision-world map. Slot lists
/// mirror the shadow registry's prefix-owner-slot idiom: removal tombstones a
/// slot (key 0) so an in-flight metered cursor retains its captured list
/// reference and observes only tombstones, and an emptied container is
/// reclaimed so a future install gets a fresh compact list. Maintained by the
/// <see cref="CollisionWorldState"/> typed install/remove helpers; consumed by
/// the landblock-replacement seal so capturing one prefix's keys never scans
/// the whole resident world (O1 of the 2026-08-02 collision
/// publication-throughput fix).
/// </summary>
internal sealed class PrefixKeyIndex
{
private readonly Dictionary<uint, List<uint>> _slots = new();
private readonly Dictionary<uint, Dictionary<uint, int>> _indices = new();
private readonly Dictionary<uint, Stack<int>> _freeSlots = new();
internal void Add(uint key)
{
uint prefix = key & 0xFFFF0000u;
if (!_slots.TryGetValue(prefix, out List<uint>? slots))
{
slots = new List<uint>();
_slots[prefix] = slots;
_indices[prefix] = new Dictionary<uint, int>();
_freeSlots[prefix] = new Stack<int>();
}
Dictionary<uint, int> indices = _indices[prefix];
if (indices.ContainsKey(key))
return;
if (_freeSlots[prefix].TryPop(out int freeIndex))
{
slots[freeIndex] = key;
indices[key] = freeIndex;
return;
}
indices[key] = slots.Count;
slots.Add(key);
}
internal void Remove(uint key)
{
uint prefix = key & 0xFFFF0000u;
if (!_indices.TryGetValue(prefix, out Dictionary<uint, int>? indices)
|| !indices.Remove(key, out int slotIndex))
{
return;
}
_slots[prefix][slotIndex] = 0u;
_freeSlots[prefix].Push(slotIndex);
if (indices.Count != 0)
return;
// An in-flight seal cursor retains its captured List reference and
// observes only tombstones. A future install gets a fresh container.
_slots.Remove(prefix);
_indices.Remove(prefix);
_freeSlots.Remove(prefix);
}
/// <summary>
/// The live slot list for one landblock prefix, or null when no key is
/// installed. Callers capture the reference plus <c>Count</c> once and
/// iterate by index, skipping tombstone slots (key 0).
/// </summary>
internal List<uint>? SlotsForPrefix(uint prefix) =>
_slots.TryGetValue(prefix & 0xFFFF0000u, out List<uint>? slots)
? slots
: null;
internal int InstalledKeyCountForPrefix(uint prefix) =>
_indices.TryGetValue(prefix & 0xFFFF0000u, out var indices)
? indices.Count
: 0;
}
/// <summary>
/// One exclusive-by-ownership collision-world root. A preparation mutates only
/// its private root; activation transfers the complete root through one volatile
@ -38,6 +113,128 @@ internal sealed class CollisionWorldState
internal List<uint> ShadowOwnerSlots { get; } = new();
internal Dictionary<uint, int> ShadowOwnerIndices { get; } = new();
internal Stack<int> ShadowOwnerFreeSlots { get; } = new();
// ── O1 per-prefix installed-key ledgers ────────────────────────────────
// Every mutation of the five landblock-scoped world maps goes through the
// typed helpers below so these ledgers stay exact. The seal's landblock-
// replacement builders enumerate one prefix's keys instead of scanning the
// whole resident map, and the retirement/removal paths retire one prefix
// in O(prefix keys).
internal PrefixKeyIndex CellStructKeys { get; } = new();
internal PrefixKeyIndex FlatCellStructKeys { get; } = new();
internal PrefixKeyIndex FlatEnvCellKeys { get; } = new();
internal PrefixKeyIndex BuildingKeys { get; } = new();
internal PrefixKeyIndex EnvCellKeys { get; } = new();
internal void SetCellStruct(uint id, CellPhysics value)
{
CellStruct[id] = value;
CellStructKeys.Add(id);
}
internal bool TryAddCellStruct(uint id, CellPhysics value)
{
if (!CellStruct.TryAdd(id, value))
return false;
CellStructKeys.Add(id);
return true;
}
internal bool RemoveCellStruct(uint id)
{
if (!CellStruct.TryRemove(id, out _))
return false;
CellStructKeys.Remove(id);
return true;
}
internal void SetFlatCellStruct(uint id, FlatCellStructureCollisionAsset value)
{
FlatCellStruct[id] = value;
FlatCellStructKeys.Add(id);
}
internal bool TryAddFlatCellStruct(uint id, FlatCellStructureCollisionAsset value)
{
if (!FlatCellStruct.TryAdd(id, value))
return false;
FlatCellStructKeys.Add(id);
return true;
}
internal bool RemoveFlatCellStruct(uint id)
{
if (!FlatCellStruct.TryRemove(id, out _))
return false;
FlatCellStructKeys.Remove(id);
return true;
}
internal void SetFlatEnvCell(uint id, FlatEnvCellTopology value)
{
FlatEnvCell[id] = value;
FlatEnvCellKeys.Add(id);
}
internal bool TryAddFlatEnvCell(uint id, FlatEnvCellTopology value)
{
if (!FlatEnvCell.TryAdd(id, value))
return false;
FlatEnvCellKeys.Add(id);
return true;
}
internal bool RemoveFlatEnvCell(uint id)
{
if (!FlatEnvCell.TryRemove(id, out _))
return false;
FlatEnvCellKeys.Remove(id);
return true;
}
internal void SetBuilding(uint id, BuildingPhysics value)
{
Buildings[id] = value;
BuildingKeys.Add(id);
}
internal bool TryAddBuilding(uint id, BuildingPhysics value)
{
if (!Buildings.TryAdd(id, value))
return false;
BuildingKeys.Add(id);
return true;
}
internal bool RemoveBuilding(uint id)
{
if (!Buildings.TryRemove(id, out _))
return false;
BuildingKeys.Remove(id);
return true;
}
internal void SetEnvCell(uint id, EnvCell value)
{
EnvCells[id] = value;
EnvCellKeys.Add(id);
}
internal bool TryAddEnvCell(uint id, EnvCell value)
{
if (!EnvCells.TryAdd(id, value))
return false;
EnvCellKeys.Add(id);
return true;
}
internal bool RemoveEnvCell(uint id)
{
if (!EnvCells.TryRemove(id, out _))
return false;
EnvCellKeys.Remove(id);
return true;
}
}
/// <summary>
@ -83,5 +280,18 @@ internal sealed class CollisionWorldStateSlot
return transferred;
}
/// <summary>
/// O2 (2026-08-02): terminally revokes a consumed staging root. The
/// per-landblock delta commit installs the staged content into the active
/// root instead of swapping roots, so the staging root no longer becomes
/// the active root — but a committed preparation must still lose access to
/// its private world exactly as the old transfer revoked it.
/// </summary>
internal void Revoke()
{
_revoked = true;
_current = null;
}
internal CollisionWorldState Capture() => Current;
}

View file

@ -483,9 +483,9 @@ public sealed class PhysicsDataCache
preparedTopology = null;
}
if (preparedStructure is not null)
_flatCellStruct.TryAdd(envCellId, preparedStructure);
_collisionWorld.Current.TryAddFlatCellStruct(envCellId, preparedStructure);
if (preparedTopology is not null)
_flatEnvCell.TryAdd(envCellId, preparedTopology);
_collisionWorld.Current.TryAddFlatEnvCell(envCellId, preparedTopology);
// UCG Stage 1: register only a loadable authored cell.
if (!CellGraph.Contains(envCellId))
@ -558,7 +558,7 @@ public sealed class PhysicsDataCache
// for every ordinary (non-house-barrier) cell.
RestrictionObj = envCell.RestrictionObj,
};
_cellStruct[envCellId] = cellPhysics;
_collisionWorld.Current.SetCellStruct(envCellId, cellPhysics);
if (PhysicsDiagnostics.ProbeDumpCellsEnabled
&& PhysicsDiagnostics.ProbeDumpCellIds.Contains(envCellId))
@ -696,8 +696,8 @@ public sealed class PhysicsDataCache
if (preparedStructure.ContainmentBsp.RootIndex < 0)
return;
_flatCellStruct.TryAdd(envCellId, preparedStructure);
_flatEnvCell.TryAdd(envCellId, preparedTopology);
_collisionWorld.Current.TryAddFlatCellStruct(envCellId, preparedStructure);
_collisionWorld.Current.TryAddFlatEnvCell(envCellId, preparedTopology);
if (!CellGraph.Contains(envCellId))
{
@ -724,7 +724,7 @@ public sealed class PhysicsDataCache
portal.Flags));
}
_cellStruct.TryAdd(envCellId, new CellPhysics
_collisionWorld.Current.TryAddCellStruct(envCellId, new CellPhysics
{
SourceId = envCellId,
WorldTransform = worldTransform,
@ -914,7 +914,7 @@ public sealed class PhysicsDataCache
/// dat-driven <see cref="CacheCellStruct"/>.
/// </summary>
public void RegisterCellStructForTest(uint envCellId, CellPhysics physics)
=> _cellStruct[envCellId] = physics;
=> _collisionWorld.Current.SetCellStruct(envCellId, physics);
/// <summary>
/// Indoor walking Phase 2 (2026-05-19). Cache the building portal list
@ -926,7 +926,7 @@ public sealed class PhysicsDataCache
{
if (_buildings.ContainsKey(landcellId)) return;
Matrix4x4.Invert(worldTransform, out var inverse);
_buildings[landcellId] = new BuildingPhysics
_collisionWorld.Current.SetBuilding(landcellId, new BuildingPhysics
{
WorldTransform = worldTransform,
InverseWorldTransform = inverse,
@ -935,7 +935,7 @@ public sealed class PhysicsDataCache
// (0x00534030) — and one building per origin landcell mirrors
// CLandBlock::init_buildings (0x0052fd80).
ModelId = modelId,
};
});
}
/// <summary>
@ -954,10 +954,33 @@ public sealed class PhysicsDataCache
/// </summary>
public void RemoveBuildingsForLandblock(uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
foreach (var key in _buildings.Keys)
if ((key & 0xFFFF0000u) == prefix)
_buildings.TryRemove(key, out _);
CollisionWorldState world = _collisionWorld.Current;
RemovePrefixKeys(
world.BuildingKeys,
landblockId & 0xFFFF0000u,
world.RemoveBuilding);
}
/// <summary>
/// Retires one landblock prefix's installed keys through the O1 ledger:
/// O(prefix keys), never a whole-map scan. Removal tombstones the captured
/// slot list, so index iteration over the captured reference stays exact.
/// </summary>
private static void RemovePrefixKeys(
PrefixKeyIndex ledger,
uint prefix,
Func<uint, bool> remove)
{
List<uint>? slots = ledger.SlotsForPrefix(prefix);
if (slots is null)
return;
int limit = slots.Count;
for (int index = 0; index < limit; index++)
{
uint key = slots[index];
if (key != 0u)
remove(key);
}
}
/// <summary>
@ -973,15 +996,13 @@ public sealed class PhysicsDataCache
public void RemoveCellsForLandblock(uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
foreach (var key in _cellStruct.Keys)
if ((key & 0xFFFF0000u) == prefix)
_cellStruct.TryRemove(key, out _);
foreach (var key in _flatCellStruct.Keys)
if ((key & 0xFFFF0000u) == prefix)
_flatCellStruct.TryRemove(key, out _);
foreach (var key in _flatEnvCell.Keys)
if ((key & 0xFFFF0000u) == prefix)
_flatEnvCell.TryRemove(key, out _);
CollisionWorldState world = _collisionWorld.Current;
RemovePrefixKeys(world.CellStructKeys, prefix, world.RemoveCellStruct);
RemovePrefixKeys(
world.FlatCellStructKeys,
prefix,
world.RemoveFlatCellStruct);
RemovePrefixKeys(world.FlatEnvCellKeys, prefix, world.RemoveFlatEnvCell);
}
public BuildingPhysics? GetBuilding(uint landcellId)
@ -990,7 +1011,8 @@ public sealed class PhysicsDataCache
public IReadOnlyCollection<uint> BuildingIds => (IReadOnlyCollection<uint>)_buildings.Keys;
/// <summary>Test helper, mirrors <see cref="RegisterCellStructForTest"/>.</summary>
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) => _buildings[landcellId] = b;
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) =>
_collisionWorld.Current.SetBuilding(landcellId, b);
internal sealed class LandblockReplacementBuilder : IDisposable
{
@ -1017,10 +1039,9 @@ public sealed class PhysicsDataCache
private readonly List<uint> _removeFlatEnvCells = new();
private readonly List<uint> _removeBuildings = new();
private readonly UcgCellGraph.LandblockReplacementBuilder _cellGraph;
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cellEnumerator;
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>? _flatCellEnumerator;
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvEnumerator;
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildingEnumerator;
private List<uint>? _keySlots;
private int _keySlotLimit;
private bool _keySlotsCaptured;
private int _phase;
private int _cursor;
@ -1078,93 +1099,139 @@ public sealed class PhysicsDataCache
_phase++;
return false;
case 2:
_cellEnumerator ??= _staging._cellStruct.GetEnumerator();
if (CapturePrefixOne(_cellEnumerator, _prefix, _cells, _cellIds))
{
// O1: enumerate the staging root's installed target-prefix
// keys instead of scanning the whole staging map, one key
// per advance.
if (TryTakeNextPrefixKey(
StagingWorld.CellStructKeys,
out uint id))
{
CaptureInstall(_staging._cellStruct, id, _cells, _cellIds);
WorkUnits++;
return false;
}
_cellEnumerator.Dispose();
_cellEnumerator = null;
_phase++;
return false;
}
case 3:
_cellEnumerator ??= _active._cellStruct.GetEnumerator();
if (CaptureRemovalOne(_cellEnumerator, _prefix, _cellIds, _removeCells))
{
// O1: enumerate the active root's installed target-prefix
// keys for removal capture. This also removes the previous
// cross-frame live enumerator over the active map.
if (TryTakeNextPrefixKey(
ActiveWorld.CellStructKeys,
out uint id))
{
CaptureRemoval(_active._cellStruct, id, _cellIds, _removeCells);
WorkUnits++;
return false;
}
_cellEnumerator.Dispose();
_cellEnumerator = null;
_phase++;
return false;
}
case 4:
_flatCellEnumerator ??= _staging._flatCellStruct.GetEnumerator();
if (CapturePrefixOne(_flatCellEnumerator, _prefix, _flatCells, _flatCellIds))
{
if (TryTakeNextPrefixKey(
StagingWorld.FlatCellStructKeys,
out uint id))
{
CaptureInstall(
_staging._flatCellStruct,
id,
_flatCells,
_flatCellIds);
WorkUnits++;
return false;
}
_flatCellEnumerator.Dispose();
_flatCellEnumerator = null;
_phase++;
return false;
}
case 5:
_flatCellEnumerator ??= _active._flatCellStruct.GetEnumerator();
if (CaptureRemovalOne(_flatCellEnumerator, _prefix, _flatCellIds, _removeFlatCells))
{
if (TryTakeNextPrefixKey(
ActiveWorld.FlatCellStructKeys,
out uint id))
{
CaptureRemoval(
_active._flatCellStruct,
id,
_flatCellIds,
_removeFlatCells);
WorkUnits++;
return false;
}
_flatCellEnumerator.Dispose();
_flatCellEnumerator = null;
_phase++;
return false;
}
case 6:
_flatEnvEnumerator ??= _staging._flatEnvCell.GetEnumerator();
if (CapturePrefixOne(_flatEnvEnumerator, _prefix, _flatEnvCells, _flatEnvCellIds))
{
if (TryTakeNextPrefixKey(
StagingWorld.FlatEnvCellKeys,
out uint id))
{
CaptureInstall(
_staging._flatEnvCell,
id,
_flatEnvCells,
_flatEnvCellIds);
WorkUnits++;
return false;
}
_flatEnvEnumerator.Dispose();
_flatEnvEnumerator = null;
_phase++;
return false;
}
case 7:
_flatEnvEnumerator ??= _active._flatEnvCell.GetEnumerator();
if (CaptureRemovalOne(_flatEnvEnumerator, _prefix, _flatEnvCellIds, _removeFlatEnvCells))
{
if (TryTakeNextPrefixKey(
ActiveWorld.FlatEnvCellKeys,
out uint id))
{
CaptureRemoval(
_active._flatEnvCell,
id,
_flatEnvCellIds,
_removeFlatEnvCells);
WorkUnits++;
return false;
}
_flatEnvEnumerator.Dispose();
_flatEnvEnumerator = null;
_phase++;
return false;
}
case 8:
_buildingEnumerator ??= _staging._buildings.GetEnumerator();
if (CapturePrefixOne(_buildingEnumerator, _prefix, _buildings, _buildingIds))
{
if (TryTakeNextPrefixKey(
StagingWorld.BuildingKeys,
out uint id))
{
CaptureInstall(
_staging._buildings,
id,
_buildings,
_buildingIds);
WorkUnits++;
return false;
}
_buildingEnumerator.Dispose();
_buildingEnumerator = null;
_phase++;
return false;
}
case 9:
_buildingEnumerator ??= _active._buildings.GetEnumerator();
if (CaptureRemovalOne(_buildingEnumerator, _prefix, _buildingIds, _removeBuildings))
{
if (TryTakeNextPrefixKey(
ActiveWorld.BuildingKeys,
out uint id))
{
CaptureRemoval(
_active._buildings,
id,
_buildingIds,
_removeBuildings);
WorkUnits++;
return false;
}
_buildingEnumerator.Dispose();
_buildingEnumerator = null;
_phase++;
return false;
}
case 10:
WorkUnits++;
if (!_cellGraph.Advance())
@ -1210,43 +1277,70 @@ public sealed class PhysicsDataCache
destination.TryAdd(id, value);
}
private static bool CapturePrefixOne<T>(
IEnumerator<KeyValuePair<uint, T>> enumerator,
uint prefix,
private CollisionWorldState StagingWorld =>
_staging._collisionWorld.Current;
private CollisionWorldState ActiveWorld =>
_active._collisionWorld.Current;
/// <summary>
/// O1 metered prefix-key cursor. The first call of a phase captures
/// the ledger's live slot-list reference and count; later calls
/// iterate by index, skipping tombstones (key 0). Removal only ever
/// tombstones a slot, so a captured reference stays exact across
/// frames without holding a map enumerator.
/// </summary>
private bool TryTakeNextPrefixKey(PrefixKeyIndex ledger, out uint key)
{
if (!_keySlotsCaptured)
{
_keySlots = ledger.SlotsForPrefix(_prefix);
_keySlotLimit = _keySlots?.Count ?? 0;
_keySlotsCaptured = true;
_cursor = 0;
}
while (_cursor < _keySlotLimit)
{
uint candidate = _keySlots![_cursor++];
if (candidate != 0u)
{
key = candidate;
return true;
}
}
key = 0u;
_keySlots = null;
_keySlotsCaptured = false;
return false;
}
private static void CaptureInstall<T>(
ConcurrentDictionary<uint, T> source,
uint id,
List<KeyValuePair<uint, T>> destination,
HashSet<uint> ids)
{
if (!enumerator.MoveNext())
return false;
KeyValuePair<uint, T> pair = enumerator.Current;
if ((pair.Key & 0xFFFF0000u) == prefix)
if (source.TryGetValue(id, out T? value))
{
destination.Add(pair);
ids.Add(pair.Key);
destination.Add(new KeyValuePair<uint, T>(id, value));
ids.Add(id);
}
return true;
}
private static bool CaptureRemovalOne<T>(
IEnumerator<KeyValuePair<uint, T>> enumerator,
uint prefix,
private static void CaptureRemoval<T>(
ConcurrentDictionary<uint, T> source,
uint id,
HashSet<uint> retained,
List<uint> destination)
{
if (!enumerator.MoveNext())
return false;
uint id = enumerator.Current.Key;
if ((id & 0xFFFF0000u) == prefix && !retained.Contains(id))
if (!retained.Contains(id) && source.ContainsKey(id))
destination.Add(id);
return true;
}
public void Dispose()
{
_cellEnumerator?.Dispose();
_flatCellEnumerator?.Dispose();
_flatEnvEnumerator?.Dispose();
_buildingEnumerator?.Dispose();
_keySlots = null;
_keySlotsCaptured = false;
_cellGraph.Dispose();
}
}

View file

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

View file

@ -1983,6 +1983,78 @@ public sealed class ShadowObjectRegistry
internal uint GetOwnerSlot(int index) => _ownerSlots[index];
/// <summary>
/// O3 (2026-08-02): commit-time owner application for one landblock
/// delta, replacing the deleted staged whole-world reflood context.
/// Retail hydrates a cell and refloods the objects associated with it —
/// <c>CObjCell::init_objects</c> (0x0052B420) →
/// <c>CPhysicsObj::recalc_cross_cells</c> (0x00515A30); the per-landblock
/// streaming analogue is: adopt each staged owner's registration/shape
/// payload and recalculate its cross-cells against the live post-delta
/// world, retire the outgoing generation's authored statics that were not
/// re-authored, and re-run the flood for every retained live owner
/// touching the replaced landblock.
/// </summary>
internal void ApplyCommittedOwnerReplacement(
ShadowObjectRegistry stagingSource,
uint ownerId,
uint landblockId)
{
ArgumentNullException.ThrowIfNull(stagingSource);
if (stagingSource.HasLogicalOwner(ownerId))
{
// Staged owner (authored target static, or an owner registered
// directly into the generation): adopt its payload, then
// recalc_cross_cells against the live world — the staged flood
// only saw the target-only staging root, so a seam footprint
// completes here.
MirrorOwnerFrom(stagingSource, ownerId);
RefloodOwnerForLandblock(ownerId, landblockId);
return;
}
if (IsStaticOwnerRootedIn(ownerId, landblockId))
{
// Outgoing generation's authored static, not re-authored by the
// replacement: it ends with its landblock (same lifetime rule as
// RetireOwnerFromLandblock's static branch).
DeregisterCore(ownerId, publishMutation: false);
RemoveOwnerPrefixMembership(ownerId);
_ownerVersions.Remove(ownerId);
AdvanceMutationRevision();
return;
}
// Retained live owner touching the replaced landblock: recalculate its
// cross-cells against the new topology. Suspended or since-removed
// owners no-op inside the reflood.
RefloodOwnerForLandblock(ownerId, landblockId);
}
/// <summary>
/// O3 (2026-08-02): retail <c>CObjCell::init_objects</c> refloods every
/// object associated with the hydrated cell at hydration time. Owners
/// that became associated with the replaced landblock after the sealed
/// capture (a mover entering the prefix mid-publication) are present in
/// the live prefix-owner slots but absent from the sealed owner list;
/// recalculate their cross-cells against the just-installed topology.
/// </summary>
internal void RefloodPrefixOwnersAfterReplacement(
uint landblockId,
IReadOnlyList<uint> sealedOwnerIds)
{
uint prefix = landblockId & 0xFFFF0000u;
if (!_prefixOwnerSlots.TryGetValue(prefix, out List<uint>? slots))
return;
var applied = new HashSet<uint>(sealedOwnerIds);
int limit = slots.Count;
for (int index = 0; index < limit; index++)
{
uint ownerId = slots[index];
if (ownerId == 0u || !applied.Add(ownerId))
continue;
RefloodOwnerForLandblock(ownerId, landblockId);
}
}
/// <summary>
/// Refreshes one staging owner from the exact active payload, then floods
/// it against the staging generation's complete cell graph. The returned
@ -2321,14 +2393,16 @@ public sealed class ShadowObjectRegistry
if (_stagingSlotIndex < _stagingSlotLimit)
{
uint ownerId = _stagingSlots![_stagingSlotIndex++];
if (_staging._entityReg.TryGetValue(
ownerId,
out RegistrationRecord? registration)
&& registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
{
// O2 (2026-08-02): every staged logical owner in the
// target's prefix slots — authored statics AND owners
// registered directly into the staging generation —
// must reach the active world through the delta
// commit's owner installs; the old whole-root transfer
// carried them implicitly. Mirrored owners identical
// to their active state reinstall in place, so the
// wider filter stays exact and prefix-scoped.
if (_staging._entityReg.ContainsKey(ownerId))
AddOwner(ownerId);
}
WorkUnits++;
return false;
}

View file

@ -46,7 +46,8 @@ public sealed class CellGraph
public bool Contains(uint envCellId) => _envCells.ContainsKey(envCellId);
public void Add(EnvCell cell) => _envCells.TryAdd(cell.Id, cell);
public void Add(EnvCell cell) =>
_collisionWorld.Current.TryAddEnvCell(cell.Id, cell);
/// <param name="landblockPrefix">Any id in the cell's landblock; masked to (id &amp; 0xFFFF0000).</param>
public void RegisterTerrain(uint landblockPrefix, TerrainSurface terrain, Vector3 worldOrigin)
@ -96,8 +97,27 @@ public sealed class CellGraph
_terrain.TryRemove(lb, out _);
for (uint low = 1u; low <= 0x40u; low++)
_outdoorCells.TryRemove(lb | low, out _);
foreach (var id in new List<uint>(_envCells.Keys))
if ((id & 0xFFFF0000u) == lb) _envCells.TryRemove(id, out _);
RemoveEnvCellPrefixKeys(lb);
}
/// <summary>
/// O1: retire one prefix's EnvCells through the installed-key ledger —
/// O(prefix keys), never a whole-map scan. Removal tombstones the
/// captured slot list, so index iteration stays exact.
/// </summary>
private void RemoveEnvCellPrefixKeys(uint prefix)
{
CollisionWorldState world = _collisionWorld.Current;
List<uint>? slots = world.EnvCellKeys.SlotsForPrefix(prefix);
if (slots is null)
return;
int limit = slots.Count;
for (int index = 0; index < limit; index++)
{
uint id = slots[index];
if (id != 0u)
world.RemoveEnvCell(id);
}
}
/// <summary>
@ -113,8 +133,7 @@ public sealed class CellGraph
{
CurrCell = null;
}
foreach (var id in new List<uint>(_envCells.Keys))
if ((id & 0xFFFF0000u) == lb) _envCells.TryRemove(id, out _);
RemoveEnvCellPrefixKeys(lb);
}
/// <summary>The universal id-&gt;cell resolver (retail CObjCell::GetVisible).</summary>
@ -170,7 +189,10 @@ public sealed class CellGraph
private readonly List<KeyValuePair<uint, EnvCell>> _envCells = new();
private readonly HashSet<uint> _stagingIds = new();
private readonly List<uint> _removeIds = new();
private IEnumerator<KeyValuePair<uint, EnvCell>>? _enumerator;
private List<uint>? _keySlots;
private int _keySlotLimit;
private bool _keySlotsCaptured;
private int _cursor;
private int _phase;
internal LandblockReplacementBuilder(
@ -181,44 +203,48 @@ public sealed class CellGraph
_active = active;
_staging = staging;
_prefix = landblockId & 0xFFFF0000u;
_enumerator = staging._envCells.GetEnumerator();
}
internal bool Advance()
{
if (_phase == 0)
{
if (_enumerator!.MoveNext())
// O1: enumerate the staging root's installed target-prefix
// EnvCell keys via the ledger instead of scanning the map.
if (TryTakeNextPrefixKey(
_staging._collisionWorld.Current.EnvCellKeys,
out uint stagingId))
{
KeyValuePair<uint, EnvCell> pair = _enumerator.Current;
if ((pair.Key & 0xFFFF0000u) == _prefix
&& (pair.Key & 0xFFFFu) >= 0x0100u)
if ((stagingId & 0xFFFFu) >= 0x0100u
&& _staging._envCells.TryGetValue(
stagingId,
out EnvCell? cell))
{
_envCells.Add(pair);
_stagingIds.Add(pair.Key);
_envCells.Add(
new KeyValuePair<uint, EnvCell>(stagingId, cell));
_stagingIds.Add(stagingId);
}
return false;
}
_enumerator.Dispose();
_enumerator = _active._envCells.GetEnumerator();
_phase = 1;
return false;
}
if (_phase == 1)
{
if (_enumerator!.MoveNext())
// O1: active-side removal capture through the ledger — no
// cross-frame live enumerator over the active map.
if (TryTakeNextPrefixKey(
_active._collisionWorld.Current.EnvCellKeys,
out uint activeId))
{
uint id = _enumerator.Current.Key;
if ((id & 0xFFFF0000u) == _prefix
&& (id & 0xFFFFu) >= 0x0100u
&& !_stagingIds.Contains(id))
if ((activeId & 0xFFFFu) >= 0x0100u
&& !_stagingIds.Contains(activeId)
&& _active._envCells.ContainsKey(activeId))
{
_removeIds.Add(id);
_removeIds.Add(activeId);
}
return false;
}
_enumerator.Dispose();
_enumerator = null;
bool hasTerrain = _staging._terrain.TryGetValue(
_prefix,
out var terrain);
@ -233,12 +259,36 @@ public sealed class CellGraph
return true;
}
private bool TryTakeNextPrefixKey(PrefixKeyIndex ledger, out uint key)
{
if (!_keySlotsCaptured)
{
_keySlots = ledger.SlotsForPrefix(_prefix);
_keySlotLimit = _keySlots?.Count ?? 0;
_keySlotsCaptured = true;
_cursor = 0;
}
while (_cursor < _keySlotLimit)
{
uint candidate = _keySlots![_cursor++];
if (candidate != 0u)
{
key = candidate;
return true;
}
}
key = 0u;
_keySlots = null;
_keySlotsCaptured = false;
return false;
}
internal PreparedCellGraphLandblock? Prepared { get; private set; }
public void Dispose()
{
_enumerator?.Dispose();
_enumerator = null;
_keySlots = null;
_keySlotsCaptured = false;
}
}
}

File diff suppressed because it is too large Load diff