mod world; use std::io::Read; use std::sync::mpsc::{self, Sender}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use tiny_http::{Header, Method, Response, Server}; use world::{DEFAULT_SCRIPT, World}; const INDEX: &str = include_str!("index.html"); 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"); 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(); thread::spawn(move || { 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"), } } _ => (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), ); }); } }