checkpoint(render): preserve pre-overhaul investigation state

This commit is contained in:
Erik 2026-09-01 18:04:24 +02:00
parent e880860291
commit b3b7d922f1
45 changed files with 3168 additions and 619 deletions

View file

@ -111,6 +111,25 @@ public class ClipFrameLayoutTests
Assert.Equal(0u, ReadUInt(bytes, 0));
}
[Fact]
public void GetSlotPlanes_BorrowsTheExactPackedClipRegion()
{
using ClipFrame frame = ClipFrame.NoClip();
Vector4[] expected =
[
new(1f, 2f, 3f, 4f),
new(-5f, 6f, -7f, 8f),
];
int slot = frame.AppendSlot(expected);
ReadOnlySpan<Vector4> actual = frame.GetSlotPlanes(checked((uint)slot));
Assert.Equal(expected.Length, actual.Length);
Assert.Equal(expected[0], actual[0]);
Assert.Equal(expected[1], actual[1]);
Assert.Equal(0, frame.GetSlotPlanes(0).Length);
}
[Fact]
public void AppendSlot_EmptyPlaneList_PacksNoClipSlot_Count0()
{

View file

@ -1,68 +0,0 @@
using System;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #129 — doors/doorways leak through terrain and houses from over a landblock
/// away. The punch's mark pass (#117, AD-18) biased the aperture fan toward
/// the viewer by a CONSTANT 0.0005 NDC. NDC depth is non-linear: a constant
/// NDC bias b spans ≈ b·d²·(fn)/(f·n) meters of eye depth at eye distance d
/// — 0.125 m at 5 m but ~190 m at a landblock (znear 0.1), so distant
/// occluders in front of an aperture passed the mark and were far-Z punched:
/// the door-shaped leak. The fix caps the bias's eye-space span
/// (PortalDepthMaskRenderer.MarkBiasNdc): identical to the validated constant
/// below the ~10 m crossover, never more than the cap beyond it.
/// </summary>
public class Issue129PunchBiasTests
{
private const float Near = PortalDepthMaskRenderer.CameraNearPlaneMeters; // 0.1 (retail znear)
private const float Far = 5000f;
/// <summary>Eye-depth span (meters) covered by an NDC depth bias b at eye
/// distance d: ndc(d) = f(dn)/((fn)d) ⇒ d(ndc) inverse ⇒
/// span = b·d²·(fn)/(f·n) (exact for small b via the derivative).</summary>
private static float EyeSpanMeters(float biasNdc, float d) =>
biasNdc * d * d * (Far - Near) / (Far * Near);
[Fact]
public void OldConstantBias_SpansMetersAtALandblock_TheLeak()
{
// The refuted form (documentation of WHY the constant was wrong):
// 0.0005 NDC at ~one landblock spans far more eye depth than any
// occluder separation — everything in front got punched.
Assert.True(EyeSpanMeters(0.0005f, 192f) > 100f);
// ...while at close range it was a sane sliver:
Assert.InRange(EyeSpanMeters(0.0005f, 5f), 0.05f, 0.30f);
}
[Fact]
public void CappedBias_MatchesValidatedConstant_AtCloseRange()
{
// Below the crossover the T5-validated constant must win unchanged —
// this preserves the #108 grass coverage bit-for-bit.
foreach (float d in new[] { 0.5f, 1f, 3f, 5f, 8f, 9.9f })
Assert.Equal(0.0005f, PortalDepthMaskRenderer.MarkBiasNdc(d), 6);
}
[Fact]
public void CappedBias_EyeSpanNeverExceedsCap_AtAnyDistance()
{
for (float d = 1f; d <= 400f; d += 1f)
{
float span = EyeSpanMeters(PortalDepthMaskRenderer.MarkBiasNdc(d), d);
Assert.True(span <= PortalDepthMaskRenderer.PunchMarkBiasEyeCapMeters * 1.02f,
FormattableString.Invariant($"bias spans {span:F2} m of eye depth at d={d} m"));
}
}
[Fact]
public void CappedBias_At200m_CannotReachOccluders()
{
// The reported #129 distance: occluder separations are tens of
// meters; the punch reach must stay under the 0.5 m cap.
float span = EyeSpanMeters(PortalDepthMaskRenderer.MarkBiasNdc(200f), 200f);
Assert.True(span <= 0.51f, FormattableString.Invariant($"span {span:F3} m at 200 m"));
}
}

View file

@ -4,12 +4,19 @@ using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Core.Meshing;
using DatReaderWriter;
using DatReaderWriter.Options;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
using DatSetup = DatReaderWriter.DBObjs.Setup;
using Xunit;
using Xunit.Abstractions;
@ -113,7 +120,7 @@ public class Issue177StairDescentCameraFloodTests
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
foreach (uint low in new uint[] { 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 })
foreach (uint low in new uint[] { 0x015E, 0x015F, 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 })
{
uint id = FacilityHub | low;
var envCell = dats.Get<DatEnvCell>(id);
@ -160,6 +167,113 @@ public class Issue177StairDescentCameraFloodTests
}
}
[Fact]
public void FacilityStairAssembly_RegisterAcross015FTo015EWithoutCollisionRows()
{
string? datDir = ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
const uint setupId = 0x02000623u;
DatSetup setup = Assert.IsType<DatSetup>(dats.Get<DatSetup>(setupId));
PhysicsEngine engine = BuildHubEngine(dats);
PhysicsDataCache cache = Assert.IsType<PhysicsDataCache>(engine.DataCache);
IReadOnlyList<MeshRef> meshRefs = SetupMesh.Flatten(setup);
foreach (MeshRef meshRef in meshRefs)
{
DatGfxObj gfxObj = Assert.IsType<DatGfxObj>(
dats.Get<DatGfxObj>(meshRef.GfxObjId));
cache.CacheGfxObj(meshRef.GfxObjId, gfxObj);
}
DatEnvCell parent = Assert.IsType<DatEnvCell>(
dats.Get<DatEnvCell>(FacilityHub | 0x015Fu));
var stair = Assert.Single(
parent.StaticObjects,
staticObject =>
staticObject.Id == setupId);
var rootPosition = new Vector3(
stair.Frame.Origin.X,
stair.Frame.Origin.Y,
stair.Frame.Origin.Z);
Quaternion rootRotation = stair.Frame.Orientation;
List<ShadowShape> parts = ShadowShapeBuilder.FromStaticRenderParts(
meshRefs,
cache.GetGfxObj,
cache.GetVisualBounds,
out bool hasPhysicsBsp);
Assert.True(hasPhysicsBsp);
Assert.NotEmpty(parts);
const uint syntheticOwner = 0x7F00DEADu;
Assert.Empty(engine.ShadowObjects.GetOwnerCells(syntheticOwner));
IReadOnlyList<uint> cells = engine.ShadowObjects.ComputeStaticRenderCells(
FacilityHub | 0x015Fu,
rootPosition,
rootRotation,
parts);
ShadowShape part = parts[0];
ShadowPartBox box = ShadowPartBox.FromShape(
part,
rootPosition,
rootRotation);
LoadedCell rootCell = CornerFloodReplayTests.LoadCell(
dats,
FacilityHub | 0x015Fu);
box.RefitToLocal(rootCell.InverseWorldTransform, out Vector3 boxMin, out Vector3 boxMax);
_out.WriteLine(FormattableString.Invariant(
$"stair root=({rootPosition.X:F3},{rootPosition.Y:F3},{rootPosition.Z:F3}) localBox=({boxMin.X:F3},{boxMin.Y:F3},{boxMin.Z:F3})..({boxMax.X:F3},{boxMax.Y:F3},{boxMax.Z:F3}) cells=[{string.Join(',', cells.Select(static id => $"0x{id:X8}"))}]"));
Assert.Contains(FacilityHub | 0x015Fu, cells);
Assert.Contains(FacilityHub | 0x015Eu, cells);
Assert.Empty(engine.ShadowObjects.GetOwnerCells(syntheticOwner));
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionId projectionId = RenderProjectionId.FromRaw(0x48A02000u);
RenderTransform transform = RenderTransform.FromRoot(
rootPosition,
rootRotation,
1f);
RenderProjectionRecord projection = new RenderProjectionRecord() with
{
Id = projectionId,
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Transform = transform,
PreviousTransform = new PreviousRenderTransform(transform.LocalToWorld),
Residency = new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(FacilityHub | 0x015Fu),
FacilityHub,
FacilityHub | 0x015Fu),
Flags = RenderProjectionFlags.Draw,
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x48A02000u,
SourceId = setupId,
ParentCellId = FacilityHub | 0x015Fu,
},
EntityPayload = new RenderEntityPayload(meshRefs, null, false),
};
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(
new WalkBuildingRegistry(),
engine.ShadowObjects);
worldData.BeginFrame(
scene.OpenQuery(),
FacilityHub,
renderCenterLbX: 0x8A,
renderCenterLbY: 0x02);
Assert.Contains(
worldData.GetCellStatics(FacilityHub | 0x015Eu).Records,
record => record.Id == projectionId);
}
/// <summary>
/// #177 re-diagnosis (fix#1 failed the visual gate): the vanish flips on a SLIGHT turn +
/// depends on zoom, so it is a gaze/eye knife-edge, not the eye-squarely-in-opening case

