sim/src/main.rs

116 lines
4.3 KiB
Rust
Raw Normal View History

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<Result<(), String>>),
}
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::<Msg>();
{
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),
);
});
}
}