// ============================================================================
// dc_level_shell PARITY GATE — does the libdc port reproduce the oracle?
//
// tests/test_outer_shell.cpp established the approach and the settings against
// the real START.MAP: cell 0.25, clamp 0.5 cells, degenerates kept, winding
// reversed, sign check 20000/20000 on interior, solid and exterior.
//
// dc_level_shell.cpp is that algorithm moved into libdc so the engine can call
// it. A port is a rewrite, so this checks the numbers still land, and then
// re-runs the check that actually matters: does the SDF built from this mesh say
// INSIDE for the level and OUTSIDE for the void?
//
// Sign sampling here does NOT need the flood-fill grid, so it stays independent
// of the module's internals: a point inside any brush must be inside the shell,
// and a point far outside the level's bounds must be outside it. Those are true
// by definition of a sealed volume, whatever the implementation does.
//
// Input: /tmp/startmap_brushes_sky.bin
// (godot4 --headless --script res://scenes/test/oracle_dump.gd -- --sky)
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/level_shell \
// tests/test_level_shell.cpp libdc/src/dc_csg4.cpp \
// libdc/src/dc_level_shell.cpp libdc/src/generators/dc_generator_mesh_sdf.cpp
// ============================================================================
#include "dc_level_shell.h"
#include "dc_generator_mesh_sdf.h"
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <vector>
#include <random>
#include <cmath>
#include <algorithm>
struct DumpBrush {
float cx, cy, cz, radius, strength;
uint32_t material;
std::vector<ConvexPlane> planes;
};
static bool read_dump(const char* path, std::vector<DumpBrush>& out) {
FILE* f = fopen(path, "rb");
if (!f) { fprintf(stderr, "cannot open %s\n", path); return false; }
char magic[4];
if (fread(magic, 1, 4, f) != 4 || memcmp(magic, "DCOR", 4) != 0) { fclose(f); return false; }
uint32_t version; float ws, ch[3], cc[3];
if (fread(&version,4,1,f)!=1 || fread(&ws,4,1,f)!=1 ||
fread(ch,4,3,f)!=3 || fread(cc,4,3,f)!=3) { fclose(f); return false; }
uint32_t n;
if (fread(&n,4,1,f)!=1) { fclose(f); return false; }
out.resize(n);
for (uint32_t i = 0; i < n; i++) {
DumpBrush& b = out[i];
if (fread(&b.cx,4,1,f)!=1||fread(&b.cy,4,1,f)!=1||fread(&b.cz,4,1,f)!=1||
fread(&b.radius,4,1,f)!=1||fread(&b.strength,4,1,f)!=1||
fread(&b.material,4,1,f)!=1) { fclose(f); return false; }
uint32_t pc;
if (fread(&pc,4,1,f)!=1) { fclose(f); return false; }
b.planes.resize(pc);
for (uint32_t p = 0; p < pc; p++) {
if (fread(&b.planes[p].nx,4,1,f)!=1||fread(&b.planes[p].ny,4,1,f)!=1||
fread(&b.planes[p].nz,4,1,f)!=1||fread(&b.planes[p].d,4,1,f)!=1) { fclose(f); return false; }
uint32_t proj; if (fread(&proj,4,1,f)!=1) { fclose(f); return false; }
}
}
fclose(f);
return true;
}
static inline bool inside_brush(const DumpBrush& b, float x, float y, float z) {
for (const ConvexPlane& p : b.planes)
if (p.nx*x + p.ny*y + p.nz*z - p.d > 0.0f) return false;
return true;
}
static int failures = 0;
static void check(const char* label, bool ok, const char* detail) {
if (!ok) failures++;
printf(" %-38s %s %s\n", label, ok ? "OK " : "FAIL", detail);
}
int main(int argc, char** argv) {
const char* path = (argc > 1) ? argv[1] : "/tmp/startmap_brushes_sky.bin";
std::vector<DumpBrush> dump;
if (!read_dump(path, dump)) return 1;
std::vector<CSG4Brush> brushes(dump.size());
for (size_t i = 0; i < dump.size(); i++) {
brushes[i].planes = dump[i].planes.data();
brushes[i].plane_count = (uint32_t)dump[i].planes.size();
brushes[i].material_id = dump[i].material;
brushes[i].id = (uint32_t)i;
}
dc_csg4::compute_bounds(brushes);
dc_level_shell::ShellMesh shell;
if (!dc_level_shell::build(brushes, shell)) { printf("FAIL — build() returned false\n"); return 1; }
printf("dc_level_shell on %zu brushes\n", dump.size());
printf(" grid %dx%dx%d solid %lld exterior %lld interior %lld\n",
shell.grid[0], shell.grid[1], shell.grid[2],
shell.cells_solid, shell.cells_exterior, shell.cells_interior);
printf(" verts %d tris %d snapped %lld unmoved %lld\n",
shell.vertex_count(), shell.triangle_count(), shell.snapped, shell.unmoved);
printf(" area %.0f m^2 volume %.0f m^3\n\n", shell.area, shell.volume);
// ---- parity with tests/test_outer_shell.cpp at the same settings ----
char buf[128];
snprintf(buf, sizeof(buf), "%d vs 563544", shell.triangle_count());
check("triangles match the oracle", shell.triangle_count() == 563544, buf);
snprintf(buf, sizeof(buf), "%.0f vs ~38498", shell.volume);
check("volume within 1% of the oracle", std::fabs(shell.volume - 38498.0) < 385.0, buf);
snprintf(buf, sizeof(buf), "%lld", shell.cells_interior);
check("interior cells found (fill sealed)", shell.cells_interior > 100000, buf);
// ---- watertightness, and a control that PROVES the check can fail ----
printf(" boundary_edges %lld nonmanifold %lld solid %.0f m^3 interior %.0f m^3 sealed=%s\n",
shell.boundary_edges, shell.nonmanifold_edges,
shell.solid_volume, shell.interior_volume, shell.sealed() ? "yes" : "NO");
snprintf(buf, sizeof(buf), "%lld", shell.boundary_edges);
check("shell is closed (0 boundary edges)", shell.boundary_edges == 0, buf);
check("shell is SEALED (air > wall)", shell.sealed(), shell.sealed() ? "yes" : "NO");
// NEGATIVE CONTROL. A brush deleted from the outer shell is exactly what the
// editor's Delete key does, and those deletions are durable and replay on
// load. Punch the boundary and the fill escapes: the interior collapses and
// the shell shrink-wraps the solid — still CLOSED, still a plausible volume,
// enclosing none of the rooms. If sealed() cannot tell that apart from a good
// shell then it is decoration, so this removes the biggest brushes (the outer
// walls and floor) and requires the answer to change.
{
std::vector<size_t> order(brushes.size());
for (size_t i = 0; i < order.size(); i++) order[i] = i;
std::sort(order.begin(), order.end(), [&](size_t a, size_t b) {
auto vol = [&](size_t i) {
const CSG4Brush& x = brushes[i];
return (double)(x.hi.x-x.lo.x) * (x.hi.y-x.lo.y) * (x.hi.z-x.lo.z);
};
return vol(a) > vol(b);
});
std::vector<CSG4Brush> holed;
const size_t drop = 12;
for (size_t k = drop; k < order.size(); k++) holed.push_back(brushes[order[k]]);
dc_level_shell::ShellMesh bad;
if (dc_level_shell::build(holed, bad)) {
printf(" CONTROL, %zu largest brushes removed: boundary_edges %lld "
"solid %.0f interior %.0f sealed=%s\n",
drop, bad.boundary_edges, bad.solid_volume, bad.interior_volume,
bad.sealed() ? "yes" : "NO");
check("a holed level is NOT sealed", !bad.sealed(),
bad.sealed() ? "still reports sealed — the check is decoration" : "caught");
}
}
// ---- the check that matters: SDF signs ----
// Convention (dc_generator_mesh_sdf.h:15): positive = inside.
FILE* o = fopen("/tmp/level_shell.obj", "w");
for (int i = 0; i < shell.vertex_count(); i++)
fprintf(o, "v %.6f %.6f %.6f\n", shell.vertices[i*3], shell.vertices[i*3+1], shell.vertices[i*3+2]);
for (size_t i = 0; i + 2 < shell.indices.size(); i += 3)
fprintf(o, "f %d %d %d\n", shell.indices[i]+1, shell.indices[i+1]+1, shell.indices[i+2]+1);
fclose(o);
DCGeneratorMeshSDF sdf;
if (!sdf.load_obj("/tmp/level_shell.obj")) { printf("FAIL — could not load emitted mesh\n"); return 1; }
float blo[3] = {1e30f,1e30f,1e30f}, bhi[3] = {-1e30f,-1e30f,-1e30f};
for (const CSG4Brush& b : brushes) {
blo[0] = std::min(blo[0], b.lo.x); bhi[0] = std::max(bhi[0], b.hi.x);
blo[1] = std::min(blo[1], b.lo.y); bhi[1] = std::max(bhi[1], b.hi.y);
blo[2] = std::min(blo[2], b.lo.z); bhi[2] = std::max(bhi[2], b.hi.z);
}
std::mt19937 rng(99);
// (a) points inside a brush must read INSIDE the shell.
int solid_ok = 0, solid_n = 0;
std::uniform_int_distribution<size_t> pick(0, dump.size() - 1);
while (solid_n < 5000) {
const DumpBrush& b = dump[pick(rng)];
std::uniform_real_distribution<float> ux(b.cx - b.radius, b.cx + b.radius),
uy(b.cy - b.radius, b.cy + b.radius),
uz(b.cz - b.radius, b.cz + b.radius);
float x = ux(rng), y = uy(rng), z = uz(rng);
if (!inside_brush(b, x, y, z)) continue;
solid_n++;
if (sdf.evaluate(x, y, z, dc_level_shell::DEFAULT_CELL) > 0.0f) solid_ok++;
}
snprintf(buf, sizeof(buf), "%d/%d", solid_ok, solid_n);
check("points in a brush read INSIDE", solid_ok == solid_n, buf);
// (b) points well outside the level's bounds must read OUTSIDE.
int out_ok = 0, out_n = 0;
std::uniform_real_distribution<float> fx(blo[0]-40.0f, bhi[0]+40.0f),
fy(blo[1]-40.0f, bhi[1]+40.0f),
fz(blo[2]-40.0f, bhi[2]+40.0f);
while (out_n < 5000) {
float x = fx(rng), y = fy(rng), z = fz(rng);
// Only sample clearly OUTSIDE the level's own bounds, so this stays a
// statement about the void and not about the fringe.
if (x > blo[0]-2 && x < bhi[0]+2 && y > blo[1]-2 && y < bhi[1]+2 &&
z > blo[2]-2 && z < bhi[2]+2) continue;
out_n++;
if (sdf.evaluate(x, y, z, dc_level_shell::DEFAULT_CELL) < 0.0f) out_ok++;
}
snprintf(buf, sizeof(buf), "%d/%d", out_ok, out_n);
check("points in open void read OUTSIDE", out_ok == out_n, buf);
printf("\nRESULT: %s\n", failures == 0 ? "PASS" : "FAIL");
return failures == 0 ? 0 : 1;
}