#![feature(exit_status_error)] use colored::Colorize; use notify::{Event, EventKind, RecursiveMode, Watcher}; use std::env::{args, var}; use std::fs; use std::io::{stdout, Cursor, Write}; use std::path::{Path, PathBuf}; use std::process::{exit, Command}; use std::sync::mpsc; use std::sync::mpsc::TryRecvError; use std::thread::sleep; use std::time::{Duration, Instant}; use tiny_http::{Response, Server, StatusCode}; use wasm_pack::command::build::{BuildOptions, Target}; use wasm_pack::command::run_wasm_pack; use wasm_pack::progressbar::LogLevel; fn workspace_dir() -> PathBuf { let output = std::process::Command::new(env!("CARGO")) .arg("locate-project") .arg("--workspace") .arg("--message-format=plain") .output() .unwrap() .stdout; let cargo_path = Path::new(std::str::from_utf8(&output).unwrap().trim()); cargo_path.parent().unwrap().to_path_buf() } fn build_client() -> anyhow::Result<()> { let cli = wasm_pack::Cli { cmd: wasm_pack::command::Command::Build(BuildOptions { path: Some(workspace_dir().join("crates/client")), scope: None, mode: Default::default(), disable_dts: false, weak_refs: false, reference_types: false, target: Target::Web, debug: false, dev: false, release: false, profiling: false, out_dir: "pkg".to_string(), out_name: None, no_pack: false, no_opt: true, extra_options: vec![], }), verbosity: 0, quiet: true, log_level: LogLevel::Error, }; run_wasm_pack(cli.cmd) } fn try_build_client() -> bool { match build_client() { Ok(_) => { println!( "{} -- Client package built successfully", "✓ Success".green().bold() ); true } Err(e) => { eprintln!( "{} -- Client package failed to build: {}", "✗ Failed".red().bold(), e ); false } } } fn start_server() { let bind = var("BIND").unwrap_or("[::]:8000".to_string()); let server = Server::http(&bind).unwrap(); println!("{} on {bind}", "✓ Listening".magenta().bold()); for req in server.incoming_requests() { let mut path = Path::new(req.url()); if path == Path::new("/") { path = Path::new("/index.html"); } let path = path.strip_prefix(Path::new("/")).unwrap(); let full_path = workspace_dir().join("crates/unified").join(path); let content = match fs::read(full_path.clone()) { Ok(r) => r, Err(_) => { let _ = req.respond(Response::new( StatusCode::from(404), vec![], Cursor::new(vec![]), None, None, )); continue; } }; let len = content.len(); let header = tiny_http::Header::from_bytes( &b"Content-Type"[..], if full_path.to_str().unwrap().ends_with(".html") { &b"text/html"[..] } else if full_path.to_str().unwrap().ends_with(".js") { &b"text/javascript"[..] } else if full_path.to_str().unwrap().ends_with(".wasm") { &b"application/wasm"[..] } else { &b"application/octet-stream"[..] }, ) .unwrap(); let response = Response::new( StatusCode::from(200), vec![header], Cursor::new(content), Some(len), None, ); let _ = req.respond(response); } } fn main() { let mut args = args(); let subcommand = args.nth(1).unwrap(); match subcommand.as_str() { "hp:unified:native" => { let asset_dir = workspace_dir().join("crates/unified/"); Command::new("dx") .args([ "serve", "--package", "starkingdoms", "--hot-patch", "--features", "bevy/hotpatching", "--features", "native", "--features", "bevy/dynamic_linking", "--args", &args.collect::>().join(" ") ]) .env("BEVY_ASSET_ROOT", asset_dir.to_string_lossy().to_string()) .env("RUST_LOG", "info,starkingdoms=trace") .spawn() .unwrap() .wait() .unwrap(); }, "client" => { if !try_build_client() { exit(1); } } "watch" | "serve" => { let serve = subcommand == "serve"; if serve { std::thread::spawn(start_server); } //try_build_client(); let (tx, rx) = mpsc::channel::>(); // Use recommended_watcher() to automatically select the best implementation // for your platform. The `EventHandler` passed to this constructor can be a // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or // another type the trait is implemented for. let mut watcher = notify::recommended_watcher(tx).unwrap(); // Add a path to be watched. All files and directories at that path and // below will be monitored for changes. watcher .watch(&workspace_dir().join("crates"), RecursiveMode::Recursive) .unwrap(); println!("{}", "[Watch] 🛈 Watching for file changes".blue().bold()); // Block forever, printing out events as they come in let mut needs_rebuild = false; loop { let res = rx.try_recv(); let res = match res { Ok(r) => r, Err(TryRecvError::Empty) => { if needs_rebuild { // wait 1s then check again, then rebuild sleep(Duration::from_secs(1)); if let Ok(r) = rx.try_recv() { r } else { try_build_client(); needs_rebuild = false; rx.recv().unwrap() } } else { rx.recv().unwrap() } } Err(TryRecvError::Disconnected) => panic!("{:?}", TryRecvError::Disconnected), }; match res { Ok(event) => { if let EventKind::Modify(_) = event.kind { for path in &event.paths { if !path.to_str().unwrap().contains("client/pkg") && !path.to_str().unwrap().ends_with("~") { needs_rebuild = true; } } } } Err(e) => { eprintln!( "{} -- Error watching for files: {}", "[Watch] ✗ Error".red().bold(), e ); } } } } "asset:process" => { // png-ify textures let paths = fs::read_dir(workspace_dir().join("crates/unified/assets/vector_textures/")) .unwrap(); let output_dir = workspace_dir().join("crates/unified/assets/textures/"); for path in paths { let path = path.unwrap().path(); let Some(extension) = path.extension() else { continue; }; if extension != "svg" { continue; } let Some(filename) = path.file_stem() else { continue; }; print!("[{}] {}.svg", ">".blue(), filename.to_string_lossy()); stdout().flush().unwrap(); let start = Instant::now(); let tree = { let mut opt = resvg::usvg::Options { resources_dir: std::fs::canonicalize(&path) .ok() .and_then(|p| p.parent().map(|p| p.to_path_buf())), ..resvg::usvg::Options::default() }; opt.fontdb_mut().load_system_fonts(); let svg_data = std::fs::read(&path).unwrap(); resvg::usvg::Tree::from_data(&svg_data, &opt).unwrap() }; let pixmap_size = tree.size().to_int_size(); let mut pixmap = resvg::tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()) .unwrap(); resvg::render( &tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut(), ); pixmap .save_png(output_dir.join(format!("{}.png", filename.to_string_lossy()))) .unwrap(); println!( " {} {}.png {} {}", "-->".dimmed(), filename.to_string_lossy(), "✓ success".green(), format!("{}ms", start.elapsed().as_millis()).dimmed() ); } } "unified:web" => { let start = Instant::now(); let target = workspace_dir().join("target/"); let unified = workspace_dir().join("crates/unified/"); exec("cargo", "build --package starkingdoms --target wasm32-unknown-unknown -F wasm --no-default-features"); let wasm_file = target.join("wasm32-unknown-unknown/debug/starkingdoms.wasm"); exec( "wasm-bindgen", format!( "--target web --typescript --out-dir {} {}", unified.join("web/").display(), wasm_file.display() ) .as_str(), ); println!( "{} {} {}", "-->".dimmed(), "✓ done".green(), format!("{}ms", start.elapsed().as_millis()).dimmed() ); } "unified:web:release" => { let start = Instant::now(); let target = workspace_dir().join("target/"); let unified = workspace_dir().join("crates/unified/"); exec("cargo", "build --package starkingdoms --lib --target wasm32-unknown-unknown -F wasm --no-default-features --profile wasm-release"); let wasm_file = target.join("wasm32-unknown-unknown/wasm-release/starkingdoms.wasm"); exec( "wasm-bindgen", format!( "--target web --typescript --out-dir {} {}", unified.join("web/").display(), wasm_file.display() ) .as_str(), ); exec( "wasm-opt", format!( "-Oz -d {} -o {}", unified.join("web/starkingdoms_bg.wasm").display(), unified.join("web/starkingdoms_bg.wasm").display() ) .as_str(), ); println!( "{} {} {}", "-->".dimmed(), "✓ done".green(), format!("{}ms", start.elapsed().as_millis()).dimmed() ); } _ => panic!("unsupported command"), } } fn exec(program: &str, args: &str) { println!("[{}] {} {}", "+".blue(), program, args); Command::new(program) .args(args.split(" ").collect::>()) .spawn() .unwrap() .wait() .unwrap() .exit_ok().unwrap(); }