OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / tests / test_carve_identity.cpp

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