#![feature(try_blocks)] //mod config; use clap::Parser; use crossbeam_channel::Sender; use log::{debug, error, info, warn}; use parking_lot::RwLock; use std::{collections::HashMap, io::Read, sync::Arc, time::SystemTime}; use tide::Request; fn get_cache_dir() -> String { let dir = directories::ProjectDirs::from("tk", "txmn", "minetest-tiler").map_or_else( String::new, |o| { o.cache_dir() .to_str() .map_or_else(String::new, String::from) }, ); if dir.ends_with('/') { dir } else { format!("{}/", dir) } } #[derive(Parser, Debug)] #[clap( author = "tuxmain (GNU AGPLv3)", about = "Map generator & server for MineTest" )] struct Args { /// Must start and end with `/` #[clap(short = 'b', long, default_value = "/")] base_url: String, /// Cache duration in seconds #[clap(short = 'c', long, default_value_t = 86400)] cache_age: u64, /// Address to listen to #[clap(short = 'l', long, default_value = "0.0.0.0:30800")] listen: String, /// Directory that will contain the map (should end with `/`) #[clap(short='o', long, default_value_t=get_cache_dir())] output_path: String, /// World directory #[clap( short = 'w', long, default_value = "/var/games/minetest-server/.minetest/world/world/" )] world_path: String, #[clap(short = 'z', long, default_value_t = 7)] zoom_default: i32, #[clap(short = 'm', long, default_value_t = 4)] zoom_min: i32, #[clap(short = 'M', long, default_value_t = 10)] zoom_max: i32, #[clap(short = 's', long, default_value_t = 256)] tile_size: usize, /// Path to minetestmapper executable #[clap(short = 'p', long, default_value = "minetestmapper")] minetestmapper_path: String, #[clap(last = true)] minetestmapper_args: Vec, } #[derive(Debug)] enum Error { Image(image::ImageError), Io(std::io::Error), Minetestmapper, } #[async_std::main] async fn main() -> tide::Result<()> { let config = Args::parse(); simplelog::TermLogger::init( simplelog::LevelFilter::Info, simplelog::Config::default(), simplelog::TerminalMode::Mixed, simplelog::ColorChoice::Auto, ) .unwrap(); info!("Output dir: {}", config.output_path); assert!( config.base_url.starts_with('/') && config.base_url.ends_with('/'), "`base_url` must start and end with `/`" ); let config = Arc::new(config); let tasks = Arc::new(RwLock::new(HashMap::new())); let mut app = tide::new(); app.at(&format!("{}:z/:x/:y", config.base_url)).get({ let config = config.clone(); let tasks = tasks.clone(); move |req| req_tile(req, tasks.clone(), config.clone()) }); app.listen(&config.listen).await?; Ok(()) } fn run_minetestmapper( z: i32, x: i32, y: i32, tile_dir: &str, tile_path: &str, config: Arc, ) -> Result<(), Error> { debug!("Generating tile ({},{},{})", z, x, y); std::fs::create_dir_all(tile_dir).map_err(|e| { error!("Generating tile ({},{},{}): {}", z, x, y, e); Error::Io(e) })?; std::process::Command::new(&config.minetestmapper_path) .args([ "-i", &config.world_path, "-o", tile_path, "--geometry", &format!( "{}:{}+{}+{}", x * config.tile_size as i32, -1 - y * config.tile_size as i32, config.tile_size, config.tile_size ), ]) .args(&config.minetestmapper_args) .output() .map_err(|e| { error!("Generating tile ({},{},{}): {}", z, x, y, e); Error::Minetestmapper })?; Ok(()) } fn read_tile_file(tile_path: &str) -> Result, Error> { let mut tile_content = Vec::new(); std::fs::File::open(&tile_path) .map_err(Error::Io)? .read_to_end(&mut tile_content) .map_err(Error::Io)?; // TODO resp_tile if error Ok(tile_content) } fn generate_tile( z: i32, x: i32, y: i32, tile_dir: &str, tile_path: &str, tasks: Arc>>>, config: Arc, ) -> Result<(), Error> { let mut generate = true; loop { let mut tasks_guard = tasks.write(); if let Some(sender) = tasks_guard.get(&(z, x, y)) { let sender = sender.clone(); drop(tasks_guard); generate = false; sender.send(()).ok(); } else { if generate { let (sender, receiver) = crossbeam_channel::bounded(0); let lock_coord = if z > config.zoom_default { // Generate 4 tiles at once (z, x & !1, y & !1) } else { (z, x, y) }; tasks_guard.insert(lock_coord, sender); drop(tasks_guard); let res: Result<(), Error> = try { match z.cmp(&config.zoom_default) { std::cmp::Ordering::Equal => { run_minetestmapper(z, x, y, tile_dir, tile_path, config)? } std::cmp::Ordering::Less => { let mut tile = image::ImageBuffer::, Vec>::new( config.tile_size as u32 * 2, config.tile_size as u32 * 2, ); if let Ok(ntile_path) = get_dep_tile(z + 1, x * 2, y * 2, tasks.clone(), config.clone()) { if let Ok(ntile) = image::open(ntile_path) { image::imageops::replace(&mut tile, &ntile.into_rgb8(), 0, 0); } } if let Ok(ntile_path) = get_dep_tile(z + 1, x * 2, y * 2 + 1, tasks.clone(), config.clone()) { if let Ok(ntile) = image::open(ntile_path) { image::imageops::replace( &mut tile, &ntile.into_rgb8(), 0, config.tile_size as i64, ); } } if let Ok(ntile_path) = get_dep_tile(z + 1, x * 2 + 1, y * 2, tasks.clone(), config.clone()) { if let Ok(ntile) = image::open(ntile_path) { image::imageops::replace( &mut tile, &ntile.into_rgb8(), config.tile_size as i64, 0, ); } } if let Ok(ntile_path) = get_dep_tile( z + 1, x * 2 + 1, y * 2 + 1, tasks.clone(), config.clone(), ) { if let Ok(ntile) = image::open(ntile_path) { image::imageops::replace( &mut tile, &ntile.into_rgb8(), config.tile_size as i64, config.tile_size as i64, ); } } std::fs::create_dir_all(tile_dir).map_err(|e| { error!("Generating tile ({},{},{}): {}", z, x, y, e); Error::Io(e) })?; image::imageops::resize( &tile, config.tile_size as u32, config.tile_size as u32, image::imageops::Triangle, ) .save(tile_path) .map_err(|e| { error!("Generating tile ({},{},{}): {}", z, x, y, e); Error::Image(e) })?; } std::cmp::Ordering::Greater => { let ntile_path = get_dep_tile( z - 1, lock_coord.1 / 2, lock_coord.2 / 2, tasks.clone(), config.clone(), )?; let ntile = image::open(ntile_path).map_err(Error::Image)?; let ntile = image::imageops::resize( &ntile, config.tile_size as u32 * 2, config.tile_size as u32 * 2, image::imageops::Nearest, ); let tile0_dir = format!("{}{}/{}", config.output_path, z, lock_coord.1); let tile1_dir = format!("{}{}/{}", config.output_path, z, lock_coord.1 + 1); std::fs::create_dir_all(&tile0_dir).map_err(|e| { error!("Generating tile ({},{},{}): {}", z, x, y, e); Error::Io(e) })?; std::fs::create_dir_all(&tile1_dir).map_err(|e| { error!("Generating tile ({},{},{}): {}", z, x, y, e); Error::Io(e) })?; image::imageops::crop_imm( &ntile, 0, 0, config.tile_size as u32, config.tile_size as u32, ) .to_image() .save(format!("{}/{}.png", tile0_dir, lock_coord.2)) .map_err(|e| { error!( "Generating tile ({},{},{}): {}", z, lock_coord.1, lock_coord.2, e ); Error::Image(e) })?; image::imageops::crop_imm( &ntile, 0, config.tile_size as u32, config.tile_size as u32, config.tile_size as u32, ) .to_image() .save(format!("{}/{}.png", tile0_dir, lock_coord.2 + 1)) .map_err(|e| { error!( "Generating tile ({},{},{}): {}", z, lock_coord.1, lock_coord.2 + 1, e ); Error::Image(e) })?; image::imageops::crop_imm( &ntile, config.tile_size as u32, 0, config.tile_size as u32, config.tile_size as u32, ) .to_image() .save(format!("{}/{}.png", tile1_dir, lock_coord.2)) .map_err(|e| { error!( "Generating tile ({},{},{}): {}", z, lock_coord.1 + 1, lock_coord.2, e ); Error::Image(e) })?; image::imageops::crop_imm( &ntile, config.tile_size as u32, config.tile_size as u32, config.tile_size as u32, config.tile_size as u32, ) .to_image() .save(format!("{}/{}.png", tile1_dir, lock_coord.2 + 1)) .map_err(|e| { error!( "Generating tile ({},{},{}): {}", z, lock_coord.1 + 1, lock_coord.2 + 1, e ); Error::Image(e) })?; } } }; tasks.write().remove(&lock_coord); receiver.recv().ok(); res?; } else { drop(tasks_guard); } return Ok(()); } } } fn get_dep_tile( z: i32, x: i32, y: i32, tasks: Arc>>>, config: Arc, ) -> Result { let tile_dir = format!("{}{}/{}", config.output_path, z, x); let tile_path = format!("{}/{}.png", tile_dir, y); match std::fs::metadata(&tile_path) { Ok(metadata) => match metadata.modified() { Ok(modified_time) => { if SystemTime::now() .duration_since(modified_time) .unwrap() .as_secs() > config.cache_age { generate_tile(z, x, y, &tile_dir, &tile_path, tasks, config)?; } } Err(e) => { warn!("Cannot get modified time of `{}`: {}", tile_path, e); generate_tile(z, x, y, &tile_dir, &tile_path, tasks, config)?; } }, Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { debug!("Tile not found `{}`", tile_path); } else { warn!("Cannot get metadata of `{}`: {}", tile_path, e); } generate_tile(z, x, y, &tile_dir, &tile_path, tasks, config)?; } } Ok(tile_path) } fn resp_tile( z: i32, x: i32, y: i32, tile_dir: &str, tile_path: &str, tasks: Arc>>>, config: Arc, ) -> Result, Error> { generate_tile(z, x, y, tile_dir, tile_path, tasks, config)?; read_tile_file(tile_path) } async fn req_tile( req: Request<()>, tasks: Arc>>>, config: Arc, ) -> tide::Result { let z: i32 = req.param("z")?.parse()?; if z > config.zoom_max || z < config.zoom_min { return Ok(tide::StatusCode::Forbidden.into()); } let x: i32 = req.param("x")?.parse()?; let y = req.param("y")?; let y = y.strip_suffix(".png").unwrap_or(y).parse()?; let tile_dir = format!("{}{}/{}", config.output_path, z, x); let tile_path = format!("{}/{}.png", tile_dir, y); match std::fs::metadata(&tile_path) { Ok(metadata) => match metadata.modified() { Ok(modified_time) => { if SystemTime::now() .duration_since(modified_time) .unwrap() .as_secs() > config.cache_age { match resp_tile(z, x, y, &tile_dir, &tile_path, tasks, config) { Ok(tile_content) => Ok(tide::Response::builder(200) .content_type(http_types::mime::PNG) .body(tile_content) .build()), Err(_) => Ok(tide::StatusCode::InternalServerError.into()), } } else { match read_tile_file(&tile_path) { Ok(tile_content) => Ok(tide::Response::builder(200) .content_type(http_types::mime::PNG) .body(tile_content) .build()), Err(_) => Ok(tide::StatusCode::InternalServerError.into()), } } } Err(e) => { warn!("Cannot get modified time of `{}`: {}", tile_path, e); match resp_tile(z, x, y, &tile_dir, &tile_path, tasks, config) { Ok(tile_content) => Ok(tide::Response::builder(200) .content_type(http_types::mime::PNG) .body(tile_content) .build()), Err(_) => Ok(tide::StatusCode::InternalServerError.into()), } } }, Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { debug!("Tile not found `{}`", tile_path); } else { warn!("Cannot get metadata of `{}`: {}", tile_path, e); } match resp_tile(z, x, y, &tile_dir, &tile_path, tasks, config) { Ok(tile_content) => Ok(tide::Response::builder(200) .content_type(http_types::mime::PNG) .body(tile_content) .build()), Err(_) => Ok(tide::StatusCode::InternalServerError.into()), } } } }