using AcDream.Core.Terrain; using Xunit; using Xunit.Abstractions; using WbTerrainUtils = WorldBuilder.Shared.Modules.Landscape.Lib.TerrainUtils; using WbCellSplitDirection = WorldBuilder.Shared.Modules.Landscape.Models.CellSplitDirection; namespace AcDream.Core.Tests.Terrain; /// /// Phase N.5b data-collection test: quantifies how often WB's /// TerrainUtils.CalculateSplitDirection disagrees with acdream's /// TerrainBlending.CalculateSplitDirection (which retail uses /// per CLandBlockStruct::ConstructPolygons at retail address /// 00531d10; named-retail decomp lines 316042-316144 contain /// the exact constants 0x0CCAC033 / 0x6C1AC587 / 0x421BE3BD / /// 0x519B8F25). /// /// Sweeps every (lbX, lbY, cellX, cellY) tuple in the world map /// (255 x 255 landblocks x 64 cells = ~4.16M cells) and reports the /// disagreement rate, per-landblock worst case, and a few named /// representative landblocks. The number drives the Path A/B/C /// decision in the N.5b brainstorm: /// - Low disagreement <5% : Path A's risk is bounded /// - Medium 5-20% : Path B (fork-patch WB) preferred /// - High >20% : Path B/C strongly preferred /// public class SplitFormulaDivergenceTest { private readonly ITestOutputHelper _out; public SplitFormulaDivergenceTest(ITestOutputHelper output) => _out = output; [Fact] public void Quantify_RetailVsWb_DivergenceRate() { // Two divergence flavors are tracked simultaneously: // // rawDisagree : retail-enum != wb-enum (pure formula output) // diagonalDisagree: retail-actually-paints-diagonal != // wb-actually-paints-diagonal (effective geometry) // // The two differ because the enums are SEMANTICALLY INVERTED: // - acdream `CellSplitDirection.SWtoNE` -> renderer paints BL->TR // (SW-NE diagonal). Matches retail per AC2D Landblocks.cpp:400-412 // where FSplitNESW=true wraps a TRIANGLE_FAN [BL, BR, TR, TL] = // diagonal BL-TR. // - WB `CellSplitDirection.SWtoNE` -> WB's TerrainGeometryGenerator // emits triangles {BL,TL,BR}+{BR,TL,TR} which share the BR-TL // diagonal (SE-NW direction). The enum name is misleading; what // WB actually draws is the OTHER diagonal. // // So the question "would WB's pipeline produce the same diagonals as // retail's pipeline?" is answered by `diagonalDisagree`, not // `rawDisagree`. If diagonalDisagree is near 0%, WB's formula + // renderer happen to compose into a correct pipeline (despite the // confusing labels). If diagonalDisagree is ~50%, the two pipelines // truly diverge and Path A would visibly break terrain on every // landblock. const int lbCount = 255; const int cellsPerSide = 8; long totalCells = 0; long rawDisagree = 0; long diagonalDisagree = 0; int worstLbDiag = 0; uint worstLbX = 0, worstLbY = 0; int bestLbDiag = 64; uint bestLbX = 0, bestLbY = 0; for (uint lbX = 0; lbX < lbCount; lbX++) for (uint lbY = 0; lbY < lbCount; lbY++) { int lbDiagDisagree = 0; for (uint cx = 0; cx < cellsPerSide; cx++) for (uint cy = 0; cy < cellsPerSide; cy++) { bool retailEnumSWtoNE = TerrainBlending.CalculateSplitDirection(lbX, cx, lbY, cy) == CellSplitDirection.SWtoNE; bool wbEnumSWtoNE = WbTerrainUtils.CalculateSplitDirection(lbX, cx, lbY, cy) == WbCellSplitDirection.SWtoNE; // What diagonal each pipeline actually paints. bool retailPaintsBLtoTR = retailEnumSWtoNE; // direct mapping bool wbPaintsBLtoTR = !wbEnumSWtoNE; // inverted mapping totalCells++; if (retailEnumSWtoNE != wbEnumSWtoNE) rawDisagree++; if (retailPaintsBLtoTR != wbPaintsBLtoTR) { diagonalDisagree++; lbDiagDisagree++; } } if (lbDiagDisagree > worstLbDiag) { worstLbDiag = lbDiagDisagree; worstLbX = lbX; worstLbY = lbY; } if (lbDiagDisagree < bestLbDiag) { bestLbDiag = lbDiagDisagree; bestLbX = lbX; bestLbY = lbY; } } double rawPct = 100.0 * rawDisagree / totalCells; double diagPct = 100.0 * diagonalDisagree / totalCells; _out.WriteLine($"=== Phase N.5b — terrain split formula divergence ==="); _out.WriteLine($"Sweep: {lbCount}x{lbCount} landblocks, {cellsPerSide*cellsPerSide} cells each"); _out.WriteLine($"Total cells: {totalCells:N0}"); _out.WriteLine(""); _out.WriteLine($"RAW enum-output disagreement : {rawDisagree,12:N0} ({rawPct:F2}%)"); _out.WriteLine($" (compares retail-enum vs wb-enum, NOT what each system actually draws)"); _out.WriteLine(""); _out.WriteLine($"DIAGONAL-actually-painted disagreement: {diagonalDisagree,12:N0} ({diagPct:F2}%)"); _out.WriteLine($" (compares retail-paints-BL->TR vs wb-paints-BL->TR; this is the"); _out.WriteLine($" number that determines whether Path A visibly works)"); _out.WriteLine(""); _out.WriteLine($"Worst landblock (diagonal): 0x{worstLbX:X2}{worstLbY:X2} disagrees on {worstLbDiag}/64 cells ({100.0*worstLbDiag/64:F1}%)"); _out.WriteLine($"Best landblock (diagonal): 0x{bestLbX:X2}{bestLbY:X2} disagrees on {bestLbDiag}/64 cells ({100.0*bestLbDiag/64:F1}%)"); // Specific landblocks of interest (per N.5b handoff representative set). var representative = new (string name, uint lbX, uint lbY)[] { ("Holtburg town", 0xA9, 0xB0), ("Holtburg LB 0xA9B1", 0xA9, 0xB1), ("Foundry-area", 0x80, 0x80), ("Cragstone", 0xCB, 0x99), ("Direlands sample", 0xC0, 0x40), ("MapOrigin 0x0000", 0x00, 0x00), ("MapCorner 0xFEFE", 0xFE, 0xFE), ("Mid-map 0x7F7F", 0x7F, 0x7F), ("Subway dungeon LB 0x0185 outdoor part", 0x01, 0x85), }; _out.WriteLine(""); _out.WriteLine("Representative landblocks (diagonal-actually-painted disagreement):"); foreach (var (name, lbX, lbY) in representative) { int dis = 0; for (uint cx = 0; cx < 8; cx++) for (uint cy = 0; cy < 8; cy++) { bool retailEnum = TerrainBlending.CalculateSplitDirection(lbX, cx, lbY, cy) == CellSplitDirection.SWtoNE; bool wbEnum = WbTerrainUtils.CalculateSplitDirection(lbX, cx, lbY, cy) == WbCellSplitDirection.SWtoNE; bool retailPaintsBLtoTR = retailEnum; bool wbPaintsBLtoTR = !wbEnum; if (retailPaintsBLtoTR != wbPaintsBLtoTR) dis++; } _out.WriteLine($" 0x{lbX:X2}{lbY:X2} {dis,2}/64 cells disagree ({100.0*dis/64:F1}%) {name}"); } // Soft-floor on the DIAGONAL comparison: if diagPct is near 0% the // formulas are equivalent post-inversion (Path A would just work // visually; the only "bug" is enum naming). If diagPct is well // above 0%, Path A truly breaks terrain. // Soft-ceiling: an inversion of inversion shouldn't push past ~70%. Assert.True(diagPct >= 0 && diagPct <= 100, $"Sanity: diagonal disagreement out of range (rate={diagPct:F2}%)"); } }