acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkVisibilityMathTests.cs
Erik d1e3e64f61 feat(render): S4 chunk 1 — retail far-punch bits, the ±12 local-input reject, the depth truth table and cross-frame latch pins
S4-c1 per docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md §6.
S3 chunk 2 already landed the persistent portalsDrawnCount latch, the
gated clear, the exit-seal counting, and the look-in isolation — this
chunk covers only what §1/§2 of the packet name as still owed: C0-C3.

C0 — far-punch depth constant (R1: DrawPortalPolyInternal @0x0059bc90's
tail). portal_depth.vert's punch branch carried the decimal 0.99999988,
which reinterprets as bits 0x3F7FFFFE — fifteen ULPs FARTHER from the
camera than retail's real constant, bits 0x3F7FFFEF. Now writes
`uintBitsToFloat(0x3F7FFFEFu)` so the exact bits survive the GLSL/SPIR-V
compiler instead of trusting a decimal literal to round-trip unchanged.
Recompiled via tools/compile-shaders.ps1 (glslc 1.4.350.0 backend
recorded, managed shaderc path used); portal_depth.vert.spv's SHA-256
re-pinned in VulkanShaderManifestTests
(51c60d0924d62c61548efcf5f9e7672a121b1b68ca0a06755e32f1a4d73a8acf,
was 4ac1c452e7ac0d08a32f67fb03f21229af2d1605baa81f407240a3626251dfd7).

T1 (new Fact PortalDepthVert_FarPunchConstant_MatchesRetailExactBits in
VulkanShaderManifestTests.cs): a SOURCE pin — reads portal_depth.vert's
punch line and reinterprets whatever literal it carries (uintBitsToFloat
hex or a plain decimal) as raw bits, asserts == 0x3F7FFFEF. Verified
against the PRE-CHANGE source by hand-reverting the line to
`clipPos.z = clipPos.w * 0.99999988;` and re-running just this test:

    Assert.Equal() Failure: Values differ
    Expected: 1065353199
    Actual:   1065353214

(1065353199 = 0x3F7FFFEF, 1065353214 = 0x3F7FFFFE). Line restored and
the test re-confirmed green afterward. MUTATION: any other literal fails
the same way.

C1 — the ±12 local-input reject (R2: 0x59BCD6-0x59BD28 then
0x59BD40-0x59BD66). The Ghidra arbitration table in
oh1-depth-lifecycle.md governs over the pseudo-C's own nested-if reading
of the four x87 FCOM results (BinaryNinja's `test ah, 0x44` condition
synthesis is FPU-flag-ambiguous and reads backward at face value — see
feedback_bn_decomp_field_names.md on decompiler flag mush as an artifact
class, not semantics): the table's row says "whole poly on any
local-input x/y == +/-12 boundary is rejected before count/clip" — taken
as written, not re-derived from the pseudo-C's literal branch nesting.

Ported as one shared predicate,
WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(ReadOnlySpan
<Vector3>): true iff any vertex's X or Y is exactly +12f/-12f (retail
tests LOCAL x/y before xformStart, the world transform). Wired at BOTH
producers that own the LOCAL polygon before it leaves cell/building
space:
  - WalkFrameDriver.OnPunchGeometry (the walk's punch-event producer,
    IWalkEventSink.OnPunchGeometry) — checked on the building-local
    WalkPolygon.Vertices before TransformToWorld; a hit returns before
    MarkIfGrown/any event append (retail's reject -> transform -> clip
    -> count order).
  - RetailPViewPassExecutor.DrawPortalDepthWrite (the exit-seal
    enumeration behind DrawExitPortalMask, the sole caller) — checked on
    cell.PortalPolygons[index]'s local vertices before the
    Vector3.Transform loop; a hit `continue`s with no `submitted++`.

