// ============================================================================
// DCBrushStore GATE — docs/level-mesh-implementation.md §12 step 2.
//
// Headline requirement: "apply N carves, undo N, assert the brush set is
// bit-identical to the start."
//
// That is the architectural invariant the whole design rests on:
//
// *** A CARVE NEVER MUTATES A BRUSH. ***
//
// If it held only by accident, undo would be an approximation and error would
// accumulate across edits. The fingerprint is an order-independent hash of every
// live brush's planes/proj/material, so "bit-identical" is checked, not assumed.
//
// Runs on the REAL 1164-brush START.MAP dump when present, and on a synthetic
// set otherwise, so it is useful without the dump.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/brush_store \
// tests/test_brush_store.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_brush_store.h"
#include "dc_csg4.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
static int g_fail = 0;
static void ok(const char* what, bool cond) {
if (!cond) g_fail++;
printf(" %-58s %s\n", what, cond ? "OK" : "**FAIL**");
}
static void eq_u(const char* what, uint64_t got, uint64_t want) {
const bool c = (got == want);
if (!c) g_fail++;
printf(" %-58s %-12llu want %-12llu %s\n", what,
(unsigned long long)got, (unsigned long long)want, c ? "OK" : "**FAIL**");
}
// ---------------------------------------------------------------------------
static bool load_dump(const char* path, DCBrushStore& store, uint32_t level_id) {
FILE* f = fopen(path, "rb");
if (!f) return false;
char m[4]; size_t r = fread(m, 1, 4, f); (void)r;
if (memcmp(m, "DCOR", 4) != 0) { fclose(f); return false; }
unsigned ver; float ws; float carve[6]; unsigned n;
r = fread(&ver, 4, 1, f); r = fread(&ws, 4, 1, f);
r = fread(carve, 4, 6, f); r = fread(&n, 4, 1, f);
for (unsigned i = 0; i < n; i++) {
float c[3], radius, strength; unsigned material, pc;
r = fread(c, 4, 3, f); r = fread(&radius, 4, 1, f);
r = fread(&strength, 4, 1, f); r = fread(&material, 4, 1, f);
r = fread(&pc, 4, 1, f);
std::vector<ConvexPlane> planes; std::vector<uint32_t> proj;
planes.reserve(pc); proj.reserve(pc);
for (unsigned p = 0; p < pc; p++) {
float v[4]; unsigned pr;
r = fread(v, 4, 4, f); r = fread(&pr, 4, 1, f);
planes.push_back({v[0], v[1], v[2], v[3]});
proj.push_back(pr);
}
store.add_brush(planes, proj, material, level_id);
}
fclose(f);
return true;
}
static void add_cube(DCBrushStore& s, float cx, float cy, float cz, float h,
uint32_t mat, uint32_t level) {
const float ax[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}};
std::vector<ConvexPlane> pl; std::vector<uint32_t> pr;
for (int i = 0; i < 6; i++) {
const float d = ax[i][0]*cx + ax[i][1]*cy + ax[i][2]*cz + h;
pl.push_back({ax[i][0], ax[i][1], ax[i][2], d});
pr.push_back((uint32_t)(i + 1));
}
s.add_brush(pl, pr, mat, level);
}
static TerrainEdit sphere_carve(float x, float y, float z, float r) {
TerrainEdit e;
e.shape = TerrainEdit::SPHERE;
e.op = TerrainEdit::OP_HARD;
e.x = x; e.y = y; e.z = z;
e.radius = r;
e.strength = -3.0f;
return e;
}
// ---------------------------------------------------------------------------
int main(int argc, char** argv) {
const char* dump = (argc > 1) ? argv[1] : "/tmp/startmap_brushes.bin";
DCBrushStore s;
bool real = load_dump(dump, s, /*level_id*/ 1);
if (real) {
printf("DCBrushStore GATE — real START.MAP dump (%zu brushes)\n\n", s.brushes().size());
} else {
printf("DCBrushStore GATE — dump not found, synthetic set\n\n");
for (int i = 0; i < 8; i++) add_cube(s, i * 3.0f, 0, 0, 1.0f, (uint32_t)i, 1);
}
const size_t N = s.brushes().size();
// ---- THE INVARIANT: carves must not touch brushes --------------------
printf("INVARIANT — a carve never mutates a brush\n");
const uint64_t fp0 = s.fingerprint();
std::vector<uint32_t> carve_ids;
const CSG4Vec3 c0 = s.brushes()[0].lo;
for (int i = 0; i < 10; i++)
carve_ids.push_back(s.apply_carve(
sphere_carve(c0.x + i * 0.4f, c0.y + 0.2f, c0.z, 1.5f)));
const uint64_t fp_after_carves = s.fingerprint();
eq_u("fingerprint after 10 carves", fp_after_carves, fp0);
ok("carves recorded", s.carves().size() == 10);
uint32_t carved_n = 0;
{ std::vector<uint32_t> v; s.carved_brushes(v); carved_n = (uint32_t)v.size(); }
ok("at least one brush classified CARVED", carved_n > 0);
printf(" (%u brushes carved, %zu pockets)\n", carved_n, [&]{
std::vector<std::vector<uint32_t>> p; s.pockets(p); return p.size(); }());
for (uint32_t id : carve_ids) s.undo_carve(id);
eq_u("fingerprint after undoing all 10", s.fingerprint(), fp0);
{ std::vector<uint32_t> v; s.carved_brushes(v);
eq_u("carved brushes after full undo", v.size(), 0); }
{ std::vector<uint32_t> v; s.clean_brushes(v);
eq_u("clean brushes after full undo", v.size(), N); }
// ---- tombstones ------------------------------------------------------
printf("\nTOMBSTONES (there is no durable delete path — see game_state.gd)\n");
const uint32_t first = s.brushes()[0].id;
ok("remove_brush tombstones", s.remove_brush(first));
{ std::vector<uint32_t> v; s.clean_brushes(v);
eq_u("clean count drops by one", v.size(), N - 1); }
ok("tombstoned brush still retrievable by id", s.get(first) != nullptr);
ok("restore_brush un-tombstones", s.restore_brush(first));
eq_u("fingerprint restored after tombstone+restore", s.fingerprint(), fp0);
const uint32_t removed = s.remove_level(1);
eq_u("remove_level tombstones every brush", removed, N);
{ std::vector<uint32_t> v; s.clean_brushes(v);
eq_u("clean count after delete-the-level", v.size(), 0); }
eq_u("restore_level restores every brush", s.restore_level(1), N);
eq_u("fingerprint restored after level delete+restore", s.fingerprint(), fp0);
// ---- shape editing DOES change geometry (contrast with carves) -------
printf("\nSHAPE EDITING — stays a brush, and DOES change the fingerprint\n");
const uint32_t gen0 = s.get(first)->generation;
ok("translate_brush", s.translate_brush(first, 1.0f, 0.0f, 0.0f));
ok("fingerprint CHANGED by a transform", s.fingerprint() != fp0);
ok("generation bumped", s.get(first)->generation > gen0);
ok("translate back", s.translate_brush(first, -1.0f, 0.0f, 0.0f));
eq_u("fingerprint returns after inverse translate", s.fingerprint(), fp0);
const uint32_t nfaces = (uint32_t)s.get(first)->planes.size();
const uint32_t fi = s.add_face(first, {0.577f, 0.577f, 0.577f, 999.0f}, 42u);
ok("add_face appended", fi == nfaces);
eq_u("plane_proj stayed parallel after add", s.get(first)->plane_proj.size(),
s.get(first)->planes.size());
ok("added face carries its proj", s.get(first)->plane_proj[fi] == 42u);
ok("remove_face", s.remove_face(first, fi));
eq_u("plane_proj stayed parallel after remove", s.get(first)->plane_proj.size(),
s.get(first)->planes.size());
eq_u("fingerprint returns after add+remove face", s.fingerprint(), fp0);
// Use a SCRATCH store — asserting this against `s` would actually remove a
// face (the guard correctly permits 6->5) and corrupt the fixture for every
// check after it. That is exactly what it did on the first run: the final
// fingerprint drifted and csg4 reported +2 faces off a genuinely changed hull.
{
DCBrushStore scratch;
add_cube(scratch, 0, 0, 0, 1.0f, 0, 1);
const uint32_t sid = scratch.brushes()[0].id;
ok("scratch cube has 6 faces", scratch.get(sid)->planes.size() == 6);
ok("remove_face 6->5 allowed", scratch.remove_face(sid, 0));
ok("remove_face 5->4 allowed", scratch.remove_face(sid, 0));
eq_u("scratch now at 4 planes", scratch.get(sid)->planes.size(), 4);
ok("remove_face REFUSES 4->3 (would be unbounded)", !scratch.remove_face(sid, 0));
eq_u("still 4 planes after refusal", scratch.get(sid)->planes.size(), 4);
}
// ---- csg4 handoff ----------------------------------------------------
printf("\nCSG4 HANDOFF — CLEAN brushes feed the exact mesher\n");
{
std::vector<CSG4Brush> view;
s.csg4_view(view);
eq_u("csg4_view exposes every clean brush", view.size(), N);
bool bounds_ok = true;
for (const CSG4Brush& v : view) if (!v.bounds_valid) bounds_ok = false;
ok("all views carry valid bounds", bounds_ok);
if (real) { // the store must reproduce the committed csg4 numbers
std::vector<CSG4Face> faces; CSG4Stats st;
dc_csg4::clip_all(view, faces, &st);
eq_u("clipped faces via store", st.out_faces, 6487);
eq_u("clipped triangles via store", st.out_tris, 12747);
}
// A carved brush must DROP OUT of the mesher's input.
const uint32_t cid = s.apply_carve(sphere_carve(c0.x, c0.y, c0.z, 1.5f));
std::vector<CSG4Brush> view2;
s.csg4_view(view2);
ok("carved brushes leave the csg4 view", view2.size() < view.size());
s.undo_carve(cid);
std::vector<CSG4Brush> view3;
s.csg4_view(view3);
eq_u("undo returns them to the csg4 view", view3.size(), view.size());
}
eq_u("\nFINAL fingerprint == initial", s.fingerprint(), fp0);
printf("\n%s\n", g_fail == 0 ? "GATE PASSED" : "GATE FAILED");
return g_fail == 0 ? 0 : 1;
}