View file

@ -0,0 +1,67 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Tests.Rendering.Gpu;
namespace AcDream.App.Tests.Rendering;
public sealed class PortalDepthMaskRetailStateTests
{
[Fact]
public void SealAndPunchUseOneRetailAlwaysWritePipelineAndOneDrawEach()
{
using var device = new RecordingGpuDevice();
var frames = new GpuDeviceFrameLifetime(device);
var scope = new VulkanWorldPassScope(sampleCount: 1);
using var renderer = new PortalDepthMaskRenderer(device, frames, scope);
RecordingGpuPipeline pipeline = Assert.Single(device.CreatedPipelines);
Assert.Equal("portal-depth-write", pipeline.Description.Name);
Assert.Equal(
new GpuDepthState(Test: true, Write: true, GpuCompareOp.Always),
pipeline.Description.Depth);
Assert.False(pipeline.Description.StencilTest);
Assert.False(pipeline.Description.ColorWrite);
Assert.Equal(GpuCullMode.None, pipeline.Description.Cull);
frames.BeginFrame();
renderer.BeginFrame(frameSlot: 0);
using (IGpuPassEncoder pass = frames.CurrentFrame!.BeginPass(
GpuPassDescription.BackbufferClear(
"portal-depth-retail-state",
Vector4.Zero,
sampleCount: 1)))
using (scope.Publish(pass))
{
Vector3[] triangle =
[
new(-1f, -1f, 1f),
new(1f, -1f, 1f),
new(0f, 1f, 1f),
];
renderer.DrawDepthFan(
triangle,
Matrix4x4.Identity,
ReadOnlySpan<Vector4>.Empty,
forceFarZ: true);
renderer.DrawDepthFan(
triangle,
Matrix4x4.Identity,
ReadOnlySpan<Vector4>.Empty,
forceFarZ: false);
}
frames.EndFrame();
Assert.Equal(
2,
device.Calls.OfType<GpuRecordedPipelineBind>()
.Count(call => call.PipelineName == "portal-depth-write"));
Assert.Equal(2, device.Calls.OfType<GpuRecordedDraw>().Count());
Assert.Collection(
device.Calls.OfType<GpuRecordedPushConstants>(),
punch => Assert.Equal(1, punch.Constants.RenderPass),
seal => Assert.Equal(0, seal.Constants.RenderPass));
Assert.Empty(device.Calls.OfType<GpuRecordedStencil>());
}
}

