acdream/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
Erik f6b4584bf3 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>
2026-09-03 06:28:09 +02:00

92 lines
3.5 KiB
C#

using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Tests.Architecture;
namespace AcDream.App.Tests.Rendering;
public sealed class RetailPViewPassExecutorTests
{
[Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{
Assert.DoesNotContain(
typeof(RetailPViewFrameInput).GetProperties(),
property => typeof(Delegate).IsAssignableFrom(property.PropertyType));
FieldInfo[] fields = typeof(RetailPViewPassExecutor).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(CellVisibility));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameInput));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameResult));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(ClipFrameAssembly));
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
[Fact]
public void Concrete_executor_brackets_terrain_diagnostics()
{
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
"DrawWalkTerrainSlice",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
int diagnosticsBegin = RequiredCallIndex(
landscapeCalls,
typeof(TerrainDrawDiagnosticsController),
nameof(TerrainDrawDiagnosticsController.Begin));
int terrainDraw = RequiredCallIndex(
landscapeCalls,
typeof(TerrainModernRenderer),
nameof(TerrainModernRenderer.Draw));
int diagnosticsComplete = RequiredCallIndex(
landscapeCalls,
typeof(TerrainDrawDiagnosticsController),
nameof(TerrainDrawDiagnosticsController.Complete));
Assert.True(diagnosticsBegin < terrainDraw);
Assert.True(terrainDraw < diagnosticsComplete);
}
[Fact]
public void Frame_composition_constructs_one_walk_executor()
{
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
int executor = RequiredCallIndex(
calls,
typeof(RetailPViewPassExecutor),
".ctor");
int renderer = RequiredCallIndex(
calls,
typeof(WorldScenePViewRenderer),
".ctor");
Assert.True(executor < renderer);
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
&& call.Target.Name == ".ctor");
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(WorldScenePViewRenderer)
&& call.Target.Name == ".ctor");
}
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(
index >= 0,
$"Expected call to {declaringType.Name}.{methodName}.");
return index;
}
}