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),
);
});
}
}