mod world; use std::io::Read; use std::path::Path; use std::sync::mpsc::{self, Sender}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use tiny_http::{Header, Method, Request, Response, Server}; use world::{DEFAULT_SCRIPT, World}; // Minimal shell; the whole UI is the Leptos app in ui/ compiled to wasm. const INDEX: &str = r#" World Simulation "#; fn serve_static(req: Request, dir: &Path, rel: &str) { let rel = rel.split('?').next().unwrap_or(""); let safe = !rel.is_empty() && rel .split('/') .all(|c| !c.is_empty() && c != ".." && c != "." && !c.contains('\\')); let ctype = match rel.rsplit('.').next() { Some("js") => "application/javascript", Some("wasm") => "application/wasm", Some("css") => "text/css", _ => "application/octet-stream", }; match safe .then(|| std::fs::read(dir.join(rel))) .and_then(Result::ok) { Some(bytes) => { let h = Header::from_bytes("Content-Type", ctype).unwrap(); let _ = req.respond(Response::from_data(bytes).with_header(h)); } None => { let _ = req.respond( Response::from_string("not found (build the UI: see ui/build.sh)") .with_status_code(404), ); } } } enum Msg { Edit(String), Script(String, Sender>), } 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()); // 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::(); { let (terrain, state, script) = ( Arc::clone(&terrain), Arc::clone(&state), Arc::clone(&script), ); thread::spawn(move || { let mut world = World::new(seed); let mut rev = u32::MAX; loop { 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(); thread::sleep(Duration::from_millis(100)); } }); } let server = Server::http(&addr).expect("failed to bind"); let ui_dir = Arc::new(std::path::PathBuf::from( std::env::var("SIM_UI_DIR").unwrap_or_else(|_| "ui/pkg".into()), )); println!("Simulation running at http://{addr}"); for mut req in server.incoming_requests() { let (terrain, state, script) = ( Arc::clone(&terrain), Arc::clone(&state), Arc::clone(&script), ); let tx = tx.clone(); let ui_dir = Arc::clone(&ui_dir); thread::spawn(move || { let post = *req.method() == Method::Post; if !post { if let Some(rel) = req.url().strip_prefix("/pkg/").map(str::to_owned) { return serve_static(req, &ui_dir, &rel); } } 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"), } } _ => (404, "not found".to_string(), "text/plain"), }; let header = Header::from_bytes("Content-Type", ctype).unwrap(); let _ = req.respond( Response::from_string(body) .with_status_code(status) .with_header(header), ); }); } }