BR-1: RESOLVED as already-equivalent - premise falsified by pre-check, equivalence pinned, #113 attribution corrected
The plan's BR-1 ('implement the skipNoTexture draw-time surface gate')
died on its pre-check: acdream ALREADY suppresses every portal fill.
ReplicateProductionEmission_OnPortalFills replicates the exact emission
conditions of the production extractors on the hall/cottage fills:
pos=False neg=False for every one (Stippling.NoPos skips the positive
side at ObjectMeshManager.PrepareGfxObjMeshData:1046,
PrepareCellStructMeshData:1394, CellMesh.Build:44, GfxObjMesh.Build:71;
the fills have no negative surface). There is nothing to gate.
What ships instead: StipplingSurfaceEquivalenceTests - 2,607 polys across
13 building models + 13 environments, ZERO violations both directions:
NoPos <=> untextured-surface. Our build-time skip is proven equivalent to
retail's draw-time skipNoTexture rule (Ghidra 0x0059d4a4, default on
@0x00820e30) on this content. The pin fails loudly if future content
breaks the invariant - the cue to implement the draw-time gate then.
Corrections folded into the plan + comparison docs:
- The #113 phantom residual CANNOT be GfxObj fills (they never reach a
vertex buffer). Plausible true sites are cell-side: flood-admitted
cells drawn with the pass-all NoClipSlice when slot-less
(RetailPViewRenderer.cs:71), and/or cell statics drawn unclipped +
un-viewcone'd (object-lists-skip-portal-view-gate, confirmed).
BR-2 opens with the probe that pins which.
- The e46d3d9 user-gate observations (filter removed phantom/doors) were
confounded - the filter was a provable mesh no-op on shells AND doors.
- Ledger rows solid-surface-skip-missing + the acdream half of
portal-polys-baked-unconditional re-marked REFUTED-for-fills; the
retail mechanism descriptions and the un-consumed PortalIndex->
CBldPortal pairing (BR-4) stand.
Suites: Core 1398 green (1392 baseline + 6 new facts) + the 4 pre-existing
#99-era failures + 1 skip. No production code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
eb689ae73f
commit
695eca2c1f
4 changed files with 265 additions and 32 deletions
|
|
@ -57,28 +57,42 @@ gate) and the client stays playable after every phase. Conformance pins come
|
||||||
from the dat harness + the flood replay harnesses; retail constants are cited
|
from the dat harness + the flood replay harnesses; retail constants are cited
|
||||||
inline when ported.
|
inline when ported.
|
||||||
|
|
||||||
### BR-1 — The draw-time surface gate (kills the phantom class)
|
### BR-1 — The surface gate — ✅ RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1)
|
||||||
|
|
||||||
**What:** classify every mesh batch at decode by surface texturedness
|
**Premise falsified before implementation (the BR-1 pre-check,
|
||||||
(`Surface.Type & (Base1Image|Base1ClipMap)`); at draw, skip untextured
|
`ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
|
||||||
batches for **building-shell entities and cell meshes only** (plain objects
|
every portal fill** — all four extraction paths skip `Stippling.NoPos`
|
||||||
keep drawing them — retail's bypass). Align the cell-side build-time
|
positive sides (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
|
||||||
`NoPos`/`NoNeg` drop with this rule: run a dat-wide sweep (all CellStructs +
|
`PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, `GfxObjMesh.Build:71`),
|
||||||
all building models in the populated landblocks) proving
|
and the Holtburg fills have no negative surface. The planned "draw-time
|
||||||
`portal-fill ⇔ untextured`; keep the cheaper build-time drop only where the
|
surface gate" has nothing to gate.
|
||||||
sweep proves equivalence, otherwise move to the draw-time gate. GfxObj-side:
|
|
||||||
fills stay in the mesh but never draw (matches retail exactly).
|
|
||||||
|
|
||||||
- **Closes:** #113 phantom staircase class (hall ramp, cottage "flying
|
**What shipped instead — the equivalence pin**
|
||||||
stairs", every building's baked fills) — without touching doors (entities).
|
(`StipplingSurfaceEquivalenceTests`): 2,607 polys across 13 building models +
|
||||||
- **Acceptance:** `DumpPortalFillSurfaceTypes`-derived conformance sweep
|
13 environments, **zero violations both directions** — `NoPos ⇔ untextured
|
||||||
green; hall + hill-cottage phantom gone and doors/windows intact at
|
surface`. Our build-time skip is therefore *proven equivalent* to retail's
|
||||||
Holtburg (user gate); all suites green.
|
draw-time `skipNoTexture` rule on this content; the
|
||||||
- **Risk note:** apertures whose flood fails become true holes
|
`portal-poly-suppression-criterion` divergence closes as
|
||||||
(retail-identical); per-building floods + DrawPortal look-in already cover
|
equivalent-with-proof. The pin fails loudly if future content breaks the
|
||||||
the visible cases. If a hole shows at an unflooded aperture, that is BR-2/
|
invariant (the cue to implement the draw-time gate then).
|
||||||
BR-4 evidence, not a BR-1 regression.
|
|
||||||
- **Size:** ~2 commits (batch metadata plumb + draw gate; sweep test).
|
**Consequences (the honest part):**
|
||||||
|
- The **#113 phantom residual is NOT GfxObj fills** — it cannot be, they
|
||||||
|
never reach a vertex buffer. The "root cause #2" attribution from the
|
||||||
|
e46d3d9 session is corrected; the e46d3d9 user-gate observations (filter
|
||||||
|
removed phantom/doors) were confounded — the filter was a provable mesh
|
||||||
|
no-op on both shells and door parts.
|
||||||
|
- The phantom's plausible true sites are cell-side: flood-admitted stair
|
||||||
|
CELLS drawn with a pass-all slice when the assembler hands them no slot
|
||||||
|
(`RetailPViewRenderer.cs:71` draws ALL visible cells; `NoClipSlice`
|
||||||
|
default), and/or stair-cell STATICS drawn unclipped + un-viewcone'd by
|
||||||
|
design (`object-lists-skip-portal-view-gate`, confirmed). **BR-2's first
|
||||||
|
task is a 10-minute probe at the hall bisect spot pinning which** —
|
||||||
|
the closure moves to BR-2/BR-3 (shells) and BR-5 (statics).
|
||||||
|
- **Closes:** the `portal-poly-suppression-criterion` divergence (as
|
||||||
|
proven-equivalent); #113's closure moves to BR-2/BR-3/BR-5.
|
||||||
|
- **Shipped:** the pre-check + equivalence pin tests; no production code
|
||||||
|
(none needed).
|
||||||
|
|
||||||
### BR-2 — Aperture depth machinery (punch / seal / clear)
|
### BR-2 — Aperture depth machinery (punch / seal / clear)
|
||||||
|
|
||||||
|
|
@ -96,12 +110,20 @@ interior stage, gated on whether any seal was drawn (`portalsDrawnCount`);
|
||||||
mesh (retail `DrawBuilding` order) so the shell's depth closes everything
|
mesh (retail `DrawBuilding` order) so the shell's depth closes everything
|
||||||
outside the punch.
|
outside the punch.
|
||||||
|
|
||||||
|
- **First task (from BR-1's falsification):** the 10-minute probe at the
|
||||||
|
hall bisect spot — when the phantom is visible, log per stair cell
|
||||||
|
(0x100..0x106) whether it drew with a real clip slot or the pass-all
|
||||||
|
`NoClipSlice`, and whether its statics drew — pinning the phantom's true
|
||||||
|
draw site (shells → fixed here/BR-3; statics → BR-5).
|
||||||
- **Closes:** #108 (outdoor terrain sweeping across the upstairs door — the
|
- **Closes:** #108 (outdoor terrain sweeping across the upstairs door — the
|
||||||
missing true-depth seal is the confirmed `missing-portal-depth-fence`
|
missing true-depth seal is the confirmed `missing-portal-depth-fence`
|
||||||
divergence); the outdoor-root depth-discipline gap; part of #109.
|
divergence); the outdoor-root depth-discipline gap; part of #109; the
|
||||||
|
#113 phantom residual if the probe pins it on pass-all shell slices.
|
||||||
- **Acceptance:** cellar↔main-floor walk shows no grass sweep (user gate);
|
- **Acceptance:** cellar↔main-floor walk shows no grass sweep (user gate);
|
||||||
new harness fact: seal depth = portal plane depth inside the clipped
|
phantom-spot check at the hall (user gate, replaces the old BR-1
|
||||||
aperture polygon (GL readback test or probe assertion); suites green.
|
acceptance); new harness fact: seal depth = portal plane depth inside the
|
||||||
|
clipped aperture polygon (GL readback test or probe assertion); suites
|
||||||
|
green.
|
||||||
- **Size:** ~3 commits (~80 lines of GL + clipper reuse per the area
|
- **Size:** ~3 commits (~80 lines of GL + clipper reuse per the area
|
||||||
estimate, plus the clear re-shape and order swap).
|
estimate, plus the clear re-shape and order swap).
|
||||||
|
|
||||||
|
|
@ -285,11 +307,15 @@ issue, none blocks BR-1…BR-8.
|
||||||
## 5. Sequencing summary
|
## 5. Sequencing summary
|
||||||
|
|
||||||
```
|
```
|
||||||
BR-1 (surface gate) — first; standalone visual win, lowest risk
|
BR-1 (surface gate) — ✅ RESOLVED as already-equivalent (pin shipped,
|
||||||
BR-2 (depth punch/seal) — second; enables BR-3
|
no production code; #113 closure moved to
|
||||||
|
BR-2/3/5 — see BR-1 section)
|
||||||
|
BR-2 (depth punch/seal) — FIRST implementation phase; opens with the
|
||||||
|
phantom-site probe; enables BR-3
|
||||||
BR-3 (delete shell chop) — closes #114 with BR-2
|
BR-3 (delete shell chop) — closes #114 with BR-2
|
||||||
BR-4 (draw-driven floods) — closes #109; flood fidelity
|
BR-4 (draw-driven floods) — closes #109; flood fidelity
|
||||||
BR-5 (viewconeCheck) — particles/objects through the same gate
|
BR-5 (viewconeCheck) — particles/objects through the same gate;
|
||||||
|
closes the phantom if it is statics-side
|
||||||
BR-6 (one gate + deletions) — consolidation after the discipline is in
|
BR-6 (one gate + deletions) — consolidation after the discipline is in
|
||||||
BR-7 (collision A6.P4) — independent track; may interleave with BR-2..5
|
BR-7 (collision A6.P4) — independent track; may interleave with BR-2..5
|
||||||
BR-8 (camera/lighting/LOD) — feel tier; BR-8a may land early
|
BR-8 (camera/lighting/LOD) — feel tier; BR-8a may land early
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,10 @@ plus three mechanisms it doesn't use. The headline findings:
|
||||||
quad on all 13 Holtburg-area building models — door fills, window fills,
|
quad on all 13 Holtburg-area building models — door fills, window fills,
|
||||||
*and* the meeting-hall phantom stair-ramp — is `Base1Solid` (untextured).
|
*and* the meeting-hall phantom stair-ramp — is `Base1Solid` (untextured).
|
||||||
Retail draws none of them, ever. The doors players see are **door
|
Retail draws none of them, ever. The doors players see are **door
|
||||||
entities**; we draw the solid fills as colored geometry, which is why
|
entities**. *(Execution-day correction, §5: acdream's extractors already
|
||||||
removing them read as "doors disappeared."
|
skip all of these via `NoPos` — proven equivalent to retail's rule by
|
||||||
|
`StipplingSurfaceEquivalenceTests`. The phantom residual is cell-side,
|
||||||
|
not these fills; see the §5 banner.)*
|
||||||
|
|
||||||
3. **Retail never geometrically clips cell or shell geometry. Pixel exactness
|
3. **Retail never geometrically clips cell or shell geometry. Pixel exactness
|
||||||
at apertures is a DEPTH discipline.** Production cell draws are whole
|
at apertures is a DEPTH discipline.** Production cell draws are whole
|
||||||
|
|
@ -254,7 +256,7 @@ area files.
|
||||||
| Sev | Verdict | Divergence |
|
| Sev | Verdict | Divergence |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| CRIT | adjusted | `portal-poly-conditional-pass-missing` — no per-frame z-punch/z-seal/ConstructView pass on portal polys |
|
| CRIT | adjusted | `portal-poly-conditional-pass-missing` — no per-frame z-punch/z-seal/ConstructView pass on portal polys |
|
||||||
| HIGH | adjusted | `solid-surface-skip-missing` — untextured (solid) batches drawn on building/cell meshes retail skips |
|
| HIGH | **REFUTED for fills** (BR-1 pre-check, §5 banner) | `solid-surface-skip-missing` — acdream's NoPos build-time skip already covers them, proven equivalent |
|
||||||
| MED | confirmed | `degrade-lod-scoped-to-humanoids` — retail degrades every non-player part per frame |
|
| MED | confirmed | `degrade-lod-scoped-to-humanoids` — retail degrades every non-player part per frame |
|
||||||
| MED | adjusted | `no-per-view-entity-pass` — no per-portal-view re-cull of objects |
|
| MED | adjusted | `no-per-view-entity-pass` — no per-portal-view re-cull of objects |
|
||||||
| MED | confirmed | `stippling-semantics-divergence` — WB's NoPos/NoNeg side-drop vs retail batch flag + sides_type |
|
| MED | confirmed | `stippling-semantics-divergence` — WB's NoPos/NoNeg side-drop vs retail batch flag + sides_type |
|
||||||
|
|
@ -263,7 +265,7 @@ area files.
|
||||||
**Area 2 — Building shells** (`wf1-building-shells.md`)
|
**Area 2 — Building shells** (`wf1-building-shells.md`)
|
||||||
| Sev | Verdict | Divergence |
|
| Sev | Verdict | Divergence |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| CRIT | unverified | `portal-polys-baked-unconditional` — fills baked + drawn; PortalIndex→CBldPortal pairing never consumed |
|
| CRIT | **acdream half REFUTED** (BR-1 pre-check); retail half + missing PortalIndex→CBldPortal pairing stand | `portal-polys-baked-unconditional` — fills are NOT drawn (NoPos skip); the un-consumed pairing remains real (BR-4) |
|
||||||
| CRIT | unverified | `no-per-slot-building-draw` — building never draws per view slot; floods not shell-draw-driven |
|
| CRIT | unverified | `no-per-slot-building-draw` — building never draws per view slot; floods not shell-draw-driven |
|
||||||
| HIGH | adjusted | `flood-gate-shape` — 48 m seed + 0.01 ε + eye-inside rescue vs retail's no-distance chain (analogues otherwise faithful) |
|
| HIGH | adjusted | `flood-gate-shape` — 48 m seed + 0.01 ε + eye-inside rescue vs retail's no-distance chain (analogues otherwise faithful) |
|
||||||
| HIGH | unverified | `aperture-depth-machinery` — far-Z punch missing; particles scissor-only |
|
| HIGH | unverified | `aperture-depth-machinery` — far-Z punch missing; particles scissor-only |
|
||||||
|
|
@ -363,13 +365,35 @@ area files.
|
||||||
|
|
||||||
## 5. Mysteries resolved this session
|
## 5. Mysteries resolved this session
|
||||||
|
|
||||||
|
> **⚠ EXECUTION-DAY CORRECTION (2026-06-11, BR-1 pre-check).** The claim that
|
||||||
|
> acdream *draws* the solid portal fills is **FALSE** — all four extraction
|
||||||
|
> paths skip `Stippling.NoPos` positive sides
|
||||||
|
> (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
|
||||||
|
> `PrepareCellStructMeshData:1394`, `CellMesh.Build:44`,
|
||||||
|
> `GfxObjMesh.Build:71`), and the fills have no negative surface
|
||||||
|
> (`ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every
|
||||||
|
> fill). The equivalence pin (`StipplingSurfaceEquivalenceTests`, 2,607
|
||||||
|
> polys, 0 violations) proves our build-time skip ⇔ retail's draw-time
|
||||||
|
> `skipNoTexture` on this content. Consequences: the ledger rows
|
||||||
|
> `solid-surface-skip-missing` (Area 1) and the acdream half of
|
||||||
|
> `portal-polys-baked-unconditional` (Area 2) are **REFUTED for the fills**
|
||||||
|
> (the retail-side mechanism descriptions stand); the e46d3d9 user-gate
|
||||||
|
> observations were confounded (the filter was a provable mesh no-op on
|
||||||
|
> shells and doors); and the **#113 phantom residual is cell-side** —
|
||||||
|
> flood-admitted cells drawn with the pass-all `NoClipSlice` when slot-less
|
||||||
|
> (`RetailPViewRenderer.cs:71`) and/or unclipped un-viewcone'd cell statics
|
||||||
|
> (`object-lists-skip-portal-view-gate`, confirmed). BR-2 opens with the
|
||||||
|
> probe that pins which. The mapping agents missed the `:1046` skip — score
|
||||||
|
> one for "verify what call sites actually pass."
|
||||||
|
|
||||||
1. **Door-vanish (charter §4.1)** — SOLVED, dat-proven (`e223325` +
|
1. **Door-vanish (charter §4.1)** — SOLVED, dat-proven (`e223325` +
|
||||||
`DumpPortalFillSurfaceTypes`): the e46d3d9 filter walked only
|
`DumpPortalFillSurfaceTypes`): the e46d3d9 filter walked only
|
||||||
`node.Polygons`, never `node.Portals` (`PortalRef`); every dropped poly was
|
`node.Polygons`, never `node.Portals` (`PortalRef`); every dropped poly was
|
||||||
a portal fill; all fills are `Base1Solid`; retail skips them via
|
a portal fill; all fills are `Base1Solid`; retail skips them via
|
||||||
`skipNoTexture`; visible doors are entities. **No static filter can be
|
`skipNoTexture`; visible doors are entities. **No static filter can be
|
||||||
correct** — but the correct rule is a *surface-type draw gate*, which is
|
correct** — and per the execution-day correction above, no filter was
|
||||||
nearly as simple.
|
*needed*: the fills were never drawn, and the gate's "doors vanished"
|
||||||
|
observation was confounded.
|
||||||
2. **#114's real shape (charter §4.2)** — retail does not crop indoor
|
2. **#114's real shape (charter §4.2)** — retail does not crop indoor
|
||||||
geometry; it punches/seals depth at apertures and draws far→near. The
|
geometry; it punches/seals depth at apertures and draws far→near. The
|
||||||
"admission-quality vs draw-quality regions" framing dissolves: regions
|
"admission-quality vs draw-quality regions" framing dissolves: regions
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,52 @@ public sealed class Issue113DoorVanishDiagnosticTests
|
||||||
return n.LengthSquared() < 1e-10f ? Vector3.Zero : Vector3.Normalize(n);
|
return n.LengthSquared() < 1e-10f ? Vector3.Zero : Vector3.Normalize(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BR-1 pre-check: replicate ObjectMeshManager.PrepareGfxObjMeshData's
|
||||||
|
/// EXACT emission conditions (lines 1040-1058: pos side emitted unless
|
||||||
|
/// Stippling.NoPos; neg side if Negative/Both or (!NoNeg and SidesType ==
|
||||||
|
/// Clockwise)) on the portal-fill polys, printing raw enum values. The
|
||||||
|
/// #113 evidence says these fills RENDER (the phantom) — if the replica
|
||||||
|
/// says "skipped", the fills reach the screen another way and BR-1's gate
|
||||||
|
/// must move.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ReplicateProductionEmission_OnPortalFills()
|
||||||
|
{
|
||||||
|
var datDir = ConformanceDats.ResolveDatDir();
|
||||||
|
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
||||||
|
foreach (uint mid in new uint[] { 0x010014C3u, 0x01000827u })
|
||||||
|
{
|
||||||
|
var gfx = dats.Get<DatReaderWriter.DBObjs.GfxObj>(mid)!;
|
||||||
|
var walked = new HashSet<ushort>();
|
||||||
|
void Walk(DatReaderWriter.Types.DrawingBSPNode? n)
|
||||||
|
{
|
||||||
|
if (n is null) return;
|
||||||
|
if (n.Polygons is not null) foreach (var pid in n.Polygons) walked.Add((ushort)pid);
|
||||||
|
Walk(n.PosNode); Walk(n.NegNode);
|
||||||
|
}
|
||||||
|
Walk(gfx.DrawingBSP!.Root);
|
||||||
|
var fills = gfx.Polygons.Keys.Where(k => !walked.Contains(k)).OrderBy(k => k).ToList();
|
||||||
|
|
||||||
|
_out.WriteLine($"=== model 0x{mid:X8} ===");
|
||||||
|
foreach (var pid in fills)
|
||||||
|
{
|
||||||
|
var poly = gfx.Polygons[pid];
|
||||||
|
bool noPos = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos);
|
||||||
|
bool hasNeg = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.Negative)
|
||||||
|
|| poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.Both)
|
||||||
|
|| (!poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoNeg)
|
||||||
|
&& poly.SidesType == DatReaderWriter.Enums.CullMode.Clockwise);
|
||||||
|
_out.WriteLine(
|
||||||
|
$" poly {pid,3}: stip={poly.Stippling}(raw={(int)poly.Stippling}) sides={poly.SidesType}(raw={(int)poly.SidesType}) " +
|
||||||
|
$"posSurf={poly.PosSurface} negSurf={poly.NegSurface} " +
|
||||||
|
$"-> production emits: pos={!noPos} neg={hasNeg && poly.NegSurface >= 0}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A confirmation: retail's building/cell mesh pass skips surface
|
/// Phase A confirmation: retail's building/cell mesh pass skips surface
|
||||||
/// batches whose CSurface type has neither Base1Image (0x2) nor
|
/// batches whose CSurface type has neither Base1Image (0x2) nor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Conformance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BR-1 conformance pin (holistic building-render port, plan §BR-1).
|
||||||
|
///
|
||||||
|
/// Retail suppresses portal-fill drawing at DRAW time via the skipNoTexture
|
||||||
|
/// rule: building/cell surface batches whose CSurface.type lacks BASE1_IMAGE
|
||||||
|
/// (0x2) and BASE1_CLIPMAP (0x4) are skipped (D3DPolyRender inner draw,
|
||||||
|
/// Ghidra 0x0059d4a0; default on @0x00820e30). acdream suppresses them at
|
||||||
|
/// BUILD time via Stippling.NoPos in all four extraction paths
|
||||||
|
/// (ObjectMeshManager.PrepareGfxObjMeshData:1046 + PrepareCellStructMeshData
|
||||||
|
/// :1394, CellMesh.Build:44, GfxObjMesh.Build:71).
|
||||||
|
///
|
||||||
|
/// These criteria are equivalent ONLY if NoPos ⇔ untextured-surface holds on
|
||||||
|
/// the content. This sweep pins both directions across the populated
|
||||||
|
/// Holtburg-area landblocks (building shell models + every Environment
|
||||||
|
/// CellStruct their cells reference + the door setup parts):
|
||||||
|
/// (a) every NoPos poly's positive surface is untextured (else our skip
|
||||||
|
/// drops something retail draws), and
|
||||||
|
/// (b) every untextured-surface poly is NoPos (else we draw something
|
||||||
|
/// retail skips on building/cell passes — the would-be phantom class).
|
||||||
|
/// Violations of (b) on PLAIN OBJECT GfxObjs are allowed — retail's bypass
|
||||||
|
/// draws solid batches for non-building/non-cell meshes, and so do we.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StipplingSurfaceEquivalenceTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _out;
|
||||||
|
public StipplingSurfaceEquivalenceTests(ITestOutputHelper output) => _out = output;
|
||||||
|
|
||||||
|
private static readonly uint[] Landblocks =
|
||||||
|
{
|
||||||
|
0xA9B40000u, // Holtburg town
|
||||||
|
0xA9B30000u, // hill cottage block
|
||||||
|
0xAAB30000u, // meeting hall block
|
||||||
|
0xA9B50000u,
|
||||||
|
0xAAB40000u,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoPosStippling_Equals_UntexturedSurface_OnBuildingsAndCells()
|
||||||
|
{
|
||||||
|
var datDir = ConformanceDats.ResolveDatDir();
|
||||||
|
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
||||||
|
int polysChecked = 0;
|
||||||
|
var aViolations = new List<string>(); // NoPos but TEXTURED (our skip would drop a retail-drawn poly)
|
||||||
|
var bViolations = new List<string>(); // untextured but NOT NoPos (we'd draw what retail skips)
|
||||||
|
|
||||||
|
bool IsTextured(DatReaderWriter.DBObjs.Surface? s) =>
|
||||||
|
s is not null &&
|
||||||
|
(s.Type.HasFlag(DatReaderWriter.Enums.SurfaceType.Base1Image)
|
||||||
|
|| s.Type.HasFlag(DatReaderWriter.Enums.SurfaceType.Base1ClipMap));
|
||||||
|
|
||||||
|
// ---- building shell GfxObjs ----
|
||||||
|
var buildingModels = new SortedSet<uint>();
|
||||||
|
var environments = new SortedSet<uint>();
|
||||||
|
foreach (uint lb in Landblocks)
|
||||||
|
{
|
||||||
|
var lbi = dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(lb | 0xFFFEu);
|
||||||
|
if (lbi is null) continue;
|
||||||
|
foreach (var b in lbi.Buildings ?? new()) buildingModels.Add(b.ModelId);
|
||||||
|
for (uint low = 0x0100; low < 0x0100 + lbi.NumCells; low++)
|
||||||
|
{
|
||||||
|
var dc = dats.Get<DatReaderWriter.DBObjs.EnvCell>(lb | low);
|
||||||
|
if (dc is not null) environments.Add(0x0D000000u | dc.EnvironmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.NotEmpty(buildingModels);
|
||||||
|
Assert.NotEmpty(environments);
|
||||||
|
|
||||||
|
foreach (var mid in buildingModels)
|
||||||
|
{
|
||||||
|
var gfx = dats.Get<DatReaderWriter.DBObjs.GfxObj>(mid);
|
||||||
|
if (gfx is null) continue;
|
||||||
|
foreach (var kv in gfx.Polygons)
|
||||||
|
{
|
||||||
|
var poly = kv.Value;
|
||||||
|
polysChecked++;
|
||||||
|
bool noPos = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos);
|
||||||
|
DatReaderWriter.DBObjs.Surface? surf = null;
|
||||||
|
if (poly.PosSurface >= 0 && poly.PosSurface < gfx.Surfaces.Count)
|
||||||
|
surf = dats.Get<DatReaderWriter.DBObjs.Surface>(gfx.Surfaces[poly.PosSurface]);
|
||||||
|
if (noPos && IsTextured(surf))
|
||||||
|
aViolations.Add($"gfx 0x{mid:X8} poly {kv.Key}: NoPos but textured surface");
|
||||||
|
if (!noPos && surf is not null && !IsTextured(surf))
|
||||||
|
bViolations.Add($"gfx 0x{mid:X8} poly {kv.Key}: textured-less surface without NoPos (type={surf.Type})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- cell structs referenced by those landblocks' interior cells ----
|
||||||
|
foreach (var envId in environments)
|
||||||
|
{
|
||||||
|
var env = dats.Get<DatReaderWriter.DBObjs.Environment>(envId);
|
||||||
|
if (env is null) continue;
|
||||||
|
foreach (var (csId, cs) in env.Cells)
|
||||||
|
{
|
||||||
|
foreach (var kv in cs.Polygons)
|
||||||
|
{
|
||||||
|
var poly = kv.Value;
|
||||||
|
polysChecked++;
|
||||||
|
bool noPos = poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos);
|
||||||
|
// CellStruct polys resolve surfaces through the EnvCell's
|
||||||
|
// surface list at runtime; the struct itself stores only the
|
||||||
|
// index. Direction (a) can't be evaluated without a specific
|
||||||
|
// EnvCell, so for structs we pin only the NoPos→portal-poly
|
||||||
|
// correspondence: every NoPos poly must be a portal polygon
|
||||||
|
// (referenced by the struct's Portals list), i.e. our skip
|
||||||
|
// removes only aperture fills, never wall geometry.
|
||||||
|
if (noPos)
|
||||||
|
{
|
||||||
|
bool isPortalPoly = cs.Portals.Any(p => p == kv.Key);
|
||||||
|
if (!isPortalPoly)
|
||||||
|
aViolations.Add($"env 0x{envId:X8} struct {csId} poly {kv.Key}: NoPos but not a portal poly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_out.WriteLine($"checked {polysChecked} polys across {buildingModels.Count} building models + {environments.Count} environments");
|
||||||
|
_out.WriteLine($"(a) NoPos-but-textured (skip would drop retail-drawn): {aViolations.Count}");
|
||||||
|
foreach (var v in aViolations.Take(20)) _out.WriteLine($" {v}");
|
||||||
|
_out.WriteLine($"(b) untextured-but-not-NoPos on buildings (we'd draw what retail skips): {bViolations.Count}");
|
||||||
|
foreach (var v in bViolations.Take(20)) _out.WriteLine($" {v}");
|
||||||
|
|
||||||
|
Assert.Empty(aViolations);
|
||||||
|
Assert.Empty(bViolations);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue