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
|
|
@ -837,6 +837,115 @@ public sealed class ParticleSystemTests
|
|||
Assert.Single(destination);
|
||||
}
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 6: retail CPhysicsObj::add_particle_shadow_to_cell
|
||||
// (0x00514a70) gives an emitter exactly one shadow in ITS OWN current
|
||||
// cell. These pins drive CopyRenderableEmittersInCell purely through the
|
||||
// public ParticleSystem API — no ShadowObjectRegistry/owner concept is
|
||||
// involved at all.
|
||||
|
||||
[Fact]
|
||||
public void CellIndex_AddRemoveMove_TracksRenderableEmittersPerCell()
|
||||
{
|
||||
var sys = MakeSystem();
|
||||
var desc = new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000090u,
|
||||
Type = ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
};
|
||||
int handle = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 501u);
|
||||
sys.UpdateEmitterOwnerCell(handle, 0x0102_0001u);
|
||||
var visible = new HashSet<uint> { 0x0102_0001u, 0x0102_0002u };
|
||||
sys.ApplyRetailView(Vector3.Zero, visible, hasCompletedView: true);
|
||||
|
||||
var destination = new List<ParticleEmitter>();
|
||||
|
||||
// Add: the emitter is enumerated by its cell once renderable.
|
||||
sys.CopyRenderableEmittersInCell(ParticleRenderPass.Scene, 0x0102_0001u, destination);
|
||||
Assert.Equal(new[] { handle }, destination.Select(e => e.Handle));
|
||||
|
||||
// A different cell (even one that IS visible) enumerates nothing.
|
||||
sys.CopyRenderableEmittersInCell(ParticleRenderPass.Scene, 0x0102_0002u, destination);
|
||||
Assert.Empty(destination);
|
||||
|
||||
// Move: the handle relocates from the old cell's bucket to the new
|
||||
// one — an emitter is in exactly one cell at a time.
|
||||
sys.UpdateEmitterOwnerCell(handle, 0x0102_0002u);
|
||||
sys.CopyRenderableEmittersInCell(ParticleRenderPass.Scene, 0x0102_0001u, destination);
|
||||
Assert.Empty(destination);
|
||||
sys.CopyRenderableEmittersInCell(ParticleRenderPass.Scene, 0x0102_0002u, destination);
|
||||
Assert.Equal(new[] { handle }, destination.Select(e => e.Handle));
|
||||
|
||||
// Remove: a hard stop clears the cell bucket too.
|
||||
sys.StopEmitter(handle, fadeOut: false);
|
||||
sys.CopyRenderableEmittersInCell(ParticleRenderPass.Scene, 0x0102_0002u, destination);
|
||||
Assert.Empty(destination);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellIndex_MultipleEmittersInOneCell_EnumerateInSpawnOrder()
|
||||
{
|
||||
var sys = MakeSystem();
|
||||
var desc = new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000091u,
|
||||
Type = ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
};
|
||||
int first = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 601u);
|
||||
sys.UpdateEmitterOwnerCell(first, 0x0203_0005u);
|
||||
int second = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 602u);
|
||||
sys.UpdateEmitterOwnerCell(second, 0x0203_0005u);
|
||||
int unattachedInSameCell = sys.SpawnEmitter(desc, Vector3.Zero);
|
||||
sys.UpdateEmitterOwnerCell(unattachedInSameCell, 0x0203_0005u);
|
||||
|
||||
var visible = new HashSet<uint> { 0x0203_0005u };
|
||||
sys.ApplyRetailView(Vector3.Zero, visible, hasCompletedView: true);
|
||||
|
||||
var destination = new List<ParticleEmitter>();
|
||||
sys.CopyRenderableEmittersInCell(ParticleRenderPass.Scene, 0x0203_0005u, destination);
|
||||
|
||||
Assert.Equal(
|
||||
new[] { first, second, unattachedInSameCell },
|
||||
destination.Select(e => e.Handle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// THE chunk-6 pin: an emitter's cell membership does not depend on its
|
||||
/// attached owner ever having any OTHER visible presence. This is what
|
||||
/// makes the retail-Hidden portalling player's emitter still draw in its
|
||||
/// arrival cell — <c>ParticleSystem</c> has no coupling to
|
||||
/// <c>ShadowObjectRegistry</c> at all; presentation/view-eligibility (set
|
||||
/// by the hook sink / <see cref="ParticleSystem.ApplyRetailView"/>) is the
|
||||
/// ONLY gate, never the owner's render/collision membership.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CopyRenderableEmittersInCell_FindsAnEmitterWhoseOwnerHasNoOtherPresence()
|
||||
{
|
||||
var sys = MakeSystem();
|
||||
var desc = new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000092u,
|
||||
Type = ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
};
|
||||
// A large, otherwise-unused owner id stands in for a hidden/suspended
|
||||
// entity that publishes no registry rows anywhere; ParticleSystem
|
||||
// never looks at any such registry, so nothing needs to simulate one.
|
||||
const uint hiddenOwnerId = 0x5000_00FFu;
|
||||
const uint arrivalCellId = 0x8A02_0141u;
|
||||
int handle = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: hiddenOwnerId);
|
||||
sys.UpdateEmitterOwnerCell(handle, arrivalCellId);
|
||||
|
||||
var visible = new HashSet<uint> { arrivalCellId };
|
||||
sys.ApplyRetailView(Vector3.Zero, visible, hasCompletedView: true);
|
||||
|
||||
var destination = new List<ParticleEmitter>();
|
||||
sys.CopyRenderableEmittersInCell(ParticleRenderPass.Scene, arrivalCellId, destination);
|
||||
|
||||
Assert.Equal(new[] { handle }, destination.Select(e => e.Handle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderedIndexes_PreserveSpawnOrderAcrossHardRemovalAndPassFiltering()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue