56 lines
8.3 KiB
Markdown
56 lines
8.3 KiB
Markdown
# 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 1–4096. 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.
|