T2 (three layers):
  1. WalkVisibilityMathTests.cs — direct unit tests of the predicate:
     Boundary_guard_rejects_a_polygon_with_one_vertex_exactly_on_plus_minus_12
     (Theory, x/y == +-12 each), Boundary_guard_admits_a_polygon_whose_
     nearest_vertex_is_just_inside_12 (Theory, x/y == +-11.999),
     Boundary_guard_rejects_the_whole_polygon_even_when_only_one_of_
     several_vertices_hits_it, Boundary_guard_ignores_the_vertical_z_
     component, Boundary_guard_admits_the_empty_polygon.
  2. WalkFrameDriverTests.OnPunchGeometry_RejectsWholePolygonOnExact
     PlusMinus12LocalVertex_ButPunchesJustInside — functional: feeds
     OnPunchGeometry a polygon with a vertex at x=12 (no PunchFan/no
     "PUNCH:" log line) then one at x=11.999 (punches normally,
     leaf.Punches has exactly one entry, log has exactly one "PUNCH:3@v0").
  3. RetailPViewPassExecutorTests.DrawPortalDepthWrite_RejectsDegenerate
     LocalPolygons_BeforeTransformOrSubmission — a real functional test of
     DrawPortalDepthWrite needs a live PortalDepthMaskRenderer the suite
     has no fake for, so this is a compiled-call-graph pin (this file's
     established pattern for exactly this situation): the guard call
     precedes both the Vector3.Transform loop and
     PortalDepthMaskRenderer.DrawDepthFan by IL offset, gated by a
     conditional branch immediately after it.

MUTATION texts, all verified live during this session then reverted:
  - OnPunchGeometry_RejectsWholePolygon... with the C1 guard deleted from
    OnPunchGeometry:
      Assert.Single() Failure: The collection contained 2 items
      Collection: [WalkPolygon { Plane = WalkPlane { Normal = <0, 0, 1>, D = -3 }, Vertices = [<0, 0, 3>, <12, 0, 3>, <5, 5, 3>] }, WalkPolygon { Plane = WalkPlane { Normal = <0, 0, 1>, D = -3 }, Vertices = [<0, 0, 3>, <11.999, 0, 3>, <5, 5, 3>] }]
  - DrawPortalDepthWrite_RejectsDegenerateLocalPolygons... with the C1
    guard deleted from DrawPortalDepthWrite:
      Expected call to WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard.
  - Boundary_guard_admits_a_polygon_whose_nearest_vertex_is_just_inside_12
    with the predicate widened to `MathF.Abs(x) >= 11.99f ||
    MathF.Abs(y) >= 11.99f` (all four rows):
      Assert.False() Failure
      Expected: False
      Actual:   True
  - Boundary_guard_rejects_a_polygon_with_one_vertex_exactly_on_plus_
    minus_12 with the predicate narrowed to strict `x > 12f || x < -12f
    || y > 12f || y < -12f` (all four rows):
      Assert.True() Failure
      Expected: True
      Actual:   False
  - Boundary_guard_rejects_the_whole_polygon_even_when_only_one_of_
    several_vertices_hits_it with the guard checking only
    localVertices[0] instead of looping every vertex:
      Assert.True() Failure
      Expected: True
      Actual:   False

C2 — no pipeline change for R3 (depth ALWAYS/write/no-cull, color writes
ENABLED with a zero-alpha SRCALPHA/INVSRCALPHA blend). acdream's
PortalDepthMaskRenderer.Rhi.cs:92,100 sets ColorWrite=false alongside
Blend=None; portal_depth.frag writes no color output at all. Provably
pixel-identical (retail's blend collapses to dst'=dst when srcAlpha is
fixed at 0, for any RGB) and the write mask is the SAFER mechanism going
forward (structurally blocks any future accidental color write,
independent of an authored zero-alpha invariant). Added register row
AD-119 to docs/architecture/retail-divergence-register.md (the next free
id after AD-118), citing DrawPortalPolyInternal @0x0059bc90 and
PortalDepthMaskRenderer.Rhi.cs; section 2's active-row count and running
header note updated (90 -> 91).

