493 lines
12 KiB
Rust
493 lines
12 KiB
Rust
#![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 <t@txmn.tk> (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<String>,
|
|
}
|
|
|
|
#[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<Args>,
|
|
) -> 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<Vec<u8>, 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<RwLock<HashMap<(i32, i32, i32), Sender<()>>>>,
|
|
config: Arc<Args>,
|
|
) -> 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::<image::Rgb<u8>, Vec<u8>>::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<RwLock<HashMap<(i32, i32, i32), Sender<()>>>>,
|
|
config: Arc<Args>,
|
|
) -> Result<String, Error> {
|
|
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<RwLock<HashMap<(i32, i32, i32), Sender<()>>>>,
|
|
config: Arc<Args>,
|
|
) -> Result<Vec<u8>, 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<RwLock<HashMap<(i32, i32, i32), Sender<()>>>>,
|
|
config: Arc<Args>,
|
|
) -> 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()),
|
|
}
|
|
}
|
|
}
|
|
}
|