View file

@ -45,16 +45,16 @@ public sealed class WalkFrameDriverTests
RetailAlphaQueue? alpha = null) : IWalkFrameLeafRenderer
{
public readonly List<WalkPolygon> Punches = new();
public readonly List<(uint CellId, uint ClipSlot)> Shells = new();
public readonly List<uint> Shells = new();
public readonly List<int> AlphaPendingAtBarrier = new();
public void DrawSky() => log.Add("SKY");
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
public void DrawCellShell(uint cellId, uint clipSlot)
public void DrawCellShell(uint cellId)
{
Shells.Add((cellId, clipSlot));
Shells.Add(cellId);
log.Add($"SHELL:{cellId:x8}");
}
@ -267,7 +267,9 @@ public sealed class WalkFrameDriverTests
var leaf = new RecordingLeafRenderer(log);
var trace = new RecordingTrace(log);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, trace);
using ClipFrame clipFrame = ClipFrame.NoClip();
var driver = new WalkFrameDriver(
fx.Dispatcher, leaf, worldData, trace, clipFrame);
var walk = new RetailFrameWalk();
// A minimal, no-op landscape (1x1 window, the one slot unpublished)
// — matches RetailFrameWalkTests' own exit-view fixture. LScape::draw
@ -296,6 +298,79 @@ public sealed class WalkFrameDriverTests
Assert.All(mdiCalls, c => Assert.Equal(1u, c.DrawCount));
// Nothing dropped: every populated record reached exactly one indirect draw.
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
// Exit seals replay through the exact portal_view slices captured by
// this walk. The legacy PortalVisibilityFrame is not consulted.
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(0));
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(1));
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(0, 0).Length);
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(1, 0).Length);
Assert.True(clipFrame.SlotCount >= 3);
}
[Fact]
public void InteriorFloodAfterLandscape_RearmsRetailPartStampForPostClearCellRepaint()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObj = 0x0200_0021UL;
const uint outdoorCellId = 0xF4180009u;
const uint interiorCellId = 0xF4180112u;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08100021u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
RenderProjectionRecord sharedPart = MakeRecord(
0x4F41806Cu,
0,
Vector3.Zero,
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)]);
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[outdoorCellId] =
new WalkFrameStaticRecords(new[] { sharedPart }, 0xF418u);
worldData.CellStaticsByCell[interiorCellId] =
new WalkFrameStaticRecords(new[] { sharedPart }, 0xF418u);
var ctx = new TestContext();
var interiorCell = new WalkCell { CellId = interiorCellId };
interiorCell.PushView();
WalkCopyView.AppendFullViewportQuad(
interiorCell.TopView,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
ctx.Cells[interiorCellId] = interiorCell;
var driver = new WalkFrameDriver(
fx.Dispatcher,
new RecordingLeafRenderer(log),
worldData,
new RecordingTrace(log));
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var landscapeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
landscapeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
sink.OnLandscapeViews(landscapeViews);
sink.OnLandscapeCellTurn(outdoorCellId);
sink.OnInteriorFloodDrawTurn([interiorCellId]);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
List<GpuRecordedMultiDrawIndirect> mdiCalls =
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
Assert.Equal(2, mdiCalls.Count);
Assert.All(mdiCalls, call => Assert.Equal(1u, call.DrawCount));
Assert.Equal(2, mdiCalls.Sum(call => (int)call.DrawCount));
Assert.Equal(2, log.Count(entry => entry == "FLUSH:1:OutdoorStatic"
|| entry == "FLUSH:1:CellStatic"));
}
// ── Deliverable: the ov==0 interior case — no exit view survives, so
@ -479,11 +554,7 @@ public sealed class WalkFrameDriverTests
Assert.Equal([0x104u], driver.LookInCells);
Assert.Collection(
leaf.Shells,
shell =>
{
Assert.Equal(0x104u, shell.CellId);
Assert.NotEqual(0u, shell.ClipSlot);
});
shell => Assert.Equal(0x104u, shell));
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
Vector2 capturedCenter = Vector2.Zero;
@ -515,6 +586,101 @@ public sealed class WalkFrameDriverTests
Assert.Equal(3, mdiCalls.Sum(c => (int)c.DrawCount));
}
[Fact]
public void RepeatedFloodTurns_DrawEnvCellShellWholeOncePerRetailFrameStamp()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint cellId = 0xF4180112u;
var cell = new WalkCell { CellId = cellId };
cell.PushView();
WalkCopyView.AppendFullViewportQuad(
cell.TopView,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
ctx.Cells[cellId] = cell;
var driver = new WalkFrameDriver(
fx.Dispatcher,
leaf,
new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnInteriorFloodDrawTurn([cellId]);
sink.OnInteriorFloodDrawTurn([cellId]);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal([cellId], leaf.Shells);
Assert.Equal(1, log.Count(entry => entry == "SHELL:f4180112"));
Assert.Equal(2, log.Count(entry => entry == "CLEAR"));
Assert.Equal(2, log.Count(entry => entry == "SEALS"));
}
[Fact]
public void LandscapeStampBoundary_RearmsWholeShellForPostClearRootRepaint()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint cellId = 0xF4180112u;
var cell = new WalkCell { CellId = cellId };
cell.PushView();
WalkCopyView.AppendFullViewportQuad(
cell.TopView,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
ctx.Cells[cellId] = cell;
var driver = new WalkFrameDriver(
fx.Dispatcher,
leaf,
new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
// LScape::draw has begun. A building look-in reached this cell before
// PView::DrawCells advances m_nFrameStamp and clears interior depth.
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var landscapeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
landscapeViews,
ctx.Rays,
ctx.WorldViewpoint,
ctx.ViewportWidth,
ctx.ViewportHeight);
sink.OnLandscapeViews(landscapeViews);
sink.OnBuildingTurn(new WalkBuilding());
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [cellId]));
// The same shell must draw again after the retail stamp increment and
// full depth clear; otherwise the pre-clear color survives unpaired
// with depth and bleeds through the root's walls.
sink.OnInteriorFloodDrawTurn([cellId]);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal([cellId, cellId], leaf.Shells);
Assert.Equal(2, log.Count(entry => entry == "SHELL:f4180112"));
Assert.True(
log.IndexOf("SHELL:f4180112") < log.IndexOf("CLEAR"),
"The look-in shell must precede the interior clear.");
Assert.True(
log.LastIndexOf("SHELL:f4180112") > log.IndexOf("SEALS"),
"The rearmed root shell must repaint after the clear and seals.");
}
// ── Fail-loud: a DrawCells turn with no preceding DrawInside/Building
// turn is a walk/driver desync, not a silent skip. ─────────────────────

View file

@ -236,7 +236,7 @@ public sealed class WalkProductionWorldConformanceTests
[Theory]
[InlineData(0xF4180100u, 36.166267f, 79.828407f)]
[InlineData(0xF4180101u, 36.391270f, 72.167931f)]
public void Cathedral_transition_keeps_south_hall_draws_inside_the_authored_aperture(
public void Cathedral_transition_keeps_south_hall_admission_bounded_for_depth_occlusion(
uint cameraCellId,
float x,
float y)
@ -244,11 +244,12 @@ public sealed class WalkProductionWorldConformanceTests
// Owner's exact 2026-08-31 repro: the remote player and special NPC
// are parented in 0xF4180112, behind opaque cathedral walls. Retail
// hides them on BOTH sides of the 0x100 <-> 0x101 transition. The walk
// legitimately reaches 0x112 through one authored building aperture;
// the regression was submitting each admitted mesh with slot 0, so the
// whole player/NPC escaped that aperture. Preserve the installed-DAT
// fact this fix depends on: every admitted route is a real, bounded
// portal polygon, never a pass-all zero-plane route.
// legitimately reaches 0x112 through one authored building aperture.
// Retail uses that cone only for coarse sphere admission, then draws
// each accepted shell/object whole; the complete intervening wall
// shell hides the remote actors through ordinary depth. Preserve the
// installed-DAT fact this depends on: every admitted route is a real,
// bounded portal polygon, never a pass-all zero-plane route.
var pose = new WalkOraclePose(
cameraCellId,
new Vector3(x, y, 169.804993f),

View file

@ -6,6 +6,77 @@ namespace AcDream.App.Tests.Rendering.Walk;
public sealed class WalkProductionWorldDataTests
{
[Fact]
public void BuildingShellBucketCellId_PortalLessBuildingUsesLandscapePositionCell()
{
const uint positionCellId = 0xF4180011u;
RenderProjectionRecord record = Record(
id: 0xCF418060u,
position: new Vector3(53.57f, 13.874f, 160f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0xCF418060u,
SourceId = 0x01001FD3u,
EffectCellId = positionCellId,
BuildingShellAnchorCellId = 0,
},
EntityPayload = new RenderEntityPayload() with
{
IsBuildingShell = true,
},
};
var building = new WalkBuilding
{
PositionCellId = positionCellId,
Portals = [],
};
Assert.Equal(
positionCellId,
WalkProductionWorldData.BuildingShellBucketCellId(in record));
Assert.Equal(
positionCellId,
WalkProductionWorldData.BuildingShellBucketCellId(building));
}
[Fact]
public void BuildingShellBucketCellId_PortalBearingBuildingKeepsInteriorAnchor()
{
const uint anchorCellId = 0xF4180112u;
RenderProjectionRecord record = Record(
id: 0xCF41805Fu,
position: new Vector3(36f, 13.8349f, 160f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0xCF41805Fu,
SourceId = 0x01001FB7u,
EffectCellId = 0xF4180009u,
BuildingShellAnchorCellId = anchorCellId,
},
EntityPayload = new RenderEntityPayload() with
{
IsBuildingShell = true,
},
};
var building = new WalkBuilding
{
PositionCellId = 0xF4180009u,
Portals =
[
new WalkBldPortal { OtherCellId = anchorCellId },
],
};
Assert.Equal(
anchorCellId,
WalkProductionWorldData.BuildingShellBucketCellId(in record));
Assert.Equal(
anchorCellId,
WalkProductionWorldData.BuildingShellBucketCellId(building));
}
[Fact]
public void BucketOutdoorRecord_UsesEveryOutdoorPhysicsShadowCellAcrossLandblockEdge()
{
@ -44,6 +115,160 @@ public sealed class WalkProductionWorldDataTests
Assert.Equal(record, Assert.Single(buckets[0xF07F0002u]));
}
[Fact]
public void BucketIndoorRecord_InstallsCrossCellPartInBothInteriorCells()
{
RenderProjectionRecord record = Record(
id: 0x48A02035u,
position: new Vector3(55.25f, -46.47f, -3f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x48A02035u,
ParentCellId = 0x8A02015Fu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketIndoorRecord(
in record,
[0x8A02015Fu, 0x8A02015Eu],
indoor,
outdoor);
Assert.Equal([0x8A02015Eu, 0x8A02015Fu], indoor.Keys.Order());
Assert.All(indoor.Values, bucket => Assert.Equal(record, Assert.Single(bucket)));
Assert.Empty(outdoor);
}
[Fact]
public void BucketIndoorRecord_CanCrossAnExitIntoLandscapeCell()
{
RenderProjectionRecord record = Record(
id: 0x48A02035u,
position: Vector3.Zero) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x48A02035u,
ParentCellId = 0x8A02015Fu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketIndoorRecord(
in record,
[0x8A02015Fu, 0x8A020021u],
indoor,
outdoor);
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
Assert.Equal(record, Assert.Single(outdoor[0x8A020021u]));
}
[Fact]
public void BucketDynamicRecord_InstallsMultipartPlayerInEveryCrossedInteriorCell()
{
RenderProjectionRecord record = Record(
id: 0x000F4243u,
position: new Vector3(58.81f, -49.42f, -0.85f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x000F4243u,
ParentCellId = 0x8A02015Eu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketDynamicRecord(
in record,
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
indoor,
outdoor,
renderCenterLbX: 0x8A,
renderCenterLbY: 0x02);
Assert.Equal(
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
indoor.Keys.Order());
Assert.All(
indoor.Values,
bucket => Assert.Equal(record, Assert.Single(bucket)));
Assert.Empty(outdoor);
}
[Fact]
public void BucketDynamicRecord_UnregisteredInteriorEffectFallsBackToParentCell()
{
RenderProjectionRecord record = Record(
id: 0x00001234u,
position: Vector3.Zero) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x00001234u,
ParentCellId = 0x8A02015Fu,
},
};
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
WalkProductionWorldData.BucketDynamicRecord(
in record,
Array.Empty<uint>(),
indoor,
outdoor,
renderCenterLbX: 0x8A,
renderCenterLbY: 0x02);
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
Assert.Empty(outdoor);
}
[Fact]
public void ResolveDynamicRenderCells_EquippedDescendantInheritsRootCellArray()
{
RenderProjectionRecord wand = Record(
id: 0x800045EEu,
position: Vector3.Zero) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0x800045EEu,
ParentCellId = 0xF4180104u,
},
EntityPayload = new RenderEntityPayload() with
{
CasterIdentity = RenderCasterIdentityKind.EquippedChild,
},
};
var owners = new Dictionary<uint, IReadOnlyList<uint>>
{
[0x5000000Au] = [0xF4180106u, 0xF4180104u],
};
var parents = new Dictionary<uint, uint>
{
[0x800045EEu] = 0x80000461u,
[0x80000461u] = 0x5000000Au,
};
IReadOnlyList<uint> cells =
WalkProductionWorldData.ResolveDynamicRenderCells(
in wand,
id => owners.TryGetValue(id, out IReadOnlyList<uint>? value)
? value
: Array.Empty<uint>(),
id => parents.TryGetValue(id, out uint value)
? value
: null);
Assert.Equal([0xF4180106u, 0xF4180104u], cells);
}
private static RenderProjectionRecord Record(uint id, Vector3 position) =>
new RenderProjectionRecord() with
{

View file

@ -69,7 +69,9 @@ public sealed class WalkStaticStreamPopulatorTests
Vector3 position,
IReadOnlyList<MeshRef> meshRefs,
bool isBuildingShell = false,
uint parentCellId = 0u) =>
uint parentCellId = 0u,
RenderCasterIdentityKind casterIdentity =
RenderCasterIdentityKind.Unclassified) =>
new(
Id: RenderProjectionId.FromRaw(localEntityId),
ProjectionClass: RenderProjectionClass.OutdoorStatic,
@ -97,7 +99,8 @@ public sealed class WalkStaticStreamPopulatorTests
EntityPayload: new RenderEntityPayload(
MeshRefs: meshRefs,
PaletteOverride: null,
IsBuildingShell: isBuildingShell));
IsBuildingShell: isBuildingShell,
CasterIdentity: casterIdentity));
private static ObjectRenderBatch MakeBatch(
uint surfaceId,
@ -274,7 +277,161 @@ public sealed class WalkStaticStreamPopulatorTests
}
[Fact]
public void ClassifyEntityForWalk_EmitsOneGpuClippedInstancePerPortalViewSlice()
public void ClassifyEntityForWalk_FrameScopeStampsEachAdmittedSetupPartOncePerRetailPass()
{
using var fx = new DispatcherFixture();
const ulong setupGfxObj = 0x1000_0020UL;
const ulong headGfxObj = 0x0100_0021UL;
const ulong torsoGfxObj = 0x0100_0022UL;
InjectRenderData(fx.Manager, headGfxObj, MakeFlatMesh(
MakeBatch(0x08000021u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
InjectRenderData(fx.Manager, torsoGfxObj, MakeFlatMesh(
MakeBatch(0x08000022u, TranslucencyKind.Opaque, 3, 4, 6, 2)));
InjectRenderData(fx.Manager, setupGfxObj, new ObjectRenderData
{
IsSetup = true,
SetupParts = new List<(ulong GfxObjId, Matrix4x4 Transform)>
{
(headGfxObj, Matrix4x4.CreateTranslation(0, 0, 2)),
(torsoGfxObj, Matrix4x4.CreateTranslation(0, 0, 1)),
},
});
RenderProjectionRecord record = MakeRecord(
220,
0,
Vector3.Zero,
[new MeshRef((uint)setupGfxObj, Matrix4x4.Identity)]);
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
fx.Dispatcher.BeginWalkPartFrame();
try
{
fx.Dispatcher.ClassifyEntityForWalk(
in record, 0x8C04u, batches, selectionParts);
Assert.Equal(2, batches.Count);
Assert.Equal(2, selectionParts.Count);
batches.Clear();
selectionParts.Clear();
fx.Dispatcher.ClassifyEntityForWalk(
in record, 0x8C04u, batches, selectionParts);
Assert.Empty(batches);
Assert.Empty(selectionParts);
// PView::DrawCells @0x005A4886 increments m_nFrameStamp after
// the landscape pass and before its interior-cell repaint. The
// same parts may therefore submit once again in the second pass.
fx.Dispatcher.AdvanceWalkPartPassStamp();
fx.Dispatcher.ClassifyEntityForWalk(
in record, 0x8C04u, batches, selectionParts);
Assert.Equal(2, batches.Count);
Assert.Equal(2, selectionParts.Count);
}
finally
{
fx.Dispatcher.EndWalkPartFrame();
}
// Direct classifier calls outside a production walk frame remain
// independent, as the conformance/referee tests require.
batches.Clear();
selectionParts.Clear();
fx.Dispatcher.ClassifyEntityForWalk(
in record, 0x8C04u, batches, selectionParts);
Assert.Equal(2, batches.Count);
Assert.Equal(2, selectionParts.Count);
}
[Fact]
public void ClassifyEntityForWalk_FrameScopeDoesNotStampPortalRejectedPart()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0100_0023UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08000023u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
RenderProjectionRecord record = MakeRecord(
221,
0,
Vector3.Zero,
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)]);
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
fx.Dispatcher.BeginWalkPartFrame();
try
{
fx.Dispatcher.ClassifyEntityForWalk(
in record,
0x8C04u,
batches,
selectionParts,
liveDynamic: true,
lookInViews: new FixedWalkViews(),
lookInRouteIndex: 0);
Assert.Empty(batches);
Assert.Empty(selectionParts);
fx.Dispatcher.ClassifyEntityForWalk(
in record,
0x8C04u,
batches,
selectionParts,
liveDynamic: true,
lookInViews: new FixedWalkViews(7u),
lookInRouteIndex: 1);
Assert.Single(batches);
Assert.Single(selectionParts);
}
finally
{
fx.Dispatcher.EndWalkPartFrame();
}
}
[Fact]
public void ClassifyEntityForWalk_LocalPlayerBypassesDrawnPartStampLikeRetail()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0100_0024UL;
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
MakeBatch(0x08000024u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
RenderProjectionRecord player = MakeRecord(
222,
0x5000_0001u,
Vector3.Zero,
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)],
casterIdentity: RenderCasterIdentityKind.LocalPlayer);
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
fx.Dispatcher.BeginWalkPartFrame();
try
{
fx.Dispatcher.ClassifyEntityForWalk(
in player, 0xF418u, batches, selectionParts,
liveDynamic: true);
Assert.Single(batches);
Assert.Single(selectionParts);
batches.Clear();
selectionParts.Clear();
fx.Dispatcher.ClassifyEntityForWalk(
in player, 0xF418u, batches, selectionParts,
liveDynamic: true);
Assert.Single(batches);
Assert.Single(selectionParts);
}
finally
{
fx.Dispatcher.EndWalkPartFrame();
}
}
[Fact]
public void ClassifyEntityForWalk_PortalViewsAdmitOneCompleteUnclippedMesh()
{
using var fx = new DispatcherFixture();
const ulong gfxObj = 0x0100_0013UL;
@ -298,7 +455,8 @@ public sealed class WalkStaticStreamPopulatorTests
lookInRouteIndex: 0,
lookInCellId: 0x8C040112u);
Assert.Equal([7u, 9u], batches.Select(static batch => batch.ClipSlot));
WbDrawDispatcher.WalkClassifiedBatch batch = Assert.Single(batches);
Assert.Equal(0u, batch.ClipSlot);
Assert.Single(selectionParts);
}

View file

@ -254,7 +254,7 @@ public sealed class WorldRenderFrameBuilderTests
}
[Fact]
public void Runtime_environment_copies_visible_cells_and_clear_restores_unscoped_lights()
public void Runtime_environment_uses_resident_lights_independent_of_drawable_cells()
{
const uint visibleCell = 0x01010100u;
const uint hiddenCell = 0x01010101u;
@ -291,7 +291,7 @@ public sealed class WorldRenderFrameBuilderTests
environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null);
Assert.Contains(visibleLight, lighting.PointSnapshot);
Assert.DoesNotContain(hiddenLight, lighting.PointSnapshot);
Assert.Contains(hiddenLight, lighting.PointSnapshot);
environment.ClearDrawableCells();
environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null);

View file

@ -1,12 +1,46 @@
using AcDream.Content.Pak;
using DatReaderWriter;
using DatReaderWriter.Options;
using GfxObj = DatReaderWriter.DBObjs.GfxObj;
namespace AcDream.Content.Tests;
[Trait("Lane", "PreparedPackage")]
public sealed class InstalledPreparedCollisionCatalogTests
{
[Fact]
public void InstalledPackage_FacilityHubStepsCarryRetailDrawingSphere()
{
const uint stairGfxObjId = 0x0100_00DEu;
string? datDir = ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=PreparedPackage requires installed retail DATs and a validated acdream.pak; see docs/release-gate.md.");
string packagePath = ResolvePackagePath(datDir);
if (!File.Exists(packagePath))
Assert.Fail("Lane=PreparedPackage requires installed retail DATs and a validated acdream.pak; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
using var adapter = new DatCollectionAdapter(dats);
using var source = new PakPreparedAssetSource(packagePath, adapter);
GfxObj stair = Assert.IsType<GfxObj>(dats.Get<GfxObj>(stairGfxObjId));
PreparedAssetReadResult prepared = source.Read(
PreparedAssetRequest.GfxObj(stairGfxObjId));
Assert.Equal(PreparedAssetReadStatus.Loaded, prepared.Status);
Assert.NotNull(prepared.Data);
Assert.NotNull(prepared.Data!.SelectionSphere);
DatReaderWriter.Types.DrawingBSPNode root = Assert.IsType<
DatReaderWriter.Types.DrawingBSPNode>(stair.DrawingBSP.Root);
Assert.Equal(
root.BoundingSphere.Origin,
prepared.Data.SelectionSphere.Origin);
Assert.Equal(
root.BoundingSphere.Radius,
prepared.Data.SelectionSphere.Radius);
}
[Fact]
public void InstalledPackage_ContainsAndReadsCanonicalCollisionKeys()
{

View file

@ -35,6 +35,47 @@ public sealed class MeshExtractorSolidFaceExtractionTests
private const uint SurfaceTextureId = 0x05000001u;
private const uint RenderSurfaceId = 0x06000001u;
[Fact]
public void PrepareMeshData_CarriesAuthoredDrawingBspSphereInsteadOfVertexAabbSphere()
{
var authored = new Sphere
{
Origin = new Vector3(-0.25f, 0.125f, 0.5f),
Radius = 3.125f,
};
GfxObj gfxObj = BuildQuadGfxObj(SolidSurfaceId, noPos: true);
gfxObj.DrawingBSP = new DrawingBSPTree
{
Root = new DrawingBSPNode { BoundingSphere = authored },
};
var dats = new FakeMeshExtractorDats();
dats.RegisterRootGfxObj(GfxObjId, gfxObj);
dats.Register(SolidSurfaceId, new Surface
{
Type = SurfaceType.Base1Solid,
ColorValue = new ColorARGB
{
Alpha = 255,
Red = 12,
Green = 34,
Blue = 56,
},
});
var extractor = new MeshExtractor(
dats,
NullLogger.Instance,
sideStagedSink: null);
ObjectMeshData? mesh = extractor.PrepareMeshData(
GfxObjId,
isSetup: false);
Assert.NotNull(mesh);
Assert.NotNull(mesh!.SelectionSphere);
Assert.Equal(authored.Origin, mesh.SelectionSphere.Origin);
Assert.Equal(authored.Radius, mesh.SelectionSphere.Radius);
}
/// <summary>
/// One quad polygon, NoPos + Base1Solid: the exact shape of the windmill
/// axle's own polygons. Must now extract to 4 vertices / 6 indices in a

View file

@ -276,8 +276,7 @@ public sealed class LightManagerTests
LightSource[] expected = FullSortOracle(
registered,
Vector3.Zero,
visibleCells: null);
Vector3.Zero);
manager.BuildPointLightSnapshot(Vector3.Zero);
Assert.Equal(expected, manager.PointSnapshot);
@ -324,23 +323,15 @@ public sealed class LightManagerTests
manager.Register(light);
}
IReadOnlySet<uint>? visibleCells = scenario % 2 == 0
? new HashSet<uint>
{
0xAAAA0101u,
0xAAAA0103u,
}
: null;
Vector3 player = new(
random.Next(-4, 5),
random.Next(-4, 5),
random.Next(-2, 3));
LightSource[] expected = FullSortOracle(
registered,
player,
visibleCells);
player);
manager.BuildPointLightSnapshot(player, visibleCells);
manager.BuildPointLightSnapshot(player);
Assert.Equal(expected, manager.PointSnapshot);
}
@ -369,15 +360,12 @@ public sealed class LightManagerTests
registered.Add(light);
manager.Register(light);
}
IReadOnlySet<uint> visibleCells =
new HashSet<uint> { fountainRoom, corridor };
Vector3 player = new(4.25f, -1.5f, 0.7f);
LightSource[] expected = FullSortOracle(
registered,
player,
visibleCells);
player);
manager.BuildPointLightSnapshot(player, visibleCells);
manager.BuildPointLightSnapshot(player);
Assert.Equal(LightManager.MaxGlobalLights, manager.PointSnapshot.Count);
Assert.Equal(expected, manager.PointSnapshot);
@ -415,71 +403,11 @@ public sealed class LightManagerTests
Assert.Equal(0, allocated);
}
// ── Visible-cell scoping (A7.L1, 2026-07-09 — the Town Network starvation fix) ──
// BuildPointLightSnapshot's player-nearest cap sorts by raw Euclidean distance,
// which is not a reliable proxy for "same room" in a dense, maze-like hub: a
// fixture on the other side of a wall can be geometrically closer than the
// player's own room's torches. The Town Network fountain room (463 registered
// fixtures, cap 128) went dark because far-denser, closer-in-a-straight-line
// corridor fixtures won the cap over the room's own lights. Filtering candidacy
// by the frame's actual visible-cell set (the render already computes this)
// fixes it without touching the distance-sort anchor (still the PLAYER, per the
// #176 correction — camera anchoring is what caused the earlier flicker).
[Fact]
public void BuildPointLightSnapshot_VisibleCellScoping_RoomLightsSurviveOverEuclideanCloserInvisibleCell()
public void BuildPointLightSnapshot_UsesAllResidentLights()
{
var mgr = new LightManager();
// A different, NOT-visible cell packed with fixtures that are, in raw
// straight-line distance, closer to the player than the room's own
// torches (e.g. a corridor on the other side of a wall).
const uint otherCellId = 0xAAAA0102u;
for (int i = 0; i < LightManager.MaxGlobalLights + 50; i++)
mgr.Register(MakePoint(new Vector3(1f + i * 0.001f, 1f, 0), range: 5f, ownerId: (uint)(i + 1), cellId: otherCellId));
// The player's own room: a handful of torches, each FARTHER in raw
// distance than every "other cell" fixture above, but the only cell
// actually visible from the player's viewpoint this frame.
const uint roomCellId = 0xAAAA0101u;
var roomTorches = new LightSource[5];
for (int i = 0; i < roomTorches.Length; i++)
{
roomTorches[i] = MakePoint(new Vector3(50f + i, 0, 0), range: 15f, cellId: roomCellId);
mgr.Register(roomTorches[i]);
}
var visibleCells = new HashSet<uint> { roomCellId };
mgr.BuildPointLightSnapshot(playerWorldPos: Vector3.Zero, visibleCells);
foreach (var torch in roomTorches)
Assert.Contains(torch, mgr.PointSnapshot);
}
[Fact]
public void BuildPointLightSnapshot_VisibleCellScoping_CellLessLightAlwaysIncluded()
{
// The viewer fill light (CellId==0) must survive scoping unconditionally —
// retail's per-frame add_dynamic_light(&viewer_light, ...) is unconditional
// (LightManager.UpdateViewerLight's doc comment).
var mgr = new LightManager();
var viewerFill = MakePoint(new Vector3(0, 0, 2), range: 15f, cellId: 0u);
mgr.Register(viewerFill);
var otherRoom = MakePoint(new Vector3(2, 0, 0), range: 5f, cellId: 0xBEEFu);
mgr.Register(otherRoom);
var visibleCells = new HashSet<uint> { 0xF00Du }; // neither light's cell
mgr.BuildPointLightSnapshot(Vector3.Zero, visibleCells);
Assert.Contains(viewerFill, mgr.PointSnapshot);
Assert.DoesNotContain(otherRoom, mgr.PointSnapshot);
}
[Fact]
public void BuildPointLightSnapshot_NoVisibleCellsArg_UnscopedLegacyBehavior()
{
// Outdoor / no-clipRoot callers omit visibleCells — every registered lit
// light stays a candidate, exactly the pre-A7.L1 behavior.
// Retail walks the resident EnvCell registry. A camera-root transition
// cannot make either resident cell stop contributing candidates.
var mgr = new LightManager();
mgr.Register(MakePoint(new Vector3(1, 0, 0), 5f, cellId: 0xAAAAu));
mgr.Register(MakePoint(new Vector3(2, 0, 0), 5f, cellId: 0xBBBBu));
@ -685,8 +613,7 @@ public sealed class LightManagerTests
private static LightSource[] FullSortOracle(
IReadOnlyList<LightSource> registered,
Vector3 player,
IReadOnlySet<uint>? visibleCells)
Vector3 player)
{
var ranked = new List<OracleRank>();
for (int index = 0; index < registered.Count; index++)
@ -694,13 +621,6 @@ public sealed class LightManagerTests
LightSource light = registered[index];
if (!light.IsLit || light.Kind == LightKind.Directional)
continue;
if (visibleCells is not null
&& light.CellId != 0
&& !visibleCells.Contains(light.CellId))
{
continue;
}
ranked.Add(new OracleRank(
light,
Vector3.DistanceSquared(light.WorldPosition, player)));

View file

@ -21,7 +21,9 @@ namespace AcDream.Core.Tests.Physics;
public sealed class CellTransitFindTransitCellsBoxTests
{
private static CellPhysics MakeCellWithPortalAtRightWall(
Matrix4x4 worldTransform, uint otherCellId, ushort flags)
Matrix4x4 worldTransform,
uint otherCellId,
ushort flags)
{
// Portal poly at local x=2.5 (right wall), normal +X. Same shape as
// CellTransitFindTransitCellsSphereTests' fixture, so the sphere-only
@ -142,6 +144,47 @@ public sealed class CellTransitFindTransitCellsBoxTests
Assert.DoesNotContain(0xA9B40101u, endToEnd);
}
[Fact]
public void StaticPartArrayCrossesExteriorPortal_KeepsOutdoorCells()
{
// CPhysicsObj::calc_cross_cells_static sets do_not_load_cells before
// entering find_bbox_cell_list, but retail's bbox worklist returns
// the CELLARRAY directly. The post-pass prune at CObjCell::
// find_cell_list+0x18E belongs only to that separate sphere route.
// This is the cathedral-ramp shape: an indoor static box genuinely
// reaches an exterior portal and must remain registered in outdoor
// shadow cells even when the seed's stab list is empty.
var cache = new PhysicsDataCache();
const uint seedCell = 0xF4180112u;
cache.RegisterCellStructForTest(
seedCell,
MakeCellWithPortalAtRightWall(
Matrix4x4.Identity,
otherCellId: 0xFFFF,
flags: 0));
var partWorldPos = new Vector3(2.0f, 0f, 2.5f);
var sphere = new Sphere { Origin = partWorldPos, Radius = 0.7f };
var box = new[]
{
MakeBox(
new Vector3(-0.7f),
new Vector3(0.7f),
partWorldPos,
Quaternion.Identity),
};
IReadOnlyList<uint> cells = CellTransit.BuildShadowCellSetFromParts(
cache,
seedCell,
box,
new[] { sphere },
isStatic: true);
Assert.Contains(seedCell, cells);
Assert.Contains(cells, id => (id & 0xFFFFu) is >= 1u and <= 64u);
}
// ── D3.2: inverse guard — a box that DOES cross admits, unchanged ──────
/// <summary>

View file

@ -5,25 +5,26 @@ namespace AcDream.Launcher.Core.Tests.Installation;
public sealed class ContentMigrationCatalogTests
{
[Fact]
public void RecipeFiveToSixRequiresOneExplicitFullRebuild()
public void RecipeSixToSevenRequiresOneExplicitFullRebuild()
{
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(5, 6);
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(6, 7);
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
Assert.Equal(5u, plan.FromRecipeVersion);
Assert.Equal(6u, plan.TargetRecipeVersion);
Assert.Contains("pak v2", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Equal(6u, plan.FromRecipeVersion);
Assert.Equal(7u, plan.TargetRecipeVersion);
Assert.Contains("DrawingBSP", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Empty(plan.EffectiveDatIds);
Assert.Empty(plan.EffectiveLandblocks);
}
[Fact]
public void AnyOlderRecipeToSixCollapsesToOneFullRebuild()
public void AnyOlderRecipeToSevenCollapsesToOneFullRebuild()
{
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(1, 6);
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(1, 7);
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
Assert.Equal(6u, plan.TargetRecipeVersion);
Assert.Equal(7u, plan.TargetRecipeVersion);
Assert.Contains("pak v2", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Contains("DrawingBSP", plan.Reason, StringComparison.OrdinalIgnoreCase);
}
}