Files

56 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.