feat(physics): S2 chunk 4 - movement publishes from the transition's cells; children inherit at registration
Movement: CommitSetPosition's RefreshPositionRows/ReplacePositionRows and the staged apply publish the retail render product from the exact cell list collision just used, the transition's cell_array retail feeds add_shadows_to_cells in CPhysicsObj::SetPositionInternal @0x00515330 (pseudo-C 283526-283539); the separate move-path bbox recompute is deleted. calc_cross_cells @0x00515230 stays the distinct full-recompute path (PhysicsShadowCommitAction.Recalculate). Children (Contract B recursion): ShadowObjectRegistry.AttachChild/DetachChild give an attached object the root's current cells as part entries only, republished whenever the root's array changes, detached at withdrawal and cascaded from the root's Deregister; nested attachment resolves to the root with a bounded, cycle-safe chain. EquippedChildRenderController attaches at realization (FromSetupRenderParts over the child's Setup) and detaches at its single removal funnel. WalkProductionWorldData's dynamic sweep reads TryGetRetailCellArray directly; the 64-hop parent-chain walk and its FindParentLocalId plumbing are deleted. CollisionWorldState.Clear now also clears the retail products. Gates (implementer's isolated worktree at identical content): Release build 0/0; Core 4,970/4,970; App hermetic 6,761/6,761; targeted walk/child/live-entity/placement/comparator 166/166; Runtime 1,884/1,884. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
706fc49397
commit
75ea269d35
14 changed files with 1072 additions and 179 deletions
|
|
@ -507,8 +507,7 @@ internal sealed class FrameRootCompositionPhase
|
|||
?? throw new InvalidOperationException(
|
||||
"The retail frame walk requires the landscape registry."),
|
||||
d.CellVisibility,
|
||||
d.PhysicsEngine.ShadowObjects,
|
||||
live.EquippedChildren.FindParentLocalId),
|
||||
d.PhysicsEngine.ShadowObjects),
|
||||
retailPViewPassExecutor,
|
||||
retailPViewPassExecutor),
|
||||
retailPViewCells,
|
||||
|
|
|
|||
|
|
@ -622,7 +622,9 @@ internal sealed class LivePresentationCompositionPhase
|
|||
childRecord,
|
||||
positionVersion,
|
||||
projectionVersion,
|
||||
d.PlayerIdentity.ServerGuid)),
|
||||
d.PlayerIdentity.ServerGuid),
|
||||
d.PhysicsEngine.ShadowObjects,
|
||||
d.PhysicsDataCache),
|
||||
static value => value.Dispose());
|
||||
Fault(LivePresentationCompositionPoint.CorePresentationCreated);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,28 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
_withdrawProjection;
|
||||
private readonly EntityEffectPoseRegistry _poses;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S2 chunk 4: the SAME canonical shadow registry
|
||||
/// <c>LiveEntityCollisionBuilder</c> publishes into — retail's
|
||||
/// <c>add_shadows_to_cells</c> child-inheritance recursion (Contract B)
|
||||
/// needs an attached child's visual part array registered against its
|
||||
/// accepted parent's CURRENT retail CELLARRAY. Attached projections
|
||||
/// (equipped weapons/shields/ammunition) never carry independent
|
||||
/// collision shapes, so this owner ONLY calls
|
||||
/// <see cref="ShadowObjectRegistry.AttachChild"/>/<see cref="ShadowObjectRegistry.DetachChild"/>
|
||||
/// — never Register/RegisterMultiPart.
|
||||
/// </summary>
|
||||
private readonly ShadowObjectRegistry _shadows;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S2 chunk 4: the SAME <c>PhysicsDataCache</c> resolver
|
||||
/// pair <see cref="AcDream.Runtime.Physics.LiveEntityCollisionBuilder"/>
|
||||
/// uses for <c>ShadowShapeBuilder.FromSetupRenderParts</c> — a static and
|
||||
/// a live Setup sharing a GfxObj id can never disagree about its render
|
||||
/// geometry.
|
||||
/// </summary>
|
||||
private readonly PhysicsDataCache _physicsData;
|
||||
|
||||
private ParentAttachmentState Relations => _liveEntities.ParentAttachments;
|
||||
|
||||
/// <summary>Raised after the attached projection is fully registered.</summary>
|
||||
|
|
@ -98,7 +120,9 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
EntityEffectPoseRegistry poses,
|
||||
Func<ParentEvent.Parsed, bool> acceptParent,
|
||||
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
|
||||
withdrawProjection)
|
||||
withdrawProjection,
|
||||
ShadowObjectRegistry shadows,
|
||||
PhysicsDataCache physicsData)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
|
|
@ -108,6 +132,8 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
_acceptParent = acceptParent ?? throw new ArgumentNullException(nameof(acceptParent));
|
||||
_withdrawProjection = withdrawProjection
|
||||
?? throw new ArgumentNullException(nameof(withdrawProjection));
|
||||
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
||||
_physicsData = physicsData ?? throw new ArgumentNullException(nameof(physicsData));
|
||||
_parentOfAttached = static child => child.ParentRecord.ProjectionKey;
|
||||
_tickAttached = TickChild;
|
||||
_reconcileAttached = ReconcileChild;
|
||||
|
|
@ -659,6 +685,15 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
CaptureParentPresentation(attached, parentEntity);
|
||||
RuntimeEntityKey childKey = RequireProjectionKey(childRecord);
|
||||
_attachedByChild[childKey] = attached;
|
||||
// Campaign OVERHAUL S2 chunk 4: register the retail render-shadow
|
||||
// inheritance (Contract B) at the exact moment the attached
|
||||
// projection's WorldEntity is registered against its accepted
|
||||
// parent — retail's add_shadows_to_cells child recursion, ported at
|
||||
// the registry rather than reconstructed at render/bucket time.
|
||||
_shadows.AttachChild(
|
||||
entity.Id,
|
||||
parentEntity.Id,
|
||||
BuildChildRenderParts(childSetup, template, scale));
|
||||
_pendingUnparentByChild.Remove(childKey);
|
||||
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
|
||||
{
|
||||
|
|
@ -1147,6 +1182,38 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
return available;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S2 chunk 4: retail's per-part render membership
|
||||
/// (<c>CPartArray::AddPartsShadow</c> 0x00517e40 walks EVERY visual
|
||||
/// part, not just the BSP-only collision dispatch) for one attached
|
||||
/// child, via <see cref="ShadowShapeBuilder.FromSetupRenderParts"/> — the
|
||||
/// SAME builder and the SAME <see cref="_physicsData"/> resolver pair
|
||||
/// <c>LiveEntityCollisionBuilder.Build</c> uses for an ordinary live
|
||||
/// Setup, so a static, a live entity, and an attached child sharing a
|
||||
/// GfxObj id can never disagree about its render geometry.
|
||||
/// <paramref name="template"/>'s per-part GfxObj identity (post-
|
||||
/// AnimPartChanged, <see cref="BuildPartTemplate"/>) supplies
|
||||
/// <c>effectivePartGfxObjIds</c>; the child's own attach-composed pose
|
||||
/// carries no per-part local-position meaning for THIS product — retail
|
||||
/// never re-floods to place a child, it only inherits the root's
|
||||
/// CELLARRAY (Contract B) — so no pose override is threaded here,
|
||||
/// matching <c>FromSetupRenderParts</c>'s own placement-frame fallback.
|
||||
/// </summary>
|
||||
private IReadOnlyList<ShadowShape> BuildChildRenderParts(
|
||||
Setup setup, IReadOnlyList<MeshRef> template, float scale)
|
||||
{
|
||||
var effectiveGfxObjIds = new uint[template.Count];
|
||||
for (int i = 0; i < template.Count; i++)
|
||||
effectiveGfxObjIds[i] = template[i].GfxObjId;
|
||||
return ShadowShapeBuilder.FromSetupRenderParts(
|
||||
setup,
|
||||
scale,
|
||||
effectiveGfxObjIds,
|
||||
partPoseOverride: null,
|
||||
_physicsData.GetGfxObj,
|
||||
_physicsData.GetVisualBounds);
|
||||
}
|
||||
|
||||
private static PaletteOverride? BuildPaletteOverride(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
if (spawn.SubPalettes is not { Count: > 0 } subPalettes)
|
||||
|
|
@ -1388,6 +1455,12 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
}
|
||||
|
||||
_attachedByChild.Remove(key);
|
||||
// Campaign OVERHAUL S2 chunk 4: this is the single funnel every
|
||||
// withdrawal/unparent/teardown path (WithdrawAttachedProjection,
|
||||
// AdvanceUnparentTransition, TearDownRecordProjections, the pending-
|
||||
// subtree captures) commits through — the exact mirror of
|
||||
// AttachChild above.
|
||||
_shadows.DetachChild(child.Entity.Id);
|
||||
ProjectionRemoved?.Invoke(child.ChildRecord);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,8 +78,7 @@ internal sealed class RetailPViewRenderer
|
|||
Walk.WalkBuildingRegistry walkBuildings,
|
||||
Walk.WalkLandscapeAssembler walkLandscape,
|
||||
CellVisibility walkCellRegistry,
|
||||
ShadowObjectRegistry shadows,
|
||||
Func<uint, uint?>? findParentLocalId = null)
|
||||
ShadowObjectRegistry shadows)
|
||||
{
|
||||
_renderSceneShadow = renderSceneShadow
|
||||
?? throw new ArgumentNullException(nameof(renderSceneShadow));
|
||||
|
|
@ -91,8 +90,7 @@ internal sealed class RetailPViewRenderer
|
|||
?? throw new ArgumentNullException(nameof(walkCellRegistry));
|
||||
_walkWorldData = new Walk.WalkProductionWorldData(
|
||||
_walkBuildings,
|
||||
shadows ?? throw new ArgumentNullException(nameof(shadows)),
|
||||
findParentLocalId);
|
||||
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
||||
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
||||
_walkDrawExitSealsAction = DrawWalkExitSeals;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
{
|
||||
private readonly WalkBuildingRegistry _buildings;
|
||||
private readonly ShadowObjectRegistry _shadows;
|
||||
private readonly Func<uint, uint?> _findParentLocalId;
|
||||
private RenderSceneQuery _scene;
|
||||
private uint _tupleLandblockId;
|
||||
private int _renderCenterLbX;
|
||||
|
|
@ -107,17 +106,13 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
|
||||
internal WalkProductionWorldData(
|
||||
WalkBuildingRegistry buildings,
|
||||
ShadowObjectRegistry shadows,
|
||||
Func<uint, uint?>? findParentLocalId = null)
|
||||
ShadowObjectRegistry shadows)
|
||||
{
|
||||
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
||||
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
||||
_findParentLocalId = findParentLocalId ?? NoParentLocalId;
|
||||
_tryGetRetailCellArray = TryGetRetailCellArrayForEntity;
|
||||
}
|
||||
|
||||
private static uint? NoParentLocalId(uint _) => null;
|
||||
|
||||
private (bool Found, IReadOnlyList<uint> Cells) TryGetRetailCellArrayForEntity(uint entityId)
|
||||
{
|
||||
bool found = _shadows.TryGetRetailCellArray(entityId, out IReadOnlyList<uint> cells);
|
||||
|
|
@ -264,8 +259,11 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
ref readonly RenderProjectionRecord record = ref _dynamicSweepScratch[i];
|
||||
IReadOnlyList<uint> renderCells = ResolveDynamicRenderCells(
|
||||
in record,
|
||||
_tryGetRetailCellArray,
|
||||
_shadows.GetOwnerCells,
|
||||
_findParentLocalId);
|
||||
out bool usedFallback);
|
||||
if (usedFallback)
|
||||
UnregisteredStaticRenderFallbackCount++;
|
||||
BucketDynamicRecord(
|
||||
in record,
|
||||
renderCells,
|
||||
|
|
@ -277,48 +275,39 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsObj::add_shadows_to_cells</c> recursively passes the
|
||||
/// root object's same CELLARRAY to every child in its CHILDLIST. Attached
|
||||
/// projections deliberately own no independent collision rows in acdream,
|
||||
/// so walk their accepted parent chain until the root's retained array is
|
||||
/// found. Ordinary dynamics continue to consume only their own rows.
|
||||
/// Campaign OVERHAUL S2 chunk 4: one dynamic record's render-cell
|
||||
/// membership, borrowed from <see cref="ShadowObjectRegistry.TryGetRetailCellArray"/>
|
||||
/// — the SAME direct read <see cref="ResolveOutdoorStaticRenderCells"/>
|
||||
/// uses. An equipped child's retail array is now published by
|
||||
/// <see cref="ShadowObjectRegistry.AttachChild"/> at the registry (retail
|
||||
/// Contract B's <c>add_shadows_to_cells</c> child-inheritance recursion),
|
||||
/// so this no longer needs its own render-side parent-chain walk — the
|
||||
/// registry already resolved a nested attachment to its ultimate root.
|
||||
/// When the registry has no retail array yet for this entity (the same
|
||||
/// streaming-window race <see cref="ResolveIndoorStaticRenderCells"/>
|
||||
/// documents), the fallback is today's collision-flood
|
||||
/// <see cref="ShadowObjectRegistry.GetOwnerCells"/> answer, counted the
|
||||
/// same way as the static fallbacks via <paramref name="usedFallback"/>.
|
||||
/// </summary>
|
||||
internal static IReadOnlyList<uint> ResolveDynamicRenderCells(
|
||||
in RenderProjectionRecord record,
|
||||
Func<uint, (bool Found, IReadOnlyList<uint> Cells)> tryGetRetailCellArray,
|
||||
Func<uint, IReadOnlyList<uint>> getOwnerCells,
|
||||
Func<uint, uint?> findParentLocalId)
|
||||
out bool usedFallback)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tryGetRetailCellArray);
|
||||
ArgumentNullException.ThrowIfNull(getOwnerCells);
|
||||
ArgumentNullException.ThrowIfNull(findParentLocalId);
|
||||
|
||||
uint current = record.Source.LocalEntityId;
|
||||
IReadOnlyList<uint> cells = getOwnerCells(current);
|
||||
if (cells.Count > 0
|
||||
|| record.EntityPayload.CasterIdentity
|
||||
!= RenderCasterIdentityKind.EquippedChild)
|
||||
(bool found, IReadOnlyList<uint> cells) =
|
||||
tryGetRetailCellArray(record.Source.LocalEntityId);
|
||||
if (found)
|
||||
{
|
||||
usedFallback = false;
|
||||
return cells;
|
||||
}
|
||||
|
||||
// ParentAttachmentState rejects cycles. Keep a hard bound here so a
|
||||
// corrupted diagnostic callback still cannot stall a render frame.
|
||||
for (int depth = 0; depth < 64; depth++)
|
||||
{
|
||||
uint? parent = findParentLocalId(current);
|
||||
if (parent is not { } parentId
|
||||
|| parentId == 0u
|
||||
|| parentId == current)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = parentId;
|
||||
cells = getOwnerCells(current);
|
||||
if (cells.Count > 0)
|
||||
return cells;
|
||||
}
|
||||
|
||||
return Array.Empty<uint>();
|
||||
usedFallback = true;
|
||||
return getOwnerCells(record.Source.LocalEntityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue