feat(render): Campaign OVERHAUL S2 chunk 5 + closeout — registry is the only render membership owner
Chunk 5 (consumer cutover): WalkProductionWorldData's per-cell views are borrowed from ShadowObjectRegistry.GetRetailPartEntriesInCell and resolved through RenderSceneQuery.TryGetByLocalEntityId; every render-side sweep, bucket, parent-cell and root-position fallback is deleted (AD-116 for the one-frame registry→scene window, counted in UnregisteredRenderMembershipCount). A live entity with visual parts but no collision geometry registers render-only (LiveEntityCollisionBuilder computes the part array before the empty-shapes gate). Closeout fixes found while landing it: - RefloodOwnerForLandblock forwards the retained part array — a reflood is retail's recalc_cross_cells over the SAME CPartArray; without it every owner touched by a landblock replacement commit lost its render membership. - Non-colliding DAT statics register render-only from BOTH publishers (LandblockPhysicsPublisher.PublishStaticEntity, LandblockPhysicsContentBuilder.RegisterRenderOnlyStatic). The G2 self-gate pixel diff caught them vanishing (Facility Hub wall panels): retail floods every object regardless of collision (CEnvCell::init_static_objects 0x0052c350, add_shadows_to_cells 0x00514ae0). - S2 dual review fix batch (arch + retail lens, lead-verified): Suspend clears the retail product (remove_shadows_from_cells 0x00511230 is one transaction); AttachChild/DetachChild advance the mutation revision so a prepared SetPosition cannot clobber a child's rows; an attached child never floods on its own re-registration; RemoveLandblock and the non-rooted RetireOwnerFromLandblock prune retail rows (render-only statics end with their landblock); a render-only owner's no-cell-array commit republishes at its destination cell (AD-117); an empty non-null part array is treated as null; per-move closures/LINQ replaced by index loops; EnvCell shells stay out of the scene's LocalEntityId index (payload-less records); the index predicate compares the id; the dead per-cell scene indices are deleted. Register: AD-116 (chunk 5), AD-117 (four residual Contract A/B readings). Evidence: s2-membership-ownership-map.md §8 (chunk 5) and §9 (closeout). Gates (Release): Core 4,984/4,984; Content 214/214; Runtime 1,884/1,884; App hermetic lane 6,760/6,760; App InstalledDat lane 217 pass / 1 skip / 2 pre-existing #383 layout-fixture failures; App Windows lane 1/1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f30039d3d9
commit
c94a1a407e
20 changed files with 2141 additions and 1026 deletions
|
|
@ -506,6 +506,20 @@ public sealed class ShadowObjectRegistry
|
|||
RetailCellArrayRoute route,
|
||||
IReadOnlyList<ShadowShape> partArray)
|
||||
{
|
||||
// Campaign OVERHAUL S2 review fix (arch F3): retail never floods an
|
||||
// attached child — add_shadows_to_cells (0x00514ae0) passes the
|
||||
// ROOT's CELLARRAY down through `children`. A re-registration or move
|
||||
// of an entity that is currently attached (an equipped item's
|
||||
// appearance update through ReplaceMultiPartPayload/RegisterMultiPart)
|
||||
// only refreshes the part array it inherits the root's cells with.
|
||||
if (_childParent.ContainsKey(entityId))
|
||||
{
|
||||
if (partArray.Count != 0)
|
||||
_childPartArrays[entityId] = partArray;
|
||||
_retailCellArrayRoutes[entityId] = route;
|
||||
PublishChildEntries(entityId);
|
||||
return;
|
||||
}
|
||||
if (_retailCellArrays.TryGetValue(entityId, out List<uint>? previousCells))
|
||||
{
|
||||
RemoveRetailPartEntriesFromCells(entityId, previousCells);
|
||||
|
|
@ -604,13 +618,71 @@ public sealed class ShadowObjectRegistry
|
|||
cellIds[i],
|
||||
out List<RetailPartEntry>? entries))
|
||||
{
|
||||
entries.RemoveAll(e => e.EntityId == entityId);
|
||||
RemoveOwnerPartRows(entries, entityId);
|
||||
if (entries.Count == 0)
|
||||
_retailPartEntriesByCell.Remove(cellIds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The <see cref="RemoveOwnerRows"/> twin for the retail part
|
||||
/// rows: a reverse index loop, no closure — this runs per cell on every
|
||||
/// accepted move (arch review F7).</summary>
|
||||
private static void RemoveOwnerPartRows(
|
||||
List<RetailPartEntry> entries,
|
||||
uint entityId)
|
||||
{
|
||||
for (int index = entries.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (entries[index].EntityId == entityId)
|
||||
entries.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static ShadowEntry[] CollectOwnerRows(
|
||||
List<ShadowEntry> entries,
|
||||
uint entityId)
|
||||
{
|
||||
int count = 0;
|
||||
for (int index = 0; index < entries.Count; index++)
|
||||
{
|
||||
if (entries[index].EntityId == entityId)
|
||||
count++;
|
||||
}
|
||||
if (count == 0)
|
||||
return Array.Empty<ShadowEntry>();
|
||||
var rows = new ShadowEntry[count];
|
||||
int written = 0;
|
||||
for (int index = 0; index < entries.Count; index++)
|
||||
{
|
||||
if (entries[index].EntityId == entityId)
|
||||
rows[written++] = entries[index];
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static RetailPartEntry[] CollectOwnerPartRows(
|
||||
List<RetailPartEntry> entries,
|
||||
uint entityId)
|
||||
{
|
||||
int count = 0;
|
||||
for (int index = 0; index < entries.Count; index++)
|
||||
{
|
||||
if (entries[index].EntityId == entityId)
|
||||
count++;
|
||||
}
|
||||
if (count == 0)
|
||||
return Array.Empty<RetailPartEntry>();
|
||||
var rows = new RetailPartEntry[count];
|
||||
int written = 0;
|
||||
for (int index = 0; index < entries.Count; index++)
|
||||
{
|
||||
if (entries[index].EntityId == entityId)
|
||||
rows[written++] = entries[index];
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes retail's <c>CPartArray::AddPartsShadow</c> (0x00517e40) rows
|
||||
/// for one entity: for every cell in <paramref name="orderedCells"/>, in
|
||||
|
|
@ -742,6 +814,12 @@ public sealed class ShadowObjectRegistry
|
|||
_childPartArrays[childEntityId] = childPartArray;
|
||||
|
||||
PublishChildEntries(childEntityId);
|
||||
// Campaign OVERHAUL S2 review fix (arch F2): an attach mutates shared
|
||||
// per-cell rows, so it must invalidate any prepared SetPosition commit
|
||||
// whose captured cell lists predate it (IsPreparedSetPositionCurrent
|
||||
// keys on the mutation revision) — otherwise that commit's wholesale
|
||||
// list install silently drops the child's rows.
|
||||
AdvanceMutationRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -786,6 +864,7 @@ public sealed class ShadowObjectRegistry
|
|||
if (siblings.Count == 0)
|
||||
_parentChildren.Remove(parentId);
|
||||
}
|
||||
AdvanceMutationRevision(); // see AttachChild (arch F2)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1041,7 +1120,32 @@ public sealed class ShadowObjectRegistry
|
|||
bool publishMutation = true,
|
||||
IReadOnlyList<ShadowShape>? partArray = null)
|
||||
{
|
||||
if (shapes.Count == 0) { Deregister(entityId); return; }
|
||||
if (shapes.Count == 0)
|
||||
{
|
||||
// Campaign OVERHAUL S2 chunk 5 (item B): a live entity whose
|
||||
// collision dispatch produced NO shapes (LiveEntityCollisionBuilder.
|
||||
// Build's Shapes==[]) but whose Setup still has visual parts — the
|
||||
// retail short-lived spell/visual effect-object case — is NOT a
|
||||
// deregistration. Contract B's render membership
|
||||
// (CPartArray::AddPartsShadow) does not require a successful
|
||||
// collision dispatch; only the collision-side shadow_object_list
|
||||
// does. Route it to the render-only registration path instead.
|
||||
// A caller with no part array either (every pre-chunk-5 test
|
||||
// fixture, and any genuinely shapeless entity) keeps the exact
|
||||
// prior behavior: deregister.
|
||||
if (partArray is { Count: > 0 })
|
||||
{
|
||||
RegisterRenderOnly(
|
||||
entityId, entityWorldPos, entityWorldRot, state, flags,
|
||||
worldOffsetX, worldOffsetY, landblockId, seedCellId,
|
||||
isStatic, publishMutation, partArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
Deregister(entityId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Flood FIRST — keep-when-empty, see Register.
|
||||
uint seed = seedCellId != 0u
|
||||
|
|
@ -1157,6 +1261,79 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S2 chunk 5 (item B): registers a live entity that
|
||||
/// has NO collision shapes at all but DOES have a visual part array — the
|
||||
/// retail short-lived spell/visual effect-object case
|
||||
/// (<c>LiveEntityCollisionBuilder.Build</c> returns
|
||||
/// <c>Shapes = []</c>, <c>RenderParts != []</c>). Retail's
|
||||
/// <c>calc_cross_cells_static</c> dispatch (Contract A) never
|
||||
/// special-cases "no collision shapes": the cylsphere-vs-bbox TEST reads
|
||||
/// the collision shapes (none here, so the test is always false) and the
|
||||
/// flood unconditionally falls through to the bbox route over the WHOLE
|
||||
/// part array (<see cref="ComputeContractACellArray"/> with an empty
|
||||
/// <c>collisionShapes</c> list). The entity therefore gets an exact
|
||||
/// retail CELLARRAY and <see cref="RetailPartEntry"/> rows exactly like a
|
||||
/// colliding entity, but never a <see cref="ShadowEntry"/> collision row
|
||||
/// anywhere — Contract B's <c>shadow_object_list</c> receives one entry
|
||||
/// per CELLARRAY cell only when the object HAS a part to shadow there;
|
||||
/// this object contributes to <c>shadow_part_list</c> only.
|
||||
/// <para>
|
||||
/// <see cref="_entityShapes"/> is retained as an EMPTY (not absent) list
|
||||
/// so every existing multi-part-dispatch site
|
||||
/// (<see cref="UpdatePosition"/>, <see cref="ReplacePositionRows"/>,
|
||||
/// <see cref="RefloodOwnerForLandblock"/>) takes the "multi-part, zero
|
||||
/// shapes" branch on a later move/reflood instead of falling back to the
|
||||
/// single-shape <see cref="Register"/> path or synthesizing a bogus
|
||||
/// zero-radius shadow entry.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void RegisterRenderOnly(
|
||||
uint entityId,
|
||||
Vector3 entityWorldPos,
|
||||
Quaternion entityWorldRot,
|
||||
uint state,
|
||||
EntityCollisionFlags flags,
|
||||
float worldOffsetX,
|
||||
float worldOffsetY,
|
||||
uint landblockId,
|
||||
uint seedCellId,
|
||||
bool isStatic,
|
||||
bool publishMutation,
|
||||
IReadOnlyList<ShadowShape> partArray)
|
||||
{
|
||||
// Flood FIRST — keep-when-empty, see Register.
|
||||
uint seed = seedCellId != 0u
|
||||
? seedCellId
|
||||
: DeriveOutdoorSeed(entityWorldPos, worldOffsetX, worldOffsetY, landblockId);
|
||||
if (seed == 0u) return;
|
||||
|
||||
(IReadOnlyList<uint> cellSet, RetailCellArrayRoute retailRoute) =
|
||||
ComputeContractACellArray(
|
||||
seed,
|
||||
entityWorldPos,
|
||||
entityWorldRot,
|
||||
state,
|
||||
collisionShapes: Array.Empty<ShadowShape>(),
|
||||
partArray,
|
||||
isStatic);
|
||||
if (cellSet.Count == 0) return; // keep-when-empty (pc:283540).
|
||||
|
||||
DeregisterCore(entityId, publishMutation: false);
|
||||
_entityShapes[entityId] = Array.Empty<ShadowShape>();
|
||||
_entityReg[entityId] = new RegistrationRecord(
|
||||
seed, entityWorldPos, entityWorldRot, state, flags, isStatic,
|
||||
IsMultiPart: true, GfxObjId: 0u, Radius: 0f,
|
||||
CollisionType: ShadowCollisionType.BSP, CylHeight: 0f, Scale: 1f);
|
||||
if (publishMutation)
|
||||
BumpOwnerVersion(entityId);
|
||||
else
|
||||
RefreshOwnerPrefixIndex(entityId);
|
||||
|
||||
_entityRetailPartArrays[entityId] = partArray;
|
||||
PublishRetailCellArray(entityId, cellSet, retailRoute, partArray);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces an existing live PartArray collision payload in its current
|
||||
/// shadow-cell membership. Retail <c>CPartArray::SetPart</c> changes the
|
||||
|
|
@ -1183,7 +1360,11 @@ public sealed class ShadowObjectRegistry
|
|||
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? prior)
|
||||
|| !prior.IsMultiPart)
|
||||
{
|
||||
if (shapes.Count == 0)
|
||||
// Campaign OVERHAUL S2 chunk 5 (item B): a not-yet-registered
|
||||
// entity with no collision shapes but a real part array still
|
||||
// needs registering (render-only) — RegisterMultiPart's own
|
||||
// shapes.Count==0 branch now handles that dispatch.
|
||||
if (shapes.Count == 0 && (partArray is null || partArray.Count == 0))
|
||||
return;
|
||||
RegisterMultiPart(
|
||||
entityId,
|
||||
|
|
@ -1217,7 +1398,10 @@ public sealed class ShadowObjectRegistry
|
|||
// this replaces the retained part array WITHOUT re-flooding. It
|
||||
// reuses the CURRENT retail CELLARRAY (if any) and only rewrites
|
||||
// which parts occupy it, mirroring the collision payload swap below.
|
||||
if (partArray is not null)
|
||||
// Arch review F9: a non-null but EMPTY part array is "no part to
|
||||
// shadow" — treated exactly like the null case rather than leaving a
|
||||
// cell array with no drawable rows behind.
|
||||
if (partArray is { Count: > 0 })
|
||||
{
|
||||
_entityRetailPartArrays[entityId] = partArray;
|
||||
if (_retailCellArrays.TryGetValue(entityId, out List<uint>? retailCells)
|
||||
|
|
@ -2270,6 +2454,37 @@ public sealed class ShadowObjectRegistry
|
|||
return;
|
||||
}
|
||||
|
||||
// Campaign OVERHAUL S2 review fix (retail F3): a RENDER-ONLY owner
|
||||
// (part array, no collision shapes, hence never any retained
|
||||
// collision cells) still moves in retail — add_shadows_to_cells
|
||||
// (0x00514ae0) adds the CShadowObj unconditionally (pc:282856) and
|
||||
// gates only AddPartsShadow on part_array != 0, so a shapeless
|
||||
// visual object keeps a cell array it travels with. Its transition
|
||||
// carries no sphere, so that array is its destination cell alone
|
||||
// (num_cells == 1 → AddPartsShadow without clip planes). AD-117
|
||||
// records the single-cell reading until a cdb trace pins the
|
||||
// zero-sphere transition's exact cell list.
|
||||
if (!_entityToCells.ContainsKey(entityId)
|
||||
&& !_suspendedEntityCells.ContainsKey(entityId)
|
||||
&& seedCellId != 0u
|
||||
&& _entityRetailPartArrays.TryGetValue(
|
||||
entityId,
|
||||
out IReadOnlyList<ShadowShape>? renderPartArray)
|
||||
&& renderPartArray.Count != 0)
|
||||
{
|
||||
_suspendedEntities.Remove(entityId);
|
||||
_entityReg[entityId] = registration with
|
||||
{
|
||||
SeedCellId = seedCellId,
|
||||
EntityWorldPos = worldPosition,
|
||||
EntityWorldRot = worldRotation,
|
||||
};
|
||||
_singleCellScratch[0] = seedCellId;
|
||||
PublishRetailProductFromExactCells(entityId, _singleCellScratch);
|
||||
BumpOwnerVersion(entityId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 4: retail's keep-when-empty gate
|
||||
// (pc:283540) reached its terminal case — no retained collision
|
||||
// cells at all to republish from — so BOTH products stay exactly as
|
||||
|
|
@ -2285,6 +2500,8 @@ public sealed class ShadowObjectRegistry
|
|||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
private readonly uint[] _singleCellScratch = new uint[1];
|
||||
|
||||
private void ReplacePositionRows(
|
||||
uint entityId,
|
||||
RegistrationRecord registration,
|
||||
|
|
@ -2326,12 +2543,12 @@ public sealed class ShadowObjectRegistry
|
|||
exactCells.Add(cellId);
|
||||
}
|
||||
|
||||
if (registration.IsMultiPart
|
||||
&& _entityShapes.TryGetValue(
|
||||
entityId,
|
||||
out IReadOnlyList<ShadowShape>? shapes))
|
||||
IReadOnlyList<ShadowShape>? shapes = null;
|
||||
bool isMultiPartDispatch = registration.IsMultiPart
|
||||
&& _entityShapes.TryGetValue(entityId, out shapes);
|
||||
if (isMultiPartDispatch)
|
||||
{
|
||||
foreach (ShadowShape shape in shapes)
|
||||
foreach (ShadowShape shape in shapes!)
|
||||
{
|
||||
Vector3 partWorldPosition = worldPosition
|
||||
+ Vector3.Transform(shape.LocalPosition, worldRotation);
|
||||
|
|
@ -2371,7 +2588,15 @@ public sealed class ShadowObjectRegistry
|
|||
AddEntryToCell(entry, exactCells[index]);
|
||||
}
|
||||
|
||||
if (exactCells.Count == 0)
|
||||
// Campaign OVERHAUL S2 chunk 5 (item B): a render-only multi-part
|
||||
// entity (registration.IsMultiPart with an EMPTY retained shapes
|
||||
// list — see RegisterRenderOnly) writes NO ShadowEntry rows above.
|
||||
// _entityToCells must stay absent for it too, or a consumer reading
|
||||
// it (GetOwnerCells, DeregisterCore's cleanup walk) would see cell
|
||||
// ids that carry no actual collision row — a membership claim this
|
||||
// registry never backs with a shadow_object_list entry.
|
||||
bool wroteCollisionEntries = !isMultiPartDispatch || shapes!.Count != 0;
|
||||
if (exactCells.Count == 0 || !wroteCollisionEntries)
|
||||
_entityToCells.Remove(entityId);
|
||||
else
|
||||
_entityToCells[entityId] = exactCells;
|
||||
|
|
@ -2414,11 +2639,26 @@ public sealed class ShadowObjectRegistry
|
|||
foreach (uint cellId in cellIds)
|
||||
{
|
||||
if (_cells.TryGetValue(cellId, out var list))
|
||||
list.RemoveAll(entry => entry.EntityId == entityId);
|
||||
RemoveOwnerRows(list, entityId);
|
||||
}
|
||||
_entityToCells.Remove(entityId);
|
||||
}
|
||||
|
||||
// Campaign OVERHAUL S2 review fix (arch F1 / retail F2): retail's
|
||||
// remove_shadows_from_cells (0x00511230) is ONE transaction over both
|
||||
// products — per shadow cell it calls CObjCell::remove_shadow_object
|
||||
// AND CPartArray::RemoveParts, then recurses through children. A
|
||||
// suspended object therefore has no render membership either; the
|
||||
// retained part array and route survive so the un-suspending move
|
||||
// (ReplacePositionRows → PublishRetailProductFromExactCells)
|
||||
// republishes from the transition's cells.
|
||||
if (_retailCellArrays.TryGetValue(entityId, out List<uint>? retailCells))
|
||||
{
|
||||
RemoveRetailPartEntriesFromCells(entityId, retailCells);
|
||||
_retailCellArrays.Remove(entityId);
|
||||
RepublishAttachedChildren(entityId);
|
||||
}
|
||||
|
||||
_suspendedEntities.Add(entityId);
|
||||
BumpOwnerVersion(entityId);
|
||||
return true;
|
||||
|
|
@ -2502,6 +2742,19 @@ public sealed class ShadowObjectRegistry
|
|||
_withdrawnPrefixesByOwner.TryGetValue(
|
||||
entityId,
|
||||
out var withdrawnBeforeReflood);
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 5 closeout: a reflood is retail's
|
||||
// recalc_cross_cells over the SAME CPartArray — the render product
|
||||
// (retail cell array + AddPartsShadow rows) is recomputed from the
|
||||
// retained part array exactly like the movement path above does.
|
||||
// Without this the walk (which reads ONLY GetRetailPartEntriesInCell
|
||||
// since chunk 5) lost every reflooded owner: landblock replacement
|
||||
// commits (PhysicsEngine.ApplyCommittedOwnerReplacement) and the
|
||||
// Content builder's post-publication reflood both come through here.
|
||||
_entityRetailPartArrays.TryGetValue(
|
||||
entityId,
|
||||
out IReadOnlyList<ShadowShape>? retainedPartArray);
|
||||
|
||||
if (reg.IsMultiPart
|
||||
&& _entityShapes.TryGetValue(entityId, out var shapes))
|
||||
{
|
||||
|
|
@ -2517,7 +2770,8 @@ public sealed class ShadowObjectRegistry
|
|||
lbPrefix,
|
||||
reg.SeedCellId,
|
||||
reg.IsStatic,
|
||||
publishMutation: false);
|
||||
publishMutation: false,
|
||||
partArray: retainedPartArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -2537,7 +2791,8 @@ public sealed class ShadowObjectRegistry
|
|||
reg.Flags,
|
||||
reg.SeedCellId,
|
||||
reg.IsStatic,
|
||||
publishMutation: false);
|
||||
publishMutation: false,
|
||||
partArray: retainedPartArray);
|
||||
}
|
||||
|
||||
// Register is also the authoritative movement/replacement API and
|
||||
|
|
@ -2870,10 +3125,69 @@ public sealed class ShadowObjectRegistry
|
|||
_withdrawnPrefixesByOwner.Remove(eid);
|
||||
}
|
||||
}
|
||||
// Campaign OVERHAUL S2 review fix (arch F4 / retail F4): the retail
|
||||
// render product ends with the landblock too — remove_shadows_from_
|
||||
// cells (0x00511230) never removes the CShadowObj without the
|
||||
// AddPartsShadow rows. Every part row in the prefix's cells goes;
|
||||
// each owner's cell array loses those cells (a dynamic owner keeps
|
||||
// its part array for the reload reflood, a static owner ends here —
|
||||
// including a render-only static, which has no collision cells and
|
||||
// so was invisible to the loops above).
|
||||
RemoveRetailProductForPrefix(lbPrefix, touchedOwners);
|
||||
foreach (uint entityId in touchedOwners)
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
private readonly List<uint> _prefixRemovalScratch = new();
|
||||
|
||||
private void RemoveRetailProductForPrefix(
|
||||
uint lbPrefix,
|
||||
HashSet<uint> touchedOwners)
|
||||
{
|
||||
_prefixRemovalScratch.Clear();
|
||||
foreach (uint cellId in _retailPartEntriesByCell.Keys)
|
||||
{
|
||||
if ((cellId & 0xFFFF0000u) == lbPrefix)
|
||||
_prefixRemovalScratch.Add(cellId);
|
||||
}
|
||||
for (int i = 0; i < _prefixRemovalScratch.Count; i++)
|
||||
_retailPartEntriesByCell.Remove(_prefixRemovalScratch[i]);
|
||||
|
||||
_prefixRemovalScratch.Clear();
|
||||
foreach (var (ownerId, cells) in _retailCellArrays)
|
||||
{
|
||||
for (int i = cells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if ((cells[i] & 0xFFFF0000u) == lbPrefix)
|
||||
{
|
||||
cells.RemoveAt(i);
|
||||
touchedOwners.Add(ownerId);
|
||||
}
|
||||
}
|
||||
if (cells.Count == 0)
|
||||
_prefixRemovalScratch.Add(ownerId);
|
||||
}
|
||||
for (int i = 0; i < _prefixRemovalScratch.Count; i++)
|
||||
{
|
||||
uint ownerId = _prefixRemovalScratch[i];
|
||||
_retailCellArrays.Remove(ownerId);
|
||||
bool endsWithLandblock =
|
||||
!_entityReg.TryGetValue(ownerId, out RegistrationRecord? registration)
|
||||
|| registration.IsStatic;
|
||||
if (!endsWithLandblock)
|
||||
continue;
|
||||
_retailCellArrayRoutes.Remove(ownerId);
|
||||
_entityRetailPartArrays.Remove(ownerId);
|
||||
// A render-only static never had collision cells, so the static
|
||||
// retirement loop above could not reach its registration.
|
||||
_entityShapes.Remove(ownerId);
|
||||
_entityReg.Remove(ownerId);
|
||||
_suspendedEntities.Remove(ownerId);
|
||||
_suspendedEntityCells.Remove(ownerId);
|
||||
_withdrawnPrefixesByOwner.Remove(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires one logical owner's rows from a streamed-out prefix. This is
|
||||
/// the owner-granular form used by the collision-generation retirement
|
||||
|
|
@ -2895,10 +3209,39 @@ public sealed class ShadowObjectRegistry
|
|||
AdvanceMutationRevision();
|
||||
return;
|
||||
}
|
||||
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
||||
return;
|
||||
|
||||
bool touched = false;
|
||||
// Campaign OVERHAUL S2 review fix (arch F4 / retail F4): the owner's
|
||||
// retail part rows leave the retired prefix's cells together with
|
||||
// its collision rows (remove_shadows_from_cells 0x00511230 removes
|
||||
// both per cell); the part array stays for the reload reflood.
|
||||
if (_retailCellArrays.TryGetValue(entityId, out List<uint>? retailCells))
|
||||
{
|
||||
for (int index = retailCells.Count - 1; index >= 0; index--)
|
||||
{
|
||||
uint cellId = retailCells[index];
|
||||
if ((cellId & 0xFFFF0000u) != prefix)
|
||||
continue;
|
||||
touched = true;
|
||||
retailCells.RemoveAt(index);
|
||||
if (_retailPartEntriesByCell.TryGetValue(
|
||||
cellId,
|
||||
out List<RetailPartEntry>? partRows))
|
||||
{
|
||||
RemoveOwnerPartRows(partRows, entityId);
|
||||
if (partRows.Count == 0)
|
||||
_retailPartEntriesByCell.Remove(cellId);
|
||||
}
|
||||
}
|
||||
if (retailCells.Count == 0)
|
||||
_retailCellArrays.Remove(entityId);
|
||||
}
|
||||
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
||||
{
|
||||
if (touched)
|
||||
BumpOwnerVersion(entityId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int index = cells.Count - 1; index >= 0; index--)
|
||||
{
|
||||
uint cellId = cells[index];
|
||||
|
|
@ -3311,8 +3654,7 @@ public sealed class ShadowObjectRegistry
|
|||
{
|
||||
rows.Add(new PreparedShadowCellRows(
|
||||
cellId,
|
||||
entries.Where(entry => entry.EntityId == entityId)
|
||||
.ToArray()));
|
||||
CollectOwnerRows(entries, entityId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3341,8 +3683,7 @@ public sealed class ShadowObjectRegistry
|
|||
{
|
||||
retailRows.Add(new PreparedShadowRetailPartRows(
|
||||
cellId,
|
||||
entries.Where(entry => entry.EntityId == entityId)
|
||||
.ToArray()));
|
||||
CollectOwnerPartRows(entries, entityId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue