OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / tests / test_brush_store.cpp

218 lines · cpp ref:
// ============================================================================
// 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;
}