feat(core): add compact durable world storage and verified CLI

This commit is contained in:
Emil
2026-09-14 16:32:48 +03:00
parent ddfcef2e1d
commit cf8d1aae2e
29 changed files with 4392 additions and 20 deletions
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "shacraft-tools"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
serde.workspace = true
serde_json.workspace = true
shacraft-core.workspace = true
+305
View File
@@ -0,0 +1,305 @@
//! Local storage probe. JSON-lines session is a test/CLI interface, not MCP.
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use serde::Deserialize;
use serde_json::{Value, json};
use shacraft_core::{BlockChange, Pos, WorldStore};
use std::{
io::{self, BufRead, Write},
path::PathBuf,
time::Instant,
};
#[derive(Parser)]
#[command(version, about = "Shacraft Core storage tools (stage 1)")]
struct Args {
#[arg(long, default_value = "data")]
data: PathBuf,
#[arg(long, default_value_t = 64)]
cache: usize,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Read JSON requests, one per line. Flush each reply after the API returns.
Session,
/// Show storage metrics; estimated payload and OS RSS remain separate.
Stats,
/// Exercise shared snapshots, independent edits, undo and reset.
Demo,
/// Reproducible storage workload. Requires an empty world store.
Benchmark {
#[arg(long, default_value_t = 256)]
sections: usize,
#[arg(long, default_value_t = 100)]
worlds: usize,
},
}
#[derive(Deserialize)]
#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
enum Request {
List,
Register {
state: String,
},
Registry,
Create {
name: String,
template: Option<String>,
},
Revision {
world: String,
},
Get {
world: String,
pos: Pos,
},
Read {
world: String,
min: Pos,
max: Pos,
},
Edit {
world: String,
expected_revision: u64,
operation_id: String,
changes: Vec<BlockChange>,
},
Undo {
world: String,
expected_revision: u64,
operation_id: String,
target_operation: String,
},
Reset {
world: String,
expected_revision: u64,
operation_id: String,
},
Stats,
Flush,
}
fn execute(store: &mut WorldStore, request: Request) -> Result<Value> {
Ok(match request {
Request::List => json!(store.list_worlds()?),
Request::Register { state } => json!(store.register_block(&state)?),
Request::Registry => json!(store.registry()),
Request::Create { name, template } => {
store.create_world(&name, template.as_deref())?;
json!({"name":name, "revision":store.revision(&name)?})
}
Request::Revision { world } => json!(store.revision(&world)?),
Request::Get { world, pos } => json!(store.get_block(&world, pos)?),
Request::Read { world, min, max } => json!(store.read_region(&world, min, max)?),
Request::Edit {
world,
expected_revision,
operation_id,
changes,
} => json!(store.edit(&world, expected_revision, &operation_id, changes)?),
Request::Undo {
world,
expected_revision,
operation_id,
target_operation,
} => json!(store.undo(&world, expected_revision, &operation_id, &target_operation)?),
Request::Reset {
world,
expected_revision,
operation_id,
} => json!(store.reset_world(&world, expected_revision, &operation_id)?),
Request::Stats => metrics(store),
Request::Flush => {
store.flush()?;
json!({"flushed":true})
}
})
}
fn os_memory() -> Value {
// Linux measurements. Unavailable on other platforms is null, never zero.
let read = std::fs::read_to_string("/proc/self/status").ok();
let field = |key: &str| -> Option<u64> {
read.as_ref()?
.lines()
.find(|line| line.starts_with(key))?
.split_whitespace()
.nth(1)?
.parse::<u64>()
.ok()?
.checked_mul(1024)
};
json!({"rss_bytes":field("VmRSS:"), "peak_rss_bytes":field("VmHWM:"),
"source":"/proc/self/status (null when unavailable)"})
}
fn metrics(store: &WorldStore) -> Value {
json!({"storage":store.stats(), "process":os_memory()})
}
fn session(store: &mut WorldStore) -> Result<()> {
let stdin = io::stdin();
let mut input = stdin.lock();
let stdout = io::stdout();
let mut output = stdout.lock();
const MAX_LINE: usize = 8 * 1024 * 1024;
loop {
let mut line = Vec::new();
loop {
let available = input.fill_buf()?;
if available.is_empty() {
break;
}
let n = available
.iter()
.position(|&c| c == b'\n')
.map_or(available.len(), |n| n + 1);
if line.len().saturating_add(n) > MAX_LINE {
bail!("request exceeds 8 MiB");
}
line.extend_from_slice(&available[..n]);
input.consume(n);
if line.last() == Some(&b'\n') {
break;
}
}
if line.is_empty() {
break;
}
let result = serde_json::from_slice::<Request>(&line)
.context("invalid request")
.and_then(|request| execute(store, request));
let reply = match result {
Ok(result) => json!({"ok":true, "result":result}),
Err(error) => json!({"ok":false, "error":format!("{error:#}")}),
};
serde_json::to_writer(&mut output, &reply)?;
writeln!(output)?;
output.flush()?;
}
Ok(())
}
fn require_empty(store: &WorldStore) -> Result<()> {
if !store.list_worlds()?.is_empty() {
bail!("demo/benchmark requires an empty store; use a fresh --data directory");
}
Ok(())
}
fn demo(store: &mut WorldStore) -> Result<Value> {
require_empty(store)?;
let stone = store.register_block("shacraft:stone")?;
store.create_world("template", None)?;
let changes = (-16..16)
.flat_map(|x| {
(-16..16).map(move |z| BlockChange {
pos: [x, 0, z],
block: stone,
})
})
.collect();
store.edit("template", 0, "build-floor", changes)?;
store.create_world("arena_a", Some("template"))?;
store.create_world("arena_b", Some("template"))?;
let edited = store.edit(
"arena_a",
0,
"break-floor",
vec![BlockChange {
pos: [0, 0, 0],
block: 0,
}],
)?;
anyhow::ensure!(
store.get_block("arena_b", [0, 0, 0])? == stone,
"fork isolation failed"
);
let undo = store.undo("arena_a", edited.revision, "restore-floor", "break-floor")?;
let reset = store.reset_world("arena_a", undo.revision, "reset-match")?;
store.flush()?;
Ok(
json!({"scenario":"shared floor -> isolated edit -> undo -> reset", "worlds":store.list_worlds()?,
"edit":edited,"undo":undo,"reset":reset,"metrics":metrics(store)}),
)
}
fn benchmark(store: &mut WorldStore, sections: usize, worlds: usize) -> Result<Value> {
require_empty(store)?;
anyhow::ensure!((1..=4096).contains(&sections), "sections must be 1..4096");
anyhow::ensure!((1..=1000).contains(&worlds), "worlds must be 1..1000");
let start = Instant::now();
let stone = store.register_block("shacraft:stone")?;
store.create_world("template", None)?;
// Each section differs, so this probes eviction as well as content sharing.
for section in 0..sections {
let sx = (section % 64) as i32 * 16;
let sz = (section / 64) as i32 * 16;
let changes = (0..256)
.map(|cell| BlockChange {
pos: [sx + cell % 16, 0, sz + cell / 16],
block: if cell as usize == section % 256 {
0
} else {
stone
},
})
.collect();
store.edit(
"template",
store.revision("template")?,
&format!("seed-{section}"),
changes,
)?;
}
let seeded_ms = start.elapsed().as_millis();
let after_template = metrics(store);
let fork_start = Instant::now();
for world in 0..worlds {
store.create_world(&format!("arena_{world}"), Some("template"))?;
}
let fork_ms = fork_start.elapsed().as_millis();
let after_forks = metrics(store);
for section in 0..sections {
let pos = [(section % 64) as i32 * 16, 0, (section / 64) as i32 * 16];
store.get_block("template", pos)?;
}
for world in 0..worlds {
let name = format!("arena_{world}");
store.edit(
&name,
0,
"isolated",
vec![BlockChange {
pos: [1, 1, 1],
block: stone,
}],
)?;
store.reset_world(&name, 1, "reset")?;
anyhow::ensure!(store.get_block(&name, [1, 1, 1])? == 0, "reset failed");
}
store.flush()?;
Ok(
json!({"scenario":"storage-only; no players, rendering or tick simulation",
"comparison":"no Paper baseline measured", "sections":sections, "forked_worlds":worlds,
"seed_ms":seeded_ms, "fork_ms":fork_ms, "elapsed_ms":start.elapsed().as_millis(),
"after_template":after_template,"after_forks":after_forks,"after_access_edit_reset":metrics(store)}),
)
}
fn main() -> Result<()> {
let args = Args::parse();
let mut store = WorldStore::open(&args.data, args.cache)?;
let result = match args.command {
Command::Session => return session(&mut store),
Command::Stats => metrics(&store),
Command::Demo => demo(&mut store)?,
Command::Benchmark { sections, worlds } => benchmark(&mut store, sections, worlds)?,
};
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}