feat(render): S2 chunk 6 — particle emitters draw by their own cell (add_particle_shadow_to_cell)
Owner G2 finding: the purple cloud around an arriving character no longer drew. The server keeps the player Hidden until acdream sends LoginComplete at reveal completion (retail-correct); the Hidden-state script's emitters spawn in the arrival cell and are view-eligible when the world appears, but the walk drew an owner's emitters only through the owner's registry rows, and a hidden owner's shadow is suspended. Retail's CPhysicsObj::add_particle_shadow_to_cell (0x00514a70) gives an emitter one shadow in its OWN current cell, drawn at that cell's turn regardless of the parent's hidden state (add_shadows_to_cells 0x00514aed skips the flood for state & 0x1000). Port: ParticleSystem keeps a per-pass cell -> renderable-handles index (maintained at every renderable/OwnerCellId change) and CopyRenderableEmittersInCell; ParticleRenderer.DrawForCell; the walk draws particles BY CELL at the existing turns (interior CellParticles, landscape LandscapeCellParticles), the events fire for every visited cell, and every owner-union particle path is deleted (UnionOwners/UnionNewOwners for particles, the outdoor drawn-owner dedupe, the executor's owner classification sets, the context ParticleOwnerIds members). The post-replay per-cell pass double-submitted the root flood's emitters and is deleted: an emitter draws once, at its cell's replay turn. AD-117 item 4 becomes a port note (the index lives in the particle system; an emitter is not a physics object in acdream). The temporary [pes-spawn]/[pes-vis] traces are removed and the ACDREAM_DUMP_PLAYSCRIPT row restored. Verified: timed arrival route logs/selfgate-20260903-062522-haze-chunk6, frame h02-arrive-400ms shows the cloud at the character in Facility Hub. Gates (Release): Core 4,987/4,987; Content 214/214; Runtime 1,884/1,884; App hermetic lane 6,760/6,760; App InstalledDat 217 pass / 2 pre-existing #383. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2d20ee917b
commit
f6b4584bf3
14 changed files with 400 additions and 336 deletions
|
|
@ -262,6 +262,42 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
FinishDraw(camera, renderPass);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws exactly one cell's renderable emitters — retail
|
||||
/// <c>CPhysicsObj::add_particle_shadow_to_cell</c> (0x00514a70): an
|
||||
/// emitter owns one shadow in its own current cell, drawn at that cell's
|
||||
/// object turn like any object, independent of its attached owner's
|
||||
/// registry membership (a hidden/suspended owner's emitter still draws).
|
||||
/// No portal-view clip is applied here — retail never clips a particle to
|
||||
/// a view; occlusion is the depth test at the alpha flush
|
||||
/// (<c>add_shadows_to_cells</c> 0x00514aed's particle branch skips the
|
||||
/// CELLARRAY flood/clip-planes entirely for this state bit).
|
||||
/// </summary>
|
||||
public void DrawForCell(
|
||||
ICamera camera,
|
||||
Vector3 cameraWorldPos,
|
||||
ParticleRenderPass renderPass,
|
||||
uint cellId,
|
||||
uint clipSlot = 0)
|
||||
{
|
||||
if (camera is null)
|
||||
return;
|
||||
|
||||
_particles.CopyRenderableEmittersInCell(renderPass, cellId, _scopedEmitterScratch);
|
||||
Matrix4x4.Invert(camera.View, out Matrix4x4 invView);
|
||||
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
|
||||
Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23));
|
||||
BuildDrawLists(
|
||||
cameraWorldPos,
|
||||
renderPass,
|
||||
cameraRight,
|
||||
cameraUp,
|
||||
emitterFilter: null,
|
||||
_scopedEmitterScratch,
|
||||
clipSlot);
|
||||
FinishDraw(camera, renderPass);
|
||||
}
|
||||
|
||||
private void FinishDraw(ICamera camera, ParticleRenderPass renderPass)
|
||||
{
|
||||
if (_submissionScratch.Count == 0)
|
||||
|
|
|
|||
|
|
@ -274,16 +274,11 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
|
||||
public void ClearInteriorDepth() => _clearInteriorDepth();
|
||||
|
||||
public void DrawStaticParticles(IReadOnlySet<uint> ownerIds) =>
|
||||
_passes.DrawLandscapeStaticParticles(
|
||||
_frame, new RetailPViewLandscapeStaticParticleContext(ownerIds));
|
||||
public void DrawStaticParticles(uint cellId) =>
|
||||
_passes.DrawLandscapeStaticParticles(_frame, cellId);
|
||||
|
||||
public void DrawCellParticles(uint cellId, IReadOnlySet<uint> ownerIds)
|
||||
{
|
||||
_passes.DrawCellParticles(
|
||||
_frame,
|
||||
new RetailPViewCellSliceContext(cellId, default, ownerIds));
|
||||
}
|
||||
public void DrawCellParticles(uint cellId) =>
|
||||
_passes.DrawCellParticles(_frame, cellId);
|
||||
|
||||
public void DrawExitSeals() => _drawExitSeals();
|
||||
|
||||
|
|
|
|||
|
|
@ -41,37 +41,6 @@ internal sealed class RetailPViewCellSource : IRetailPViewCellSource
|
|||
_cells.TryGetCell(cellId, out LoadedCell? cell) ? cell : null;
|
||||
}
|
||||
|
||||
internal sealed class RetailPViewParticleClassifications
|
||||
{
|
||||
private readonly HashSet<uint> _outdoor = [];
|
||||
private readonly HashSet<uint> _visible = [];
|
||||
private readonly HashSet<uint> _dynamics = [];
|
||||
|
||||
public IReadOnlySet<uint> Outdoor => _outdoor;
|
||||
public HashSet<uint> Visible => _visible;
|
||||
public HashSet<uint> Dynamics => _dynamics;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_outdoor.Clear();
|
||||
_visible.Clear();
|
||||
_dynamics.Clear();
|
||||
}
|
||||
|
||||
public void ReplaceOutdoor(IReadOnlyList<WorldEntity> owners)
|
||||
{
|
||||
_outdoor.Clear();
|
||||
foreach (WorldEntity owner in owners)
|
||||
_outdoor.Add(owner.Id);
|
||||
}
|
||||
|
||||
public void ReplaceOutdoor(IReadOnlySet<uint> ownerIds)
|
||||
{
|
||||
_outdoor.Clear();
|
||||
_outdoor.UnionWith(ownerIds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Concrete GL implementation of the named passes ordered by
|
||||
/// <see cref="RetailPViewRenderer"/>. It owns reusable pass-local particle
|
||||
|
|
@ -107,14 +76,17 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
private readonly RetailAlphaQueue _alpha;
|
||||
private readonly WorldRenderDiagnostics _diagnostics;
|
||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||
private readonly RetailPViewParticleClassifications _particleClassifications = new();
|
||||
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
||||
/// <summary>
|
||||
/// Borrowed until the next late landscape pass. The outdoor-root post-world
|
||||
/// particle pass consumes this synchronously before another PView frame.
|
||||
/// </summary>
|
||||
// Campaign OVERHAUL S2 chunk 6: the walk now draws every owner's scene
|
||||
// particles at its own cell's turn (DrawLandscapeStaticParticles/
|
||||
// DrawCellParticles below), so there is no longer a per-frame outdoor
|
||||
// owner set to hand the flat-world safety path's post-world particle
|
||||
// pass — that pass's outdoorOwnerIds parameter is unused on every live
|
||||
// code path (WorldScenePassExecutor.DrawPostWorldParticles) and this
|
||||
// property now always reports empty.
|
||||
private static readonly IReadOnlySet<uint> NoOutdoorSceneParticleEntityIds = new HashSet<uint>();
|
||||
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds =>
|
||||
_particleClassifications.Outdoor;
|
||||
NoOutdoorSceneParticleEntityIds;
|
||||
|
||||
public RetailPViewPassExecutor(
|
||||
IWorldPassSurface surface,
|
||||
|
|
@ -150,7 +122,9 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_particleClassifications.BeginFrame();
|
||||
// Campaign OVERHAUL S2 chunk 6: no per-frame particle-owner
|
||||
// classification to reset any more — particle draws are cell-scoped
|
||||
// and read live from ParticleSystem's own retained cell index.
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the shared dispatcher, for
|
||||
|
|
@ -178,7 +152,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
TryAbort(_frameGlState.RestoreFrameDefaults);
|
||||
TryAbort(() => _envCells.SetClipRouting(null));
|
||||
TryAbort(_entities.ClearClipRouting);
|
||||
TryAbort(_particleClassifications.BeginFrame);
|
||||
TryAbort(_noSceneParticleEntityIds.Clear);
|
||||
if (failures is { Count: > 0 })
|
||||
throw new AggregateException("Retail PView pass abort failed.", failures);
|
||||
|
|
@ -311,26 +284,27 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
|
||||
public void DrawLandscapeStaticParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewLandscapeStaticParticleContext context)
|
||||
uint cellId)
|
||||
{
|
||||
// One unclipped submission per owner per frame. Retail never clips a
|
||||
// One unclipped submission per cell per frame. Retail never clips a
|
||||
// particle to a portal view — its polys join the one alpha list during
|
||||
// the owner cell's walk turn and the depth test at the flush decides
|
||||
// occlusion (FlushAlphaList @0x0059D2E0). The former per-slice call
|
||||
// with the slice's clip slot both hardware-cut effects at aperture
|
||||
// boundaries and double-submitted owners visible in two slices.
|
||||
// Retail CPhysicsObj::add_particle_shadow_to_cell (0x00514a70): an
|
||||
// emitter owns one shadow in its OWN current cell, so this is a cell
|
||||
// lookup, not an owner union — a hidden/suspended owner's emitter
|
||||
// still draws here.
|
||||
DisableClipDistances();
|
||||
|
||||
_particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds);
|
||||
if (_particleClassifications.Outdoor.Count > 0
|
||||
&& _particles is not null
|
||||
&& _particleRenderer is not null)
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
_particleRenderer.DrawForOwners(
|
||||
_particleRenderer.DrawForCell(
|
||||
frame.Camera,
|
||||
frame.CameraWorldPosition,
|
||||
ParticleRenderPass.Scene,
|
||||
_particleClassifications.Outdoor,
|
||||
cellId,
|
||||
clipSlot: 0);
|
||||
}
|
||||
|
||||
|
|
@ -385,29 +359,22 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
|
||||
public void DrawCellParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewCellSliceContext context)
|
||||
uint cellId)
|
||||
{
|
||||
if (_particles is null
|
||||
|| _particleRenderer is null
|
||||
|| context.ParticleOwnerIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<uint> visible = _particleClassifications.Visible;
|
||||
visible.Clear();
|
||||
visible.UnionWith(context.ParticleOwnerIds);
|
||||
if (visible.Count == 0)
|
||||
if (_particles is null || _particleRenderer is null)
|
||||
return;
|
||||
|
||||
DisableClipDistances();
|
||||
// Retail never clips cell particles to a portal view: the owner
|
||||
// cell's walls own occlusion via the depth test at the alpha flush.
|
||||
_particleRenderer.DrawForOwners(
|
||||
// CPhysicsObj::add_particle_shadow_to_cell (0x00514a70) draws an
|
||||
// emitter in its OWN current cell, so this is a cell lookup, not an
|
||||
// owner union — a hidden/suspended owner's emitter still draws here.
|
||||
_particleRenderer.DrawForCell(
|
||||
frame.Camera,
|
||||
frame.CameraWorldPosition,
|
||||
ParticleRenderPass.Scene,
|
||||
visible,
|
||||
cellId,
|
||||
clipSlot: 0);
|
||||
DisableClipDistances();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,6 @@ internal sealed class RetailPViewRenderer
|
|||
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
||||
private readonly RetailPViewFrameResult _frameResultScratch = new();
|
||||
|
||||
private static readonly ClipViewSlice NoClipSlice =
|
||||
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
|
||||
private static readonly IReadOnlySet<uint> NoParticleOwners =
|
||||
new HashSet<uint>();
|
||||
|
||||
private readonly HashSet<uint> _cellParticleOwnerScratch = new();
|
||||
|
||||
// MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across
|
||||
// frames instead of `new HashSet<uint>(pvFrame.OrderedVisibleCells)` every
|
||||
// call. Every walk consumer reads it synchronously in this frame.
|
||||
|
|
@ -579,38 +572,12 @@ internal sealed class RetailPViewRenderer
|
|||
// building's pre-punch AlphaBarrier drains the farther content
|
||||
// against still-true depth before its punch stamps far-Z.
|
||||
|
||||
// Cell-stage particle owners for the interior root's OWN flood cells
|
||||
// (non-look-in — the walk draws their statics too, via
|
||||
// OnInteriorFloodDrawTurn/EmitCellTurn, so the retired CellStatic
|
||||
// route's cell-particle submission needs the same re-sourcing).
|
||||
// These stay POST-replay: interior emitters draw in the final world
|
||||
// scope where the cell walls already own depth (retail's cell-walk
|
||||
// insertion). Look-in cells get their OWN per-cell union in
|
||||
// DrawBuildingLookInDynamics so a static owner is never submitted
|
||||
// twice.
|
||||
_cellParticleOwnerScratch.Clear();
|
||||
foreach (uint cellId in driver.VisitedCells)
|
||||
{
|
||||
if (driver.LookInCells.Contains(cellId))
|
||||
continue;
|
||||
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
|
||||
}
|
||||
if (_cellParticleOwnerScratch.Count > 0)
|
||||
{
|
||||
passes.DrawCellParticles(
|
||||
ctx,
|
||||
new RetailPViewCellSliceContext(0u, NoClipSlice, _cellParticleOwnerScratch));
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnionRecordOwners(
|
||||
Walk.WalkFrameStaticRecords records, HashSet<uint> destination)
|
||||
{
|
||||
foreach (RenderProjectionRecord record in records.Records)
|
||||
{
|
||||
if (record.Source.LocalEntityId != 0)
|
||||
destination.Add(record.Source.LocalEntityId);
|
||||
}
|
||||
// Campaign OVERHAUL S2 chunk 6: an emitter draws ONCE, at its own
|
||||
// cell's turn inside Replay (WalkFrameEventKind.CellParticles fires for
|
||||
// every visited interior cell — root flood and look-in alike), exactly
|
||||
// retail's add_particle_shadow_to_cell (0x00514a70) membership. The
|
||||
// former post-replay per-cell pass double-submitted the root flood's
|
||||
// emitters and is deleted.
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
||||
|
|
@ -857,24 +824,9 @@ public sealed class RetailPViewFrameResult
|
|||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene-particle owners for ONE unclipped landscape-stage submission (the
|
||||
/// union of every outside slice's cone survivors). Mesh alpha for the same
|
||||
/// owners is already queued by the entity routes; retail inserts each
|
||||
/// emitter's polys into the single alpha list once, during its owner cell's
|
||||
/// walk turn, with no portal-view clip.
|
||||
/// </summary>
|
||||
public readonly record struct RetailPViewLandscapeStaticParticleContext(
|
||||
IReadOnlySet<uint> ParticleOwnerIds);
|
||||
|
||||
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
||||
/// outside-stage dynamics to mesh-draw, plus the particle owners not already
|
||||
/// submitted at a pre-building barrier.</summary>
|
||||
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> Dynamics);
|
||||
|
||||
public readonly record struct RetailPViewCellSliceContext(
|
||||
uint CellId,
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlySet<uint> ParticleOwnerIds);
|
||||
|
|
|
|||
|
|
@ -124,16 +124,19 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// portal depth writes, and later depth-tested repaint own visibility.</summary>
|
||||
void DrawCellShell(uint cellId);
|
||||
|
||||
/// <summary>One landscape cell's or building shell's static-owner
|
||||
/// particle submission, at its own walk turn — see
|
||||
/// <see cref="WalkFrameEventKind.StaticParticles"/> for the retail
|
||||
/// positional invariant this carries (the #132 falls containment).</summary>
|
||||
void DrawStaticParticles(IReadOnlySet<uint> ownerIds);
|
||||
/// <summary>One landscape (land) cell's own-cell particle emitters, at
|
||||
/// its own walk turn — see <see cref="WalkFrameEventKind.StaticParticles"/>
|
||||
/// for the retail positional invariant this carries (the #132 falls
|
||||
/// containment). Retail <c>CPhysicsObj::add_particle_shadow_to_cell</c>
|
||||
/// (0x00514a70) draws an emitter at its own current cell's object turn
|
||||
/// regardless of its attached owner's registry membership, so this is a
|
||||
/// cell lookup, not an owner union.</summary>
|
||||
void DrawStaticParticles(uint cellId);
|
||||
|
||||
/// <summary>Submits one indoor cell's static + dynamic particle owners at
|
||||
/// that cell's own object-list turn. Meshes have already entered the
|
||||
/// ordered stream before this leaf event.</summary>
|
||||
void DrawCellParticles(uint cellId, IReadOnlySet<uint> ownerIds);
|
||||
/// <summary>Submits one indoor cell's own-cell particle emitters at that
|
||||
/// cell's own object-list turn. Meshes have already entered the ordered
|
||||
/// stream before this leaf event.</summary>
|
||||
void DrawCellParticles(uint cellId);
|
||||
|
||||
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
|
||||
/// (pc:432731-432732) between the outside stage and the interior root's
|
||||
|
|
@ -265,11 +268,10 @@ internal enum WalkFrameEventKind : byte
|
|||
ExitSeals,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawStaticParticles"/> —
|
||||
/// ONE landscape cell's (<see cref="WalkFrameEvent.CellId"/>) or ONE
|
||||
/// building shell's (<see cref="WalkFrameEvent.Building"/>) static-owner
|
||||
/// particle submission, emitted AT ITS OWN WALK TURN. Retail's falls
|
||||
/// containment is positional: an outdoor emitter's polys join the one
|
||||
/// alpha list during its owner cell's <c>DrawObjCell</c> in the
|
||||
/// ONE landscape (land) cell's (<see cref="WalkFrameEvent.CellId"/>)
|
||||
/// own-cell particle emitters, emitted AT ITS OWN WALK TURN. Retail's
|
||||
/// falls containment is positional: an outdoor emitter's polys join the
|
||||
/// one alpha list during its owner cell's <c>DrawObjCell</c> in the
|
||||
/// far-to-near landscape walk, so every nearer building's pre-punch
|
||||
/// alpha barrier (<c>DrawBuilding</c> @0x0059f2a0's
|
||||
/// <c>FlushAlphaList</c> @0x0059f30b) drains the already-queued FARTHER
|
||||
|
|
@ -278,11 +280,21 @@ internal enum WalkFrameEventKind : byte
|
|||
/// closure ran AFTER every punch — the barriers fired over an empty
|
||||
/// queue and the falls drained against punched-far aperture pixels (the
|
||||
/// cathedral bleed; the old pipeline's user-verified #132 fix
|
||||
/// `e102fb36` encoded the same invariant).</summary>
|
||||
/// `e102fb36` encoded the same invariant). A building's own shell fires
|
||||
/// no event of this kind any more — its interior emitters live in their
|
||||
/// own EnvCells and draw at those cells' own <see cref="CellParticles"/>
|
||||
/// turns (Campaign OVERHAUL S2 chunk 6, retail
|
||||
/// <c>CPhysicsObj::add_particle_shadow_to_cell</c> 0x00514a70: an emitter
|
||||
/// owns one shadow in its OWN current cell, never its parent's).</summary>
|
||||
StaticParticles,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellParticles"/> —
|
||||
/// <see cref="WalkFrameEvent.CellId"/> is the cell.</summary>
|
||||
/// <see cref="WalkFrameEvent.CellId"/> is the cell. Fires at EVERY
|
||||
/// visited interior cell's own object-list turn regardless of whether
|
||||
/// that cell has any visible static/dynamic owner record: an emitter
|
||||
/// draws at its own cell's turn independent of its attached owner's
|
||||
/// registry membership (a hidden/suspended owner's emitter still shows —
|
||||
/// Campaign OVERHAUL S2 chunk 6, the portal-haze fix).</summary>
|
||||
CellParticles,
|
||||
}
|
||||
|
||||
|
|
@ -340,15 +352,13 @@ internal readonly record struct WalkLookInTurn(
|
|||
internal readonly struct WalkFrameEvent
|
||||
{
|
||||
private WalkFrameEvent(
|
||||
WalkFrameEventKind kind, int intArg, uint cellId, float floatArg, WalkPolygon? polygon,
|
||||
WalkBuilding? building = null)
|
||||
WalkFrameEventKind kind, int intArg, uint cellId, float floatArg, WalkPolygon? polygon)
|
||||
{
|
||||
Kind = kind;
|
||||
IntArg = intArg;
|
||||
CellId = cellId;
|
||||
FloatArg = floatArg;
|
||||
Polygon = polygon;
|
||||
Building = building;
|
||||
}
|
||||
|
||||
internal WalkFrameEventKind Kind { get; }
|
||||
|
|
@ -361,11 +371,6 @@ internal readonly struct WalkFrameEvent
|
|||
|
||||
internal WalkPolygon? Polygon { get; }
|
||||
|
||||
/// <summary><see cref="WalkFrameEventKind.StaticParticles"/> only: the
|
||||
/// building whose shell statics' owners submit at this turn; null for a
|
||||
/// landscape cell's turn (then <see cref="CellId"/> names the cell).</summary>
|
||||
internal WalkBuilding? Building { get; }
|
||||
|
||||
internal static WalkFrameEvent Mark(int exclusiveEnd) =>
|
||||
new(WalkFrameEventKind.StreamMark, exclusiveEnd, 0, 0f, null);
|
||||
|
||||
|
|
@ -390,9 +395,6 @@ internal readonly struct WalkFrameEvent
|
|||
internal static WalkFrameEvent LandscapeCellParticles(uint cellId) =>
|
||||
new(WalkFrameEventKind.StaticParticles, 0, cellId, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent BuildingShellParticles(WalkBuilding building) =>
|
||||
new(WalkFrameEventKind.StaticParticles, 0, 0, 0f, null, building);
|
||||
|
||||
internal static WalkFrameEvent CellParticles(uint cellId) =>
|
||||
new(WalkFrameEventKind.CellParticles, 0, cellId, 0f, null);
|
||||
|
||||
|
|
@ -516,7 +518,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
private readonly List<int> _floodViewRouteScratch = new();
|
||||
private WalkPlane _lookInCyPlane;
|
||||
|
||||
private readonly HashSet<uint> _outdoorParticleOwnersDrawnThisFrame = new();
|
||||
private readonly HashSet<uint> _cellShellsDrawnThisFrame = new();
|
||||
|
||||
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
|
||||
|
|
@ -582,33 +583,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
return routeIndex;
|
||||
}
|
||||
|
||||
// Replay scratch for StaticParticles events (sequential replay — one
|
||||
// reused set is safe).
|
||||
private readonly HashSet<uint> _staticParticleOwnerScratch = new();
|
||||
|
||||
private static void UnionOwners(
|
||||
in WalkFrameStaticRecords records, HashSet<uint> destination)
|
||||
{
|
||||
foreach (RenderProjectionRecord record in records.Records)
|
||||
{
|
||||
if (record.Source.LocalEntityId != 0)
|
||||
destination.Add(record.Source.LocalEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnionNewOwners(
|
||||
in WalkFrameStaticRecords records,
|
||||
HashSet<uint> destination,
|
||||
HashSet<uint> drawnOnce)
|
||||
{
|
||||
foreach (RenderProjectionRecord record in records.Records)
|
||||
{
|
||||
uint ownerId = record.Source.LocalEntityId;
|
||||
if (ownerId != 0 && drawnOnce.Add(ownerId))
|
||||
destination.Add(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
internal List<WalkBuilding> VisitedBuildings { get; } = new();
|
||||
|
||||
internal HashSet<uint> VisitedLandscapeCellIds { get; } = new();
|
||||
|
|
@ -678,14 +652,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_visibleClipSlotScratch.Clear();
|
||||
_floodViewRouteScratch.Clear();
|
||||
_dispatcher.EndWalkPartFrame();
|
||||
_outdoorParticleOwnersDrawnThisFrame.Clear();
|
||||
_cellShellsDrawnThisFrame.Clear();
|
||||
_lookInCyPlane = default;
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
VisitedLandscapeCellIds.Clear();
|
||||
InteriorFloodCells.Clear();
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
_cellViewRouteIndex = 0;
|
||||
_landscapeViewRouteIndex = -1;
|
||||
}
|
||||
|
|
@ -814,7 +786,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_lookInPlanes.Clear();
|
||||
_visibleClipSlotScratch.Clear();
|
||||
_lookInCyPlane = ctx.CyPlane;
|
||||
_outdoorParticleOwnersDrawnThisFrame.Clear();
|
||||
_cellShellsDrawnThisFrame.Clear();
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
|
|
@ -925,41 +896,15 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_leafRenderer.DrawExitSeals();
|
||||
break;
|
||||
case WalkFrameEventKind.StaticParticles:
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
if (e.Building is WalkBuilding shellOwner)
|
||||
{
|
||||
UnionOwners(
|
||||
_worldData.GetBuildingShellStatics(shellOwner),
|
||||
_staticParticleOwnerScratch);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnionNewOwners(
|
||||
_worldData.GetOutdoorStatics(e.CellId),
|
||||
_staticParticleOwnerScratch,
|
||||
_outdoorParticleOwnersDrawnThisFrame);
|
||||
UnionNewOwners(
|
||||
_worldData.GetOutdoorDynamics(e.CellId),
|
||||
_staticParticleOwnerScratch,
|
||||
_outdoorParticleOwnersDrawnThisFrame);
|
||||
}
|
||||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
|
||||
// Retail CPhysicsObj::add_particle_shadow_to_cell
|
||||
// (0x00514a70): an emitter owns one shadow in its OWN
|
||||
// current cell, drawn at that cell's object turn
|
||||
// regardless of any owner's registry membership —
|
||||
// this is a cell lookup, not an owner union.
|
||||
_leafRenderer.DrawStaticParticles(e.CellId);
|
||||
break;
|
||||
case WalkFrameEventKind.CellParticles:
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
UnionOwners(
|
||||
_worldData.GetCellStatics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
UnionOwners(
|
||||
_worldData.GetCellDynamics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
{
|
||||
_leafRenderer.DrawCellParticles(
|
||||
e.CellId,
|
||||
_staticParticleOwnerScratch);
|
||||
}
|
||||
_leafRenderer.DrawCellParticles(e.CellId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1181,17 +1126,17 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
}
|
||||
// FW4 (the #132 positional invariant): this cell's emitter owners
|
||||
// submit AT THIS TURN, so nearer buildings' pre-punch barriers
|
||||
// drain them against still-true depth — see
|
||||
// WalkFrameEventKind.StaticParticles. Mark first so the cell's own
|
||||
// meshes flush ahead of its particle submission (retail's
|
||||
// per-object DrawObjCell order).
|
||||
if (HasAnyOwner(records) || HasAnyOwner(dynamics))
|
||||
{
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
|
||||
}
|
||||
// FW4 (the #132 positional invariant): this cell's emitters submit
|
||||
// AT THIS TURN, so nearer buildings' pre-punch barriers drain them
|
||||
// against still-true depth — see WalkFrameEventKind.StaticParticles.
|
||||
// Mark first so the cell's own meshes flush ahead of its particle
|
||||
// submission (retail's per-object DrawObjCell order). Fires
|
||||
// unconditionally: retail's add_particle_shadow_to_cell draws an
|
||||
// emitter at its own cell's turn independent of whether that cell
|
||||
// has any visible static/dynamic owner record (Campaign OVERHAUL S2
|
||||
// chunk 6 — a suspended/hidden owner's emitter must still show).
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
|
||||
}
|
||||
|
||||
void IWalkEventSink.OnLandscapeViews(WalkPortalView activeViews)
|
||||
|
|
@ -1202,16 +1147,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
CaptureViews(0, activeViews);
|
||||
}
|
||||
|
||||
private static bool HasAnyOwner(in WalkFrameStaticRecords records)
|
||||
{
|
||||
foreach (RenderProjectionRecord record in records.Records)
|
||||
{
|
||||
if (record.Source.LocalEntityId != 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void IWalkEventSink.OnBuildingTurn(WalkBuilding building)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(building);
|
||||
|
|
@ -1257,14 +1192,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
MarkAlphaIfGrown();
|
||||
}
|
||||
}
|
||||
// FW4 (the #132 positional invariant): the building's own shell
|
||||
// emitters submit at the shell turn, after the shell content
|
||||
// flushes — see WalkFrameEventKind.StaticParticles.
|
||||
if (HasAnyOwner(shell))
|
||||
{
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.BuildingShellParticles(building));
|
||||
}
|
||||
// Campaign OVERHAUL S2 chunk 6: a building's shell fires no particle
|
||||
// turn of its own any more. Retail's add_particle_shadow_to_cell
|
||||
// (0x00514a70) gives an emitter exactly one shadow in ITS OWN
|
||||
// current cell — a building's interior emitters live in their own
|
||||
// EnvCells and draw at those cells' own CellParticles turns, never
|
||||
// at their parent building's shell turn.
|
||||
}
|
||||
|
||||
void IWalkEventSink.OnPunchGeometry(
|
||||
|
|
@ -1495,8 +1428,11 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
alphaSubmissions: _alphaSubmissions);
|
||||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
if (HasAnyOwner(records) || HasAnyOwner(dynamics))
|
||||
_events.Add(WalkFrameEvent.CellParticles(cellId));
|
||||
// Fires unconditionally: retail's add_particle_shadow_to_cell draws
|
||||
// an emitter at its own cell's turn independent of whether that cell
|
||||
// has any visible static/dynamic owner record (Campaign OVERHAUL S2
|
||||
// chunk 6 — a suspended/hidden owner's emitter must still show).
|
||||
_events.Add(WalkFrameEvent.CellParticles(cellId));
|
||||
}
|
||||
|
||||
private void CaptureCellViews(uint cellId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue