Standing inside (or looking into) a windowed building like the Agent of Arcanum, interior objects (furniture, NPCs, the player) were lit by the directional sun because acdream's sun gate was per-FRAME (keyed on the player being in a sealed cell), not per-DRAW as retail does it. Retail's PView::DrawCells (0x005a4840) runs two stages per frame: outdoor stage → useSunlightSet(1) (0x005a485a): sun ON interior stage → useSunlightSet(0) (0x005a49f3): sun OFF DrawMeshInternal (0x0059f398) then calls minimize_object_lighting only when useSunlight==0, so indoor objects ALWAYS skip the sun regardless of whether the player's cell is windowed or sealed. Fix: add a per-instance uint SSBO (binding=6 instanceIndoor[]) whose value is IndoorObjectReceivesTorches(ParentCellId) — the same predicate AP-43 already uses for the torch gate. In mesh_modern.vert, nest the sun loop inside an additional `if (instanceIndoor[instanceIndex] == 0u)` check inside the existing `if (uLightingMode == 0)` block. Indoor objects get torches (unchanged) but now skip the sun; outdoor objects keep the sun and still get no torches. The ambient regime (UpdateSunFromSky: 0.2 sealed / sky otherwise) is untouched — it was already correct. Mechanically: _currentEntityIndoor set once per entity in ComputeEntityLightSet; appended to InstanceGroup.IndoorFlags in AppendCurrentLightSet; grown/packed/uploaded in the same cursor loop as _clipSlotData and _lightSetData; deleted in Dispose. Mode-1 draws (EnvCellRenderer) never read binding=6 — the sun loop is inside the uLightingMode==0 uniform-control-flow branch. AP-43 divergence register updated: the sun half is now per-draw (no longer a residual). Residual narrowed to the unaudited ebp_2 test in CellManager::ChangePosition (no observed impact). Tests: WbDrawDispatcherIndoorFlagTests pins IndoorObjectReceivesTorches for the spec §5 representative ids: 0xA9B40172 (Agent of Arcanum EnvCell) → 1; 0xA9B40031 (land sub-cell) → 0; 0xA9B4FFFF (landblock) → 0; null (outdoor shell) → 0; plus the boundary cases 0x0100/0x00FF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
3.6 KiB
C#
82 lines
3.6 KiB
C#
using AcDream.App.Rendering.Wb;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Wb;
|
|
|
|
/// <summary>
|
|
/// #142 — pins the per-instance "indoor" flag predicate used by both the torch
|
|
/// gate (AP-43) and the new sun gate (binding=6 <c>instanceIndoor[]</c>).
|
|
/// The SSBO flag written by <see cref="WbDrawDispatcher.AppendCurrentLightSet"/>
|
|
/// equals <c>IndoorObjectReceivesTorches(entity.ParentCellId) ? 1u : 0u</c>.
|
|
/// This test pins that the shared predicate returns the correct value for the
|
|
/// representative cell ids used in the spec acceptance gate (§5).
|
|
///
|
|
/// <para>
|
|
/// Retail decomp anchor: <c>useSunlightSet</c> 0x0054d450 (sun only, never
|
|
/// ambient); <c>PView::DrawCells</c> 0x005a4840 — outdoor stage runs
|
|
/// <c>useSunlightSet(1)</c> (0x005a485a), interior stage runs
|
|
/// <c>useSunlightSet(0)</c> (0x005a49f3). Indoor objects skip the sun, not
|
|
/// because the player is inside, but because the OBJECT is in an interior cell.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class WbDrawDispatcherIndoorFlagTests
|
|
{
|
|
// ── Spec §5 representative ids ─────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void EnvCell_AgenOfArcanum_IndoorFlag1()
|
|
{
|
|
// 0xA9B40172: low word 0x0172 >= 0x0100 → indoor (EnvCell)
|
|
// This is the exact cell from the in-game probe (agent-arcanum-probe.log
|
|
// 2026-06-20) that triggered #142 — the Agent of Arcanum interior.
|
|
uint envCell = 0xA9B40172u;
|
|
Assert.True(WbDrawDispatcher.IndoorObjectReceivesTorches(envCell),
|
|
"EnvCell 0xA9B40172 must be indoor (flag=1): objects here skip the sun.");
|
|
}
|
|
|
|
[Fact]
|
|
public void LandSubCell_OutdoorFlag0()
|
|
{
|
|
// 0xA9B40031: low word 0x0031 < 0x0100 → outdoor land sub-cell
|
|
uint landSubCell = 0xA9B40031u;
|
|
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(landSubCell),
|
|
"Land sub-cell 0xA9B40031 must be outdoor (flag=0): objects here get the sun.");
|
|
}
|
|
|
|
[Fact]
|
|
public void LandblockId_OutdoorFlag0()
|
|
{
|
|
// 0xA9B4FFFF: low word 0xFFFF — a landblock pseudo-id, not an indoor cell
|
|
uint landblockId = 0xA9B4FFFFu;
|
|
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(landblockId),
|
|
"Landblock id 0xA9B4FFFF must be outdoor (flag=0): the 0xFFFF low-word is excluded.");
|
|
}
|
|
|
|
[Fact]
|
|
public void NullParentCellId_OutdoorFlag0()
|
|
{
|
|
// Outdoor scenery / building exterior shells have no ParentCellId → outdoor
|
|
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(null),
|
|
"Null ParentCellId must be outdoor (flag=0): building shells get the sun, not torches.");
|
|
}
|
|
|
|
// ── Boundary: first valid EnvCell low word ──────────────────────────────
|
|
|
|
[Fact]
|
|
public void CellId_LowWord0x0100_IsIndoor()
|
|
{
|
|
// Low word exactly 0x0100 is the first indoor EnvCell id — just on the boundary
|
|
uint firstEnvCell = 0xA9B40100u;
|
|
Assert.True(WbDrawDispatcher.IndoorObjectReceivesTorches(firstEnvCell),
|
|
"Low word 0x0100 is the lowest valid EnvCell id (inclusive boundary → indoor).");
|
|
}
|
|
|
|
[Fact]
|
|
public void CellId_LowWord0x00FF_IsOutdoor()
|
|
{
|
|
// Low word 0x00FF is one below the EnvCell range — still outdoor
|
|
uint lastLandSubCell = 0xA9B400FFu;
|
|
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(lastLandSubCell),
|
|
"Low word 0x00FF is just below the EnvCell range (exclusive boundary → outdoor).");
|
|
}
|
|
}
|