Files

8.3 KiB
Raw Permalink Blame History

Implementation contract, v1

Stage 1 core Rust API (public in shacraft_core): type Pos = [i32;3]; type BlockId = u32; WorldStore::open(path: impl AsRef<Path>, cache_sections: usize) -> anyhow::Result<Self> list_worlds(&self) -> Result<Vec<WorldInfo>>; create_world(&mut self, name: &str, template: Option<&str>) -> Result<()>; reset_world(&mut self, name: &str, expected_revision: u64, operation_id: &str) -> Result<EditResult>. register_block(&mut self, canonical_state: &str) -> Result<BlockId>; registry(&self) -> &[String] (air is 0). get_block(&mut self, world: &str, pos: Pos) -> Result<BlockId>; revision(&self, world: &str) -> Result<u64>. edit(&mut self, world: &str, expected_revision: u64, operation_id: &str, changes: Vec<BlockChange>) -> Result<EditResult>. undo(&mut self, world: &str, expected_revision: u64, operation_id: &str, target_operation: &str) -> Result<EditResult>. read_region(&mut self, world: &str, min: Pos, max: Pos) -> Result<Vec<BlockChange>> returns nonair cells, bounds inclusive, capped at 262144 cells. stats(&self) -> serde_json::Value; flush(&mut self) -> Result<()>. collect_garbage(&mut self, max_blobs: usize) -> Result<usize> deletes at most 1..4096 unreachable blobs per call, preserving snapshots and operation history. This limits deletions, not SQL scan latency, and does not vacuum the database file. BlockChange { pub pos: Pos, pub block: BlockId } derives Serialize/Deserialize/Clone. EditResult { pub revision: u64, pub changed: usize, pub replayed: bool } derives Serialize/Deserialize/Clone. WorldInfo { pub name: String, pub revision: u64, pub template: Option<String> } derives Serialize/Deserialize/Clone. All durable changes atomic/recoverable; bounded section cache and disk operation history; independent forks pin immutable templates. Core has no networking/rendering.

Stage 1 storage semantics (accepted before implementation):

  • SQLite transactions with WAL and synchronous=FULL are preferred to an untested bespoke durability journal. Use a bounded SQLite cache and a bounded decoded section LRU; report both. Reject a second writer using an exclusive advisory lock file. Reject cache_sections=0 explicitly.
  • create_world(template) snapshots the source's current effective contents using references to immutable section blobs. The source's subsequent edits/reset never alter the child. Snapshot metadata stays disk indexed; creating the first snapshot can cost O(number of stored section references), without expanding all blocks in RAM.
  • reset returns the instance to its pinned base (or air for worlds without a base), increments revision monotonically, and has expected_revision/idempotency checks just like edit. A reset is a barrier for undo of older operations.
  • Operation IDs scoped per world, bounded UTF-8 strings. Look up idempotency before comparing the current revision. Repeating identical payload returns the stored result with replayed=true; same ID with a different payload is an error. Results survive restart and resets.
  • Every new successful edit (including changed=0) increments the revision; an exact replay does not. This is an explicit initial API policy, and no-op edits are also undo barriers.
  • Transactions publish and are acknowledged only after durable commit. Registry additions are durable. All input validation precedes mutation. Revisions fit SQLite's positive signed 64-bit integer range; reject overflow.
  • Undo is atomic and conflict aware. It may conservatively require current revision==the target edit's resulting revision (safe initial implementation, explicitly documented); selective nonoverlapping undo is a later extension. Never overwrite later writes or ABA cycles silently. Undoing an undo/reset is unsupported initially.
  • Default transaction limit 32768 cells; read_region limit 262144 volume; inclusive bounds and checked arithmetic. Reject duplicate cell positions in one edit, unknown BlockId, nonexistent world, malformed name, negative/overflowing volume and excessive input before writing.
  • World names match [A-Za-z0-9_-]{1,64}; block canonical names and registry cardinality are bounded. Minimum supported coordinate domain [-30000000,30000000] for every axis with checked validation. Air ID 0 cannot be reassigned. registry returns owned canonical identifiers shared by worlds; bounds and its RAM footprint are reported.
  • Section encoding versioned and defensively decoded: uniform or minimal-width palette + packed indices. Deduplicate immutable blobs; references are stored in indexed SQL tables. GC of unreachable blobs is explicit and bounded; disk retention is documented separately from RAM accounting.
  • Diagnostics distinguish decoded payload estimates, cache entries/capacity, SQLite cache configuration, registry bytes, disk data and measured OS RSS. Do not call estimated payload size total server RAM.
  • Implemented cache stores immutable encoded sections (uniform/palette/dense); cache_payload_bytes sums encoded lengths including their headers, excluding allocator/map overhead. transient_decoded_payload_peak_estimate_bytes counts temporary section-array payload, not total transient allocation. SQLite page cache budget is 4 MiB (SQLite setting, not a hard process RSS cap).
  • Implemented cardinality limits: registry 262144 states with at most 512 ASCII bytes per canonical state; worlds 10000; operation IDs 1..128 UTF-8 bytes without control characters. Registry strings reside in RAM and are accounted separately. Operation history is disk indexed and retained indefinitely in this initial release; explicit GC does not remove it.