C3 — the truth table + cross-frame latch tests. The (root kind,
draw_landscape, outside-view count, previous count) table's cells are
mostly already covered by S3 chunk 2's own tests — this chunk adds only
the genuinely missing rows/cases, and leaves every existing test
untouched:

  Pre-existing coverage (named, not reproduced):
    - interior, ov==0, prior==0 ->
      RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeAndNeverFlushesClearsOrSeals
    - interior, ov>0, prior==0 ->
      RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
      and OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears
      (its own frame 1)
    - interior, ov>0, prior>0 (T4's "frame 1 seals N>0 -> frame 2
      clears" half) ->
      OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears
      (its own frame 2)
    - T4's "frame 1 seals 0 -> frame 2 does not clear" half (repeated
      across three consecutive ov>0 frames, subsuming the two-frame
      case) -> OnInteriorFloodDrawTurn_FloodWithNoExitPortal_NeverClearsAcrossFrames
    - one look-in isolated from the root latch ->
      LookInDrawCells_NeitherArmsNorConsumesThePortalsDrawnCounter
  No further T4 test was added — the two existing facts above already
  prove both halves of the two-consecutive-frames latch case exactly.

  New rows added this chunk:
    - WalkFrame_OutdoorRoot_NeverFiresTheInteriorClearSealMachinery: root
      kind == OUTDOOR. RetailFrameWalk.WalkFrame's outdoor branch
      ((cameraCellId & 0xFFFF) < 0x100) calls DrawLandscape directly and
      never calls DrawInside/OnInteriorFloodDrawTurn at all, so the whole
      LFLUSH/stamp/CLEAR/SEALS mechanism structurally cannot fire —
      driven end-to-end through RunFrame with an outdoor cameraCellId,
      asserting SKY present, LFLUSH/CLEAR/SEALS absent, counter stays 0.
      MUTATION (verified, then reverted): added a stray
      `sink.OnInteriorFloodDrawTurn([], 1);` call to WalkFrame's outdoor
      branch:
        Assert.DoesNotContain() Failure: Item found in collection
                         ↓ (pos 1)
        Collection: ["SKY", "LFLUSH", "SEALS"]
        Found:      "LFLUSH"
    - OnInteriorFloodDrawTurn_OvZeroAfterAPriorArmedCounter_LeavesTheLatch
      CompletelyUntouched: interior, ov==0 immediately after an EARLIER
      ov>0 frame armed the counter — proves the counter is left EXACTLY
      as an earlier frame left it (not merely "not cleared this frame"),
      since S3 §8.1 R3 gates the ENTIRE outside_view.view_count>0 block,
      including the read-then-zero decision itself, on ov>0.
      MUTATION (verified, then reverted): moved
      `int armed = PortalsDrawnCount; PortalsDrawnCount = 0;` out of the
      `if (outsideViewCount > 0)` gate in
      WalkFrameDriver.OnInteriorFloodDrawTurn (unconditional
      read-then-zero every call):
        Assert.Equal() Failure: Values differ
        Expected: 1
        Actual:   0
      (every OTHER WalkFrameDriverTests fact stayed green under this same
      mutation — this new test is the only one that catches it).
    - MultipleLookIns_WithinOneFrameAndAcrossFrames_NeverTouchTheRootLatch
      (T5): extends the single-look-in fact to TWO look-ins in one frame
      then a THIRD in a later frame. MUTATION (verified, then reverted):
      a `_mutationLookInCalls` counter in HandleDrawCellsTurn's
      LookInStatic branch that resets PortalsDrawnCount on the SECOND
      look-in call:
        Assert.Equal() Failure: Values differ
        Expected: 1
        Actual:   0
      — while LookInDrawCells_NeitherArmsNorConsumesThePortalsDrawnCounter
      (one look-in only) stayed green under the identical mutation,
      confirming this test's incremental value over the existing single-
      look-in fact.

