#ifndef DC_LEVEL_JOURNAL_H
#define DC_LEVEL_JOURNAL_H
// ---------------------------------------------------------------------------
// dc_journal — THE JOURNAL IS THE STATE; DCBrushStore IS A DERIVED CACHE.
//
// hyper-games #22 §5 items 2 and 5. Everything a peer can do to a level is a
// RECORD; the level is what you get by folding the live records over the base:
//
// brush set = base(imported .MAP) (+) fold(live records, by (seq, author))
//
// UNDO IS NOT AN INVERSE OP. It is `undone: true` on a record, plus a re-fold.
// The previous scheme stored the plane it replaced (`prev_n`/`prev_d`) and
// re-applied that, which is only correct for a linear history of ONE plane: nudge
// a face twice, undo the first, and record 1's `prev` overwrites record 2's value
// — the plane lands where neither op asked. A fold cannot have that bug, because
// there is nothing to invert.
//
// AND IT IS SHARED. dc_level.h deliberately pushed shape-edit undo onto the
// caller, which is affordable with one UI and not with two: an in-game interface
// and blender-bridge would each build a private stack over the same journal, which
// is the first thing that breaks. The fold lives HERE, in engine-free libdc, so
// every consumer inherits the same undo rather than reimplementing it.
//
// ORDER — the conflict rule level-mesh-implementation.md §9a left undesigned.
// The durable view is last-write-wins per key, NOT an ordered op stream, so
// records arrive in any order and no arrival order can be trusted. (seq, author)
// is a total order every peer computes identically: seq is unix-seeded so it
// tracks causality loosely, and the author dot breaks ties deterministically. Two
// peers editing one face therefore converge on the later (seq, author).
//
// NAMING, one rule: A RECORD'S OWN DOT NAMES WHATEVER IT CREATES; its payload
// keys name what it TARGETS. So `brush.create` makes a brush called
// "<author>:<seq>", `face.add` gives the new face that same dot, `carve` names the
// carve, and `brush.split` names the back half it cuts off — while `face.set`,
// `face.remove`, `brush.remove` and `brush.translate` carry `brush_key`/`face_key`
// to say what they act on. Nothing needs a separate
// id field, because `world:level:<author>:op:<seq>` already IS the dot.
//
// A record whose target is gone is SKIPPED, never guessed at. A face that was
// removed elsewhere in the journal resolves to nothing, and applying the edit to
// whichever face inherited its index is exactly the silent wrong-geometry failure
// dots exist to prevent.
// ---------------------------------------------------------------------------
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
#include "dc_brush_store.h"
#include "dc_edit.h"
namespace dc_journal {
enum class Kind {
Unknown = 0,
BrushCreate, // planes/proj/material_id/level_id/sky; named by the record's dot
BrushRemove, // brush_key -> tombstone
BrushSplit, // brush_key + plane; the BACK half is named by the record's dot
BrushTranslate, // brush_key + dx,dy,dz
// brush_key + a diagonal scale and the pivot it is about. Its own kind
// because it is the only edit that changes a brush's SIZE, and because a
// scale is not expressible as a translation of planes — see
// DCBrushStore::scale_brush.
BrushScale,
FaceSet, // brush_key + face_key + plane
FaceAdd, // brush_key + plane + face_proj; the new face takes the dot
FaceRemove, // brush_key + face_key
Carve, // carve shape; named by the record's dot
// TRUE VERTEX / EDGE DRAG. brush_key + the corner POSITIONS being moved + a
// delta. Its own kind because it is the one edit that re-derives the plane set
// instead of moving planes — everything else here keeps face identity by
// construction, and this one has to re-match it. Positions rather than indices
// because a vertex has no stable index: it is derived, and the derivation order
// changes the moment the plane set does.
BrushReshape,
// THE CSG SET. Each carries its OPERAND, never a brush id — ids come from a local
// counter, so a record naming one replays against different geometry on another
// peer. Subtract and intersect carry the operand's PLANE SET outright, which also
// makes them independent of a cutter brush that the operation itself consumes.
// Merge is the exception and has to be: it is defined over brushes, and which one
// SURVIVES is part of the meaning, so it carries durable brush KEYS in order.
BrushSubtract, // brush_key + planes (the cutter); pieces named from the dot
BrushIntersect, // brush_key + planes (the other solid)
BrushMerge, // brush_keys, in order; the first survives
};
struct Record {
// Identity — the two halves of the durable key world:level:<author>:op:<seq>.
std::string author;
uint64_t seq = 0;
Kind kind = Kind::Unknown;
// A TOMBSTONE VALUE, never an erase. GameState.erase_state has no durable path
// (it clears the local mirrors and never sends), so an erased record would
// simply come back on the next room load. Undo therefore has to be a value.
bool undone = false;
// Targets.
std::string brush_key;
std::string face_key;
// Payloads (only the ones this kind needs are read).
std::vector<ConvexPlane> planes;
std::vector<uint32_t> proj;
uint32_t material_id = 0;
uint32_t level_id = 0;
bool sky = false;
// Air the level owns rather than geometry (DCBrush::clearance). Journalled
// like any other property of a create, because a clearance volume has to
// replicate and undo with the shape it belongs to — a peer that replayed the
// stairs without their headroom would build a flight buried in hillside.
bool clearance = false;
// The operator invocation this brush belongs to. Journalled with the create
// because the grouping IS part of what was created — a peer that replayed an
// arch as nine unrelated brushes would be able to take it apart in ways the
// author could not.
std::string group;
ConvexPlane plane{0.0f, 0.0f, 0.0f, 0.0f};
uint32_t face_proj = 0;
float dx = 0.0f, dy = 0.0f, dz = 0.0f;
// BrushScale: the diagonal factor and the pivot it is taken about. The pivot
// is carried rather than derived, because "the group's centre" is a function
// of the whole selection and a replaying peer must scale about the SAME point
// the author did or the pieces land apart.
float sx = 1.0f, sy = 1.0f, sz = 1.0f;
float px = 0.0f, py = 0.0f, pz = 0.0f;
TerrainEdit carve;
// BrushReshape: the corners being dragged, in the state the fold has reached by
// the time this record replays. Sound because the fold is ordered — every record
// before this one has already been applied, so the geometry these name is the
// geometry that was there when it was authored.
std::vector<CSG4Vec3> points;
// BrushMerge: the brushes being merged, in order, by durable key. The first is
// the survivor — it keeps its id and its name — so the ORDER is load-bearing and
// not a set.
std::vector<std::string> brush_keys;
std::string dot() const { return author + ":" + std::to_string(seq); }
};
struct FoldStats {
uint32_t applied = 0; // records that changed the store
uint32_t skipped = 0; // live records whose target is gone / were refused
uint32_t undone = 0; // tombstoned records, deliberately not applied
};
// The total order. seq first (it approximates causality), author to break ties.
inline bool before(const Record& a, const Record& b) {
if (a.seq != b.seq) return a.seq < b.seq;
return a.author < b.author;
}
// Apply ONE record. False when it could not be applied — an unknown brush, a face
// that is gone, or a store refusal (an edit that would empty the brush). A refusal
// is information: it means this peer's geometry and the journal disagree.
inline bool apply(const Record& r, DCBrushStore& store) {
switch (r.kind) {
case Kind::BrushCreate: {
// Already known: this is a re-fold, so the brush is sitting tombstoned at
// its as-created plane set and only needs bringing back. Idempotent either
// way — replay is not once-only.
const uint32_t known = store.brush_by_key(r.dot());
if (known) { store.restore_brush(known); return true; }
return store.add_brush(r.planes, r.proj, r.material_id, r.level_id,
r.sky, r.dot(), r.clearance, r.group) != 0u;
}
case Kind::BrushRemove: {
const uint32_t id = store.brush_by_key(r.brush_key);
return id && store.remove_brush(id);
}
case Kind::BrushSplit: {
// The target keeps the front half; the back half is a NEW brush named by
// this record's dot, exactly like a create. split_brush is idempotent and
// restores both halves after a rewind, so replay needs no special case here.
const uint32_t id = store.brush_by_key(r.brush_key);
if (!id) return false;
return store.split_brush(id, r.plane, r.dot()) != 0u;
}
case Kind::BrushScale: {
const uint32_t id = store.brush_by_key(r.brush_key);
return id && store.scale_brush(id, r.sx, r.sy, r.sz, r.px, r.py, r.pz);
}
case Kind::BrushTranslate: {
const uint32_t id = store.brush_by_key(r.brush_key);
return id && store.translate_brush(id, r.dx, r.dy, r.dz);
}
case Kind::FaceSet: {
const uint32_t id = store.brush_by_key(r.brush_key);
if (!id) return false;
const int f = store.face_by_key(id, r.face_key);
if (f < 0) return false;
return store.set_face_plane(id, (uint32_t)f, r.plane);
}
case Kind::FaceAdd: {
const uint32_t id = store.brush_by_key(r.brush_key);
if (!id) return false;
return store.add_face(id, r.plane, r.face_proj, r.dot()) != UINT32_MAX;
}
case Kind::FaceRemove: {
const uint32_t id = store.brush_by_key(r.brush_key);
if (!id) return false;
const int f = store.face_by_key(id, r.face_key);
if (f < 0) return false;
return store.remove_face(id, (uint32_t)f);
}
case Kind::Carve:
return store.apply_carve(r.carve, r.dot()) != 0u;
case Kind::BrushSubtract: {
const uint32_t id = store.brush_by_key(r.brush_key);
if (!id || r.planes.empty()) return false;
std::vector<uint32_t> pieces;
// >= 0 is applied: 0 means the cutter swallowed the brush, which IS the edit.
// -1 is a refusal — the cutter missed — and that is a skip, not a failure.
return store.subtract_brush(id, r.planes, r.dot(), pieces) >= 0;
}
case Kind::BrushIntersect: {
const uint32_t id = store.brush_by_key(r.brush_key);
if (!id || r.planes.empty()) return false;
return store.intersect_brush(id, r.planes, r.dot());
}
case Kind::BrushMerge: {
std::vector<uint32_t> ids;
for (const std::string& k : r.brush_keys) {
const uint32_t id = store.brush_by_key(k);
if (!id) return false; // a merged brush is gone: skip, never guess
ids.push_back(id);
}
return store.merge_brushes(ids, r.dot());
}
case Kind::BrushReshape: {
const uint32_t id = store.brush_by_key(r.brush_key);
if (!id) return false;
return store.move_brush_points(id, r.points, {r.dx, r.dy, r.dz}, r.dot());
}
case Kind::Unknown:
default:
return false;
}
}
// Rewind to the base, then apply every live record in (seq, author) order.
//
// Takes the vector BY VALUE because it sorts it: the caller's arrival order is
// meaningless and must not be mistaken for the fold's order.
inline FoldStats fold(std::vector<Record> records, DCBrushStore& store) {
std::stable_sort(records.begin(), records.end(), before);
store.reset_to_base();
FoldStats st;
for (const Record& r : records) {
if (r.undone) { st.undone++; continue; }
if (apply(r, store)) st.applied++;
else st.skipped++;
}
return st;
}
} // namespace dc_journal
#endif // DC_LEVEL_JOURNAL_H