Planned Stage 2 server protocol (JSON, WebSocket /ws; not implemented in Stage 1): Client first: {type:'join',name:'Player',world:'lobby',manifest_hash:'…'}. Server welcome: {type:'welcome',id,world,revision,registry:[canonical block states],blocks:[{pos:[x,y,z],block:id}],players:[],spawn:[x,y,z],manifest_hash}. Client inputs at <=30Hz: {type:'input',seq,yaw,pitch,forward,strafe,jump}; yaw/pitch radians; forward/strafe in [-1,1]. Coordinates: +Y up; yaw=0 looks toward -Z; +X right. Position is feet. Client actions: {type:'break',pos:[x,y,z]} or {type:'place',pos:[x,y,z],block:id}; {type:'switch_world',world}; {type:'chat',text}. Server tick 20Hz: {type:'state',players:[{id,name,position:[x,y,z],yaw,pitch}],tick,ack:seq,match:{phase,remaining,winner}}. Server edit: {type:'blocks',revision,changes:[{pos,block}]}. Server error: {type:'error',message}; chat {type:'chat',name,text}. Client fetch GET /api/manifest -> {protocol:1,hash,packages:[],...}; GET /api/worlds -> [{name,revision,template}]; GET /api/catalog -> [{id,state,color,solid,shape}]. Server serves client from / . Control HTTP: GET /api/health, /api/metrics, /api/worlds, /api/catalog public; POST /api/control with Authorization: Bearer token and {method,params}, JSON return {result:...} or {error:'...'}. Control methods: world.list, world.create {name,template?}, world.reset {world,expected_revision,operation_id}, world.read {world,min,max}, world.edit {world,expected_revision,operation_id,changes}, world.undo {world,expected_revision,operation_id,target_operation}, catalog.search {query,limit}, metrics, arena.start {world}, camera.capture {world,min?,max?} (top-down PNG), entity.spawn {world,kind,position}, entity.list {world}. Default server host 127.0.0.1 port 4000, token file data/control.token (0600). No publishing or production access.

Integrated MVP components

The gameplay protocol and Control API boundaries are documented in SERVER.md; the stdio MCP implementation in MCP.md; packages and WASM in PACKAGES.md; the full catalog in CONTENT.md; and Anvil/Sponge and typed sidecars in interop.md. These documents extend the core contract above without changing its durability rules.

WorldStore::section_positions(world, after: Option<Pos>, limit) returns paginated effective coordinates of 16³ sections, including inherited sections and air overrides, ordered by x/y/z. The limit is 14096. Export holds the single-writer lock and does not change the world between pages.

Core history is stored on disk. Entity/rule metadata has a separate transaction domain and revision. Preview plans live for 5 minutes and do not survive a restart; an accepted edit follows the WorldStore durability contract. A failed metadata commit does not publish changed state. If an active match is interrupted, its arena resets to the pinned base on the next startup.