Gates: dotnet build (App.Tests and App) 0 warnings/0 errors; hermetic
lane 6832/6832 passed; InstalledDat lane against
C:/Users/erikn/Documents/Asheron's Call — exactly the four known
failures (TowerAscentReplayTests.TowerAscent_StaircaseStaysConeVisible_
EveryStep, LayoutImporterMediaBearingChildSweepTests.
MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets and
LayoutImporterInvisibleSweepTests.EveryAuthoredInvisibleWidget_
StartsHiddenAcrossAllLayouts — both #383 — and
WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff #458),
243 passed / 1 skipped / 4 failed / 248 total, no new failures; shader
tests (VulkanShaderDescriptorContractTests/VulkanShaderManifestTests/
RenderPackSpirvValidatorTests/SkyVertexLayoutTests) 35/35; register
tests (Divergence|Register filter) 52/52.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 23:38:52 +02:00

298 lines
12 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
public sealed class WalkVisibilityMathTests
{
// ---- get_pt_limit @0x0054b840 ----
[Fact]
public void Up_normal_plane_encodes_inside_above_as_negative_height()
{
// Plane z >= 5 at the origin column: N=(0,0,1), d=-5 → h=5, inside above.
var plane = new WalkPlane(new Vector3(0, 0, 1), -5f);
Assert.Equal(-5f, WalkVisibilityMath.GetPointLimit(0, 0, plane));
}
[Fact]
public void Up_normal_plane_at_or_above_sky_height_is_outside()
{
var plane = new WalkPlane(new Vector3(0, 0, 1), -1000f);
Assert.Equal(
WalkVisibilityMath.OutsideColumn,
WalkVisibilityMath.GetPointLimit(0, 0, plane));
}
[Fact]
public void Up_normal_plane_with_nonpositive_height_is_wholly_inside()
{
var plane = new WalkPlane(new Vector3(0, 0, 1), 3f); // z >= -3
Assert.Equal(
WalkVisibilityMath.InsideColumn,
WalkVisibilityMath.GetPointLimit(0, 0, plane));
}
[Fact]
public void Down_normal_plane_encodes_inside_below_as_positive_height()
{
// Plane z <= 7: N=(0,0,-1), d=7 → h=7, inside below.
var plane = new WalkPlane(new Vector3(0, 0, -1), 7f);
Assert.Equal(7f, WalkVisibilityMath.GetPointLimit(0, 0, plane));
}
[Fact]
public void Down_normal_plane_with_nonpositive_height_is_outside()
{
var plane = new WalkPlane(new Vector3(0, 0, -1), -2f); // z <= -2: nothing above ground
Assert.Equal(
WalkVisibilityMath.OutsideColumn,
WalkVisibilityMath.GetPointLimit(0, 0, plane));
}
[Fact]
public void Vertical_plane_uses_the_side_of_the_ground_point()
{
var plane = new WalkPlane(new Vector3(1, 0, 0), -10f); // x >= 10
Assert.Equal(
WalkVisibilityMath.OutsideColumn,
WalkVisibilityMath.GetPointLimit(5f, 0, plane));
Assert.Equal(
WalkVisibilityMath.InsideColumn,
WalkVisibilityMath.GetPointLimit(15f, 0, plane));
// ON the plane (within epsilon) counts inside.
Assert.Equal(
WalkVisibilityMath.InsideColumn,
WalkVisibilityMath.GetPointLimit(10f, 0, plane));
}
// ---- corner_plane_check @0x0054b930 ----
[Theory]
[InlineData(1001f, 0f, 10f, WalkBoundingType.Outside)] // sentinel outside
[InlineData(0f, 0f, 10f, WalkBoundingType.EntirelyInside)] // sentinel inside
[InlineData(-5f, 6f, 10f, WalkBoundingType.EntirelyInside)] // inside above 5; slab [6,10] wholly above
[InlineData(-5f, 2f, 10f, WalkBoundingType.PartiallyInside)] // slab straddles 5
[InlineData(-5f, 2f, 4f, WalkBoundingType.Outside)] // slab wholly below 5
[InlineData(7f, 2f, 6f, WalkBoundingType.EntirelyInside)] // inside below 7; slab wholly below
[InlineData(7f, 2f, 10f, WalkBoundingType.PartiallyInside)] // slab straddles 7
[InlineData(7f, 8f, 10f, WalkBoundingType.Outside)] // slab wholly above 7
public void Corner_check_classifies_the_slab(
float bound, float minZ, float maxZ, WalkBoundingType expected)
=> Assert.Equal(expected, WalkVisibilityMath.CornerPlaneCheck(bound, minZ, maxZ));
[Fact]
public void Corner_check_boundary_touch_is_out_on_the_out_side_and_in_on_the_in_side()
{
// Retail equality edges (flood appendix report 4): maxZ == h with
// inside-above rejects; minZ == h with inside-above accepts entirely.
Assert.Equal(
WalkBoundingType.Outside,
WalkVisibilityMath.CornerPlaneCheck(-5f, 2f, 5f));
Assert.Equal(
WalkBoundingType.EntirelyInside,
WalkVisibilityMath.CornerPlaneCheck(-5f, 5f, 10f));
// Inside-below: minZ == h rejects; maxZ == h accepts entirely.
Assert.Equal(
WalkBoundingType.Outside,
WalkVisibilityMath.CornerPlaneCheck(5f, 5f, 10f));
Assert.Equal(
WalkBoundingType.EntirelyInside,
WalkVisibilityMath.CornerPlaneCheck(5f, 2f, 5f));
}
// ---- block_plane_check @0x0054d060 ----
[Fact]
public void Plane_check_requires_unanimity_for_outside_and_entirely_inside()
{
Assert.Equal(
WalkBoundingType.Outside,
WalkVisibilityMath.BlockPlaneCheck(1001f, 1001f, 1001f, 1001f, 0f, 10f));
Assert.Equal(
WalkBoundingType.EntirelyInside,
WalkVisibilityMath.BlockPlaneCheck(0f, 0f, 0f, 0f, 0f, 10f));
// 3-of-4 outside is still PARTIAL (the block may straddle the plane).
Assert.Equal(
WalkBoundingType.PartiallyInside,
WalkVisibilityMath.BlockPlaneCheck(1001f, 1001f, 1001f, 0f, 0f, 10f));
Assert.Equal(
WalkBoundingType.PartiallyInside,
WalkVisibilityMath.BlockPlaneCheck(0f, 0f, 0f, 1001f, 0f, 10f));
}
// ---- block_check @0x0054dc50 ----
[Fact]
public void Block_check_culls_on_any_single_fully_outside_plane()
{
// Plane 0 (CY) inside everywhere; plane 1 outside at all four corners.
float[] c = [0f, 1001f];
Assert.Equal(
WalkBoundingType.Outside,
WalkVisibilityMath.BlockCheck(c, c, c, c, planeCount: 1, maxZ: 10f, minZ: 0f));
}
[Fact]
public void Block_check_demotion_is_sticky_across_planes()
{
// CY entirely inside; plane 1 partial at one corner; plane 2 entirely
// inside — the partial must survive to the final result.
float[] cornerA = [0f, -5f, 0f]; // straddles h=5 for slab [2,10]
float[] cornerB = [0f, 0f, 0f];
Assert.Equal(
WalkBoundingType.PartiallyInside,
WalkVisibilityMath.BlockCheck(
cornerA, cornerB, cornerB, cornerB, planeCount: 2, maxZ: 10f, minZ: 2f));
}
[Fact]
public void Block_check_is_entirely_inside_only_with_unanimity_on_every_plane()
{
float[] c = [0f, 0f, 0f];
Assert.Equal(
WalkBoundingType.EntirelyInside,
WalkVisibilityMath.BlockCheck(c, c, c, c, planeCount: 2, maxZ: 10f, minZ: 0f));
}
// ---- FillClipHeights (get_clip_height @0x0054cff0) ----
[Fact]
public void Clip_heights_write_the_cy_plane_then_every_edge_plane()
{
var cy = new WalkPlane(new Vector3(0, 0, 1), -5f);
WalkPlane[] edges =
[
new(new Vector3(0, 0, -1), 20f),
new(new Vector3(1, 0, 0), -100f),
];
Span<float> bounds = stackalloc float[3];
WalkVisibilityMath.FillClipHeights(0f, 0f, cy, edges, bounds);
Assert.Equal(-5f, bounds[0]);
Assert.Equal(20f, bounds[1]);
Assert.Equal(WalkVisibilityMath.OutsideColumn, bounds[2]);
}
// ---- viewconeCheck @0x0054c250 ----
private static readonly WalkPlane Cy = new(new Vector3(0, 1, 0), 0f); // forward = +Y, eye at origin
[Fact]
public void Sphere_fully_behind_the_near_plane_is_outside()
=> Assert.Equal(
WalkBoundingType.Outside,
WalkVisibilityMath.ViewconeCheck(new Vector3(0, -5, 0), 1f, Cy, []));
[Fact]
public void Cull_is_strict_and_partial_is_inclusive_at_the_boundary()
{
// d == -r exactly: NOT culled (strict d < -r), and partial (d <= r).
Assert.Equal(
WalkBoundingType.PartiallyInside,
WalkVisibilityMath.ViewconeCheck(new Vector3(0, -1, 0), 1f, Cy, []));
// d == +r exactly: tangent from inside counts PARTIAL, not entirely.
Assert.Equal(
WalkBoundingType.PartiallyInside,
WalkVisibilityMath.ViewconeCheck(new Vector3(0, 1, 0), 1f, Cy, []));
Assert.Equal(
WalkBoundingType.EntirelyInside,
WalkVisibilityMath.ViewconeCheck(new Vector3(0, 1.01f, 0), 1f, Cy, []));
}
[Fact]
public void Edge_planes_cull_and_demote_like_the_cy_plane()
{
WalkPlane[] edges = [new(new Vector3(1, 0, 0), 0f)]; // inside is x >= 0
Assert.Equal(
WalkBoundingType.Outside,
WalkVisibilityMath.ViewconeCheck(new Vector3(-3, 5, 0), 1f, Cy, edges));
Assert.Equal(
WalkBoundingType.PartiallyInside,
WalkVisibilityMath.ViewconeCheck(new Vector3(0.5f, 5, 0), 1f, Cy, edges));
Assert.Equal(
WalkBoundingType.EntirelyInside,
WalkVisibilityMath.ViewconeCheck(new Vector3(3, 5, 0), 1f, Cy, edges));
}
// ---- DrawPortalPolyInternal @0x0059bc90's degenerate-input guard
// (S4-c1 C1, T2): ANY source vertex whose LOCAL x or y lands exactly on
// +/-12 rejects the WHOLE polygon. ----
[Theory]
[InlineData(12f, 0f)] // x == +12 exactly
[InlineData(-12f, 0f)] // x == -12 exactly
[InlineData(0f, 12f)] // y == +12 exactly
[InlineData(0f, -12f)] // y == -12 exactly
public void Boundary_guard_rejects_a_polygon_with_one_vertex_exactly_on_plus_minus_12(
float x, float y)
{
// MUTATION (verified during S4-c1's own implementation): narrow the
// guard's exact equality to a strict `x > 12f` / `x < -12f` (no
// boundary-inclusive case at all) — the exact +/-12 boundary this
// theory's four rows probe stops rejecting and all four fail.
Vector3[] polygon = [new Vector3(0, 0, 3), new Vector3(x, y, 3), new Vector3(5, 5, 3)];
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
}
[Theory]
[InlineData(11.999f, 0f)]
[InlineData(-11.999f, 0f)]
[InlineData(0f, 11.999f)]
[InlineData(0f, -11.999f)]
public void Boundary_guard_admits_a_polygon_whose_nearest_vertex_is_just_inside_12(
float x, float y)
{
// MUTATION (verified during S4-c1's own implementation): widen the
// guard's exact equality to a near-boundary tolerance, e.g.
// `MathF.Abs(x) >= 11.99f` instead of `x == 12f` — a "close enough
// to the boundary" mistake that still leaves ordinary far-from-12
// coordinates alone. 11.999 sits inside that widened band, so this
// theory's four rows fail; a strict `x >= 12f` (no tolerance at all)
// does NOT catch this test — 11.999 &lt; 12 either way — which is
// exactly why Boundary_guard_rejects_a_polygon_with_one_vertex_
// exactly_on_plus_minus_12 above exists as the other half of the
// pin: it fails instead if the guard is narrowed to a strict `&gt;`
// that lets the exact +/-12 boundary through.
Vector3[] polygon = [new Vector3(0, 0, 3), new Vector3(x, y, 3), new Vector3(5, 5, 3)];
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
}
[Fact]
public void Boundary_guard_rejects_the_whole_polygon_even_when_only_one_of_several_vertices_hits_it()
{
// Retail's four var_* flags are OR'd across the WHOLE vertex loop
// before the single post-loop decision (0x59BD42-0x59BD66) — a
// degenerate vertex anywhere in the fan condemns every vertex in it,
// not just its own. MUTATION (verified during S4-c1's own
// implementation): check only localVertices[0] instead of looping
// every vertex — the degenerate vertex here is the LAST of four, so
// the guard would wrongly return false and this fails.
Vector3[] polygon =
[
new Vector3(-3f, -3f, 3f),
new Vector3(3f, -3f, 3f),
new Vector3(3f, 3f, 3f),
new Vector3(12f, 3f, 3f), // the one degenerate vertex
];
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
}
[Fact]
public void Boundary_guard_ignores_the_vertical_z_component()
{
// Retail's guard reads only the two horizontal FIELDS the decomp
// names (arg1->vertices[i] as the x source, ecx_1[1] as the y
// source) — a vertex whose HEIGHT happens to be +/-12 is an entirely
// ordinary local coordinate and must not trip the guard.
Vector3[] polygon = [new Vector3(0, 0, 12f), new Vector3(1, 1, -12f), new Vector3(2, 0, 0)];
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
}
[Fact]
public void Boundary_guard_admits_the_empty_polygon()
{
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard([]));
}
}