2026-10-07 16:04:37 +00:00
|
|
|
mod world;
|
|
|
|
|
|
2026-10-07 16:42:59 +00:00
|
|
|
use std::io::Read;
|
|
|
|
|
use std::sync::mpsc::{self, Sender};
|
2026-10-07 16:04:37 +00:00
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
use std::thread;
|
|
|
|
|
use std::time::Duration;
|
2026-10-07 16:42:59 +00:00
|
|
|
use tiny_http::{Header, Method, Response, Server};
|
|
|
|
|
use world::{DEFAULT_SCRIPT, World};
|
2026-10-07 16:04:37 +00:00
|
|
|
|
|
|
|
|
const INDEX: &str = include_str!("index.html");
|
|
|
|
|
|
2026-10-07 16:42:59 +00:00
|
|
|
enum Msg {
|
|
|
|
|
Edit(String),
|
|
|
|
|
Script(String, Sender<Result<(), String>>),
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:04:37 +00:00
|
|
|
fn main() {
|
|
|
|
|
let seed = std::env::args()
|
|
|
|
|
.nth(1)
|
|
|
|
|
.and_then(|s| s.parse().ok())
|
|
|
|
|
.unwrap_or(42u64);
|
|
|
|
|
let addr = std::env::var("SIM_ADDR").unwrap_or_else(|_| "127.0.0.1:7878".into());
|
2026-10-07 16:42:59 +00:00
|
|
|
// The Rune VM is not Send, so the world lives on the sim thread and publishes JSON snapshots.
|
|
|
|
|
let terrain = Arc::new(Mutex::new(String::new()));
|
|
|
|
|
let state = Arc::new(Mutex::new(String::new()));
|
|
|
|
|
let script = Arc::new(Mutex::new(DEFAULT_SCRIPT.to_string()));
|
|
|
|
|
let (tx, rx) = mpsc::channel::<Msg>();
|
2026-10-07 16:04:37 +00:00
|
|
|
{
|
2026-10-07 16:42:59 +00:00
|
|
|
let (terrain, state, script) = (
|
|
|
|
|
Arc::clone(&terrain),
|
|
|
|
|
Arc::clone(&state),
|
|
|
|
|
Arc::clone(&script),
|
|
|
|
|
);
|
2026-10-07 16:04:37 +00:00
|
|
|
thread::spawn(move || {
|
2026-10-07 16:42:59 +00:00
|
|
|
let mut world = World::new(seed);
|
|
|
|
|
let mut rev = u32::MAX;
|
2026-10-07 16:04:37 +00:00
|
|
|
loop {
|
2026-10-07 16:42:59 +00:00
|
|
|
while let Ok(msg) = rx.try_recv() {
|
|
|
|
|
match msg {
|
|
|
|
|
Msg::Edit(cmd) => world.apply(&cmd),
|
|
|
|
|
Msg::Script(src, reply) => {
|
|
|
|
|
let r = world.set_script(&src);
|
|
|
|
|
if r.is_ok() {
|
|
|
|
|
*script.lock().unwrap() = src;
|
|
|
|
|
}
|
|
|
|
|
let _ = reply.send(r);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !world.paused {
|
|
|
|
|
for _ in 0..world.speed {
|
|
|
|
|
world.step();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if world.terrain_rev != rev {
|
|
|
|
|
rev = world.terrain_rev;
|
|
|
|
|
*terrain.lock().unwrap() = world.terrain_json();
|
|
|
|
|
}
|
|
|
|
|
*state.lock().unwrap() = world.state_json();
|
2026-10-07 16:04:37 +00:00
|
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let server = Server::http(&addr).expect("failed to bind");
|
|
|
|
|
println!("Simulation running at http://{addr}");
|
2026-10-07 16:42:59 +00:00
|
|
|
for mut req in server.incoming_requests() {
|
|
|
|
|
let (terrain, state, script) = (
|
|
|
|
|
Arc::clone(&terrain),
|
|
|
|
|
Arc::clone(&state),
|
|
|
|
|
Arc::clone(&script),
|
|
|
|
|
);
|
|
|
|
|
let tx = tx.clone();
|
2026-10-07 16:04:37 +00:00
|
|
|
thread::spawn(move || {
|
2026-10-07 16:42:59 +00:00
|
|
|
let post = *req.method() == Method::Post;
|
|
|
|
|
let mut input = String::new();
|
|
|
|
|
if post {
|
|
|
|
|
let _ = req.as_reader().take(65536).read_to_string(&mut input);
|
|
|
|
|
}
|
|
|
|
|
let (status, body, ctype) = match (post, req.url()) {
|
|
|
|
|
(false, "/") => (200, INDEX.to_string(), "text/html; charset=utf-8"),
|
|
|
|
|
(false, "/api/terrain") => {
|
|
|
|
|
(200, terrain.lock().unwrap().clone(), "application/json")
|
|
|
|
|
}
|
|
|
|
|
(false, "/api/state") => (200, state.lock().unwrap().clone(), "application/json"),
|
|
|
|
|
(false, "/api/script") => (
|
|
|
|
|
200,
|
|
|
|
|
script.lock().unwrap().clone(),
|
|
|
|
|
"text/plain; charset=utf-8",
|
|
|
|
|
),
|
|
|
|
|
(true, "/api/edit") if input.len() <= 256 => {
|
|
|
|
|
let _ = tx.send(Msg::Edit(input));
|
|
|
|
|
(200, "ok".to_string(), "text/plain")
|
|
|
|
|
}
|
|
|
|
|
(true, "/api/script") => {
|
|
|
|
|
let (rtx, rrx) = mpsc::channel();
|
|
|
|
|
let _ = tx.send(Msg::Script(input, rtx));
|
|
|
|
|
match rrx.recv_timeout(Duration::from_secs(5)) {
|
|
|
|
|
Ok(Ok(())) => (200, "ok".to_string(), "text/plain"),
|
|
|
|
|
Ok(Err(e)) => (400, e, "text/plain; charset=utf-8"),
|
|
|
|
|
Err(_) => (503, "timed out".to_string(), "text/plain"),
|
|
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
2026-10-07 16:42:59 +00:00
|
|
|
_ => (404, "not found".to_string(), "text/plain"),
|
2026-10-07 16:04:37 +00:00
|
|
|
};
|
|
|
|
|
let header = Header::from_bytes("Content-Type", ctype).unwrap();
|
2026-10-07 16:42:59 +00:00
|
|
|
let _ = req.respond(
|
|
|
|
|
Response::from_string(body)
|
|
|
|
|
.with_status_code(status)
|
|
|
|
|
.with_header(header),
|
|
|
|
|
);
|
2026-10-07 16:04:37 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|