// ============================================================================
// CARVE IDENTITY — undo must roll back the carve the author meant, on every peer.
//
// hyper-games #22 §5 item 4, "shipped and wrong". level_persistence.gd persists a
// carve with `local_id`, the value of the AUTHOR's `next_carve_id_` counter, and
// persist_undo/_on_state_changed then call undo_carve(local_id) on whatever peer
// is replaying. A replaying peer mints its OWN ids as it applies carves, so that
// number names a different carve there — and the store cheerfully undoes it. No
// error, no divergence report: a wall comes back somewhere else.
//
// The CONTROL below reproduces exactly that, with the minimum that makes the ids
// disagree: the replaying peer carved once itself first. That is not a contrived
// setup — it is any peer that touched the level before a late-joining replay, and
// it is also just "peers applied things in a different order", which a
// last-write-wins KV view guarantees will happen.
//
// The fix is the same construction faces and brushes now use: the carve is named
// by the DOT of the op that created it. `world:level:<author>:op:<seq>` already IS
// that pair, so the state key carries the identity and no `local_id` field needs
// to exist at all.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/cident \
// tests/test_carve_identity.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_brush_store.h"
#include "dc_csg4.h"
#include <cstdio>
#include <string>
#include <vector>
static int fails=0;
static void ok(const char*w,bool c){ if(!c)fails++; printf(" %-58s %s\n",w,c?"OK":"**FAIL**"); }
static void eqi(const char*w,long g,long v){ bool c=g==v; if(!c)fails++; printf(" %-58s %-6ld want %-6ld %s\n",w,g,v,c?"OK":"**FAIL**"); }
static std::vector<ConvexPlane> box(float a,float b,float c,float d,float e,float f){
return {{-1,0,0,-a},{1,0,0,d},{0,-1,0,-b},{0,1,0,e},{0,0,-1,-c},{0,0,1,f}};
}
static const std::vector<uint32_t> P6{0,0,0,0,0,0};
static TerrainEdit sphere(float x,float r){
TerrainEdit e; e.shape=TerrainEdit::SPHERE; e.op=TerrainEdit::OP_HARD;
e.x=x; e.y=0; e.z=0; e.radius=r; e.strength=-1.0f; return e;
}
// Is the carve at this x still ACTIVE in the store? That is the only question the
// player can perceive, so it is the one the test asks.
static bool active_at(const DCBrushStore&s,float x){
for(const DCCarve&c:s.carves())
if(std::fabs(c.shape.x-x)<1e-4f) return !c.undone;
return false;
}
int main(){
printf("=== Carve identity across peers ===\n\n");
printf("-- CONTROL: undo-by-local-id rolls back the WRONG carve --\n");
// Peer A authors two carves. Its local ids happen to be 1 and 2.
DCBrushStore A;
A.add_brush(box(-40,-2,-2,40,2,2),P6,1,0);
const uint32_t a_x=A.apply_carve(sphere(10.0f,0.6f)); // "alice:1"
const uint32_t a_y=A.apply_carve(sphere(20.0f,0.6f)); // "alice:2"
eqi("A's first carve is local id",(long)a_x,1);
eqi("A's second carve is local id",(long)a_y,2);
// Peer B carved once itself, THEN replayed A's two records. Same journal, same
// order — only B's counter was not where A's was.
DCBrushStore B;
B.add_brush(box(-40,-2,-2,40,2,2),P6,1,0);
B.apply_carve(sphere(-5.0f,0.6f)); // B's own
B.apply_carve(sphere(10.0f,0.6f)); // replay of A's first
B.apply_carve(sphere(20.0f,0.6f)); // replay of A's second
eqi("B holds three carves",(long)B.carves().size(),3);
// A undoes its SECOND carve and publishes {undone:true, local_id:2}.
ok("A undoes its own second carve",A.undo_carve(a_y));
ok ("on A: the carve at x=20 is gone",!active_at(A,20.0f));
ok ("on A: the carve at x=10 is untouched",active_at(A,10.0f));
// B replays that record the way the shipped code does: by the author's number.
B.undo_carve(2);
ok ("on B: the carve at x=20 is STILL THERE (the bug)",active_at(B,20.0f));
ok ("on B: the carve at x=10 was undone instead (the bug)",!active_at(B,10.0f));
printf("\n-- the dot names the carve, so the same record undoes the same carve --\n");
DCBrushStore A2,B2;
A2.add_brush(box(-40,-2,-2,40,2,2),P6,1,0);
B2.add_brush(box(-40,-2,-2,40,2,2),P6,1,0);
A2.apply_carve(sphere(10.0f,0.6f),"alice:1");
A2.apply_carve(sphere(20.0f,0.6f),"alice:2");
B2.apply_carve(sphere(-5.0f,0.6f),"bob:1");
B2.apply_carve(sphere(10.0f,0.6f),"alice:1");
B2.apply_carve(sphere(20.0f,0.6f),"alice:2");
ok("A2 undoes alice:2",A2.undo_carve_by_key("alice:2"));
ok("B2 replays that same dot",B2.undo_carve_by_key("alice:2"));
ok ("on A2: x=20 gone",!active_at(A2,20.0f));
ok ("on B2: x=20 gone — the SAME carve",!active_at(B2,20.0f));
ok ("on B2: x=10 untouched",active_at(B2,10.0f));
ok ("on B2: bob's own carve untouched",active_at(B2,-5.0f));
printf("\n-- resolving, redo, and replay idempotence --\n");
eqi("carve_by_key resolves to the local handle",(long)B2.carve_by_key("alice:1"),2);
eqi("an unknown dot resolves to none",(long)B2.carve_by_key("carol:9"),0);
eqi("an empty dot resolves to none",(long)B2.carve_by_key(""),0);
ok ("redo by dot",B2.redo_carve_by_key("alice:2"));
ok ("x=20 is back",active_at(B2,20.0f));
ok ("undoing an unknown dot is refused, not silently applied",
!B2.undo_carve_by_key("carol:9"));
// Replay is not once-only, so applying the same carve record twice must not
// stack a second carve — the same reasoning add_face and add_brush carry.
const int n=(int)B2.carves().size();
B2.apply_carve(sphere(10.0f,0.6f),"alice:1");
eqi("re-applying a dot adds no carve",(long)B2.carves().size(),n);
printf("\n%s\n",fails?"FAILED":"PASS");
return fails?1:0;
}