mirror of
https://github.com/louis-e/arnis.git
synced 2026-02-19 23:46:10 -05:00
Compare commits
89 Commits
parallel-p
...
fix/depend
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
489e571a42 | ||
|
|
67e22a574a | ||
|
|
c094db8464 | ||
|
|
9be9104c8d | ||
|
|
8c0f0cc366 | ||
|
|
403469dcb5 | ||
|
|
e0674823fd | ||
|
|
438328ec28 | ||
|
|
d2a8f09487 | ||
|
|
318ab1e26c | ||
|
|
6adb8d050e | ||
|
|
0cc32e70b9 | ||
|
|
ed07de68a6 | ||
|
|
0fda04f2be | ||
|
|
8598a1847b | ||
|
|
2422786607 | ||
|
|
4e2d886077 | ||
|
|
4299863410 | ||
|
|
b316f95030 | ||
|
|
7cc53434a7 | ||
|
|
6bd17c937d | ||
|
|
cf198f9e93 | ||
|
|
bedf2b763a | ||
|
|
02823134df | ||
|
|
36e1c04e6f | ||
|
|
333ed52e28 | ||
|
|
e9c8f203a7 | ||
|
|
87069665fe | ||
|
|
5bfb8606e2 | ||
|
|
44023f99e2 | ||
|
|
3aad272c20 | ||
|
|
9f47b0269b | ||
|
|
c7e1fec02c | ||
|
|
fb05e2f2b8 | ||
|
|
7015cfff5f | ||
|
|
78ca5a49ce | ||
|
|
e265f8fa7e | ||
|
|
552f4ab013 | ||
|
|
319eb656ee | ||
|
|
11a756ab06 | ||
|
|
0f93853dcb | ||
|
|
b4c47f559c | ||
|
|
a86e23129b | ||
|
|
69b30ef59f | ||
|
|
1733f5d664 | ||
|
|
e6b6de27ff | ||
|
|
ac0fc275dc | ||
|
|
de1f52bfaf | ||
|
|
851aec71d0 | ||
|
|
03cc86f3e2 | ||
|
|
d4af5ce7ef | ||
|
|
382ab19a0d | ||
|
|
38678deefc | ||
|
|
674a2d9656 | ||
|
|
c4ad3dd61a | ||
|
|
e42bc121fa | ||
|
|
19da1fe55d | ||
|
|
663394f3b5 | ||
|
|
bef7bd3965 | ||
|
|
d8e1b29146 | ||
|
|
a9f53b2cd6 | ||
|
|
516b9ecf33 | ||
|
|
babe610cca | ||
|
|
20922a3be6 | ||
|
|
f1dc3b8ffb | ||
|
|
602767c1d1 | ||
|
|
96e6d9e129 | ||
|
|
c722ea689f | ||
|
|
f473e980a2 | ||
|
|
1901c21049 | ||
|
|
bc41838671 | ||
|
|
11de6cfd85 | ||
|
|
92f629fc96 | ||
|
|
880d86971d | ||
|
|
1421247ea4 | ||
|
|
1b21dec366 | ||
|
|
c9a9d55f76 | ||
|
|
53846a7b5a | ||
|
|
0593615909 | ||
|
|
ceec7cc190 | ||
|
|
d876f5ce60 | ||
|
|
d3a416754d | ||
|
|
fc6c2a255f | ||
|
|
3b70694167 | ||
|
|
e5f0b1050a | ||
|
|
a0fd0c12e2 | ||
|
|
9b87e3538a | ||
|
|
46959365df | ||
|
|
f57d14b200 |
3044
Cargo.lock
generated
3044
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,8 @@ overflow-checks = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gui"]
|
default = ["gui"]
|
||||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "tauri-build", "bedrock"]
|
||||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek", "rusty-leveldb"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = {version = "2", optional = true}
|
tauri-build = {version = "2", optional = true}
|
||||||
@@ -25,7 +25,7 @@ base64 = "0.22.1"
|
|||||||
byteorder = { version = "1.5", optional = true }
|
byteorder = { version = "1.5", optional = true }
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
dirs = {version = "6.0.0", optional = true }
|
dirs = "6.0.0"
|
||||||
fastanvil = "0.32.0"
|
fastanvil = "0.32.0"
|
||||||
fastnbt = "2.6.0"
|
fastnbt = "2.6.0"
|
||||||
flate2 = "1.1"
|
flate2 = "1.1"
|
||||||
@@ -40,7 +40,7 @@ once_cell = "1.21.3"
|
|||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
reqwest = { version = "0.13.1", features = ["blocking", "json", "query"] }
|
||||||
rfd = { version = "0.16.0", optional = true }
|
rfd = { version = "0.16.0", optional = true }
|
||||||
semver = "1.0.27"
|
semver = "1.0.27"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -54,6 +54,7 @@ bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs",
|
|||||||
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
||||||
vek = { version = "0.17", optional = true }
|
vek = { version = "0.17", optional = true }
|
||||||
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
||||||
|
rusty-leveldb = { version = "3", optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||||
|
|||||||
Binary file not shown.
147
src/args.rs
147
src/args.rs
@@ -19,9 +19,14 @@ pub struct Args {
|
|||||||
#[arg(long, group = "location")]
|
#[arg(long, group = "location")]
|
||||||
pub save_json_file: Option<String>,
|
pub save_json_file: Option<String>,
|
||||||
|
|
||||||
/// Path to the Minecraft world (required)
|
/// Output directory for the generated world (required for Java, optional for Bedrock).
|
||||||
#[arg(long, value_parser = validate_minecraft_world_path)]
|
/// Use --output-dir (or the deprecated --path alias) to specify where the world is created.
|
||||||
pub path: PathBuf,
|
#[arg(long = "output-dir", alias = "path")]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Generate a Bedrock Edition world (.mcworld) instead of Java Edition
|
||||||
|
#[arg(long)]
|
||||||
|
pub bedrock: bool,
|
||||||
|
|
||||||
/// Downloader method (requests/curl/wget) (optional)
|
/// Downloader method (requests/curl/wget) (optional)
|
||||||
#[arg(long, default_value = "requests")]
|
#[arg(long, default_value = "requests")]
|
||||||
@@ -40,17 +45,23 @@ pub struct Args {
|
|||||||
pub terrain: bool,
|
pub terrain: bool,
|
||||||
|
|
||||||
/// Enable interior generation (optional)
|
/// Enable interior generation (optional)
|
||||||
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
|
#[arg(long, default_value_t = true)]
|
||||||
pub interior: bool,
|
pub interior: bool,
|
||||||
|
|
||||||
/// Enable roof generation (optional)
|
/// Enable roof generation (optional)
|
||||||
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
|
#[arg(long, default_value_t = true)]
|
||||||
pub roof: bool,
|
pub roof: bool,
|
||||||
|
|
||||||
/// Enable filling ground (optional)
|
/// Enable filling ground (optional)
|
||||||
#[arg(long, default_value_t = false, action = clap::ArgAction::SetFalse)]
|
#[arg(long, default_value_t = false)]
|
||||||
pub fillground: bool,
|
pub fillground: bool,
|
||||||
|
|
||||||
|
/// Enable city boundary ground generation (optional)
|
||||||
|
/// When enabled, detects building clusters and places stone ground in urban areas.
|
||||||
|
/// Isolated buildings in rural areas will keep grass around them.
|
||||||
|
#[arg(long, default_value_t = true)]
|
||||||
|
pub city_boundaries: bool,
|
||||||
|
|
||||||
/// Enable debug mode (optional)
|
/// Enable debug mode (optional)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
@@ -60,19 +71,41 @@ pub struct Args {
|
|||||||
pub timeout: Option<Duration>,
|
pub timeout: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
/// Validates CLI arguments after parsing.
|
||||||
let mc_world_path = PathBuf::from(path);
|
/// For Java Edition: `--path` is required and must point to an existing directory
|
||||||
if !mc_world_path.exists() {
|
/// where a new world will be created automatically.
|
||||||
return Err(format!("Path does not exist: {path}"));
|
/// For Bedrock Edition (`--bedrock`): `--path` is optional (defaults to Desktop output).
|
||||||
|
pub fn validate_args(args: &Args) -> Result<(), String> {
|
||||||
|
if args.bedrock {
|
||||||
|
// Bedrock: path is optional; if provided, it must be an existing directory
|
||||||
|
if let Some(ref path) = args.path {
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("Path does not exist: {}", path.display()));
|
||||||
|
}
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(format!("Path is not a directory: {}", path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Java: path is required and must be an existing directory
|
||||||
|
match &args.path {
|
||||||
|
None => {
|
||||||
|
return Err(
|
||||||
|
"The --output-dir argument is required for Java Edition. Provide the directory where the world should be created. Use --bedrock for Bedrock Edition output."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(ref path) => {
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("Path does not exist: {}", path.display()));
|
||||||
|
}
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(format!("Path is not a directory: {}", path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !mc_world_path.is_dir() {
|
Ok(())
|
||||||
return Err(format!("Path is not a directory: {path}"));
|
|
||||||
}
|
|
||||||
let region = mc_world_path.join("region");
|
|
||||||
if !region.is_dir() {
|
|
||||||
return Err(format!("No Minecraft world found at {region:?}"));
|
|
||||||
}
|
|
||||||
Ok(mc_world_path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||||
@@ -84,22 +117,15 @@ fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntEr
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn minecraft_tmpdir() -> tempfile::TempDir {
|
|
||||||
let tmpdir = tempfile::tempdir().unwrap();
|
|
||||||
// create a `region` directory in the tempdir
|
|
||||||
let region_path = tmpdir.path().join("region");
|
|
||||||
std::fs::create_dir(®ion_path).unwrap();
|
|
||||||
tmpdir
|
|
||||||
}
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_flags() {
|
fn test_flags() {
|
||||||
let tmpdir = minecraft_tmpdir();
|
let tmpdir = tempfile::tempdir().unwrap();
|
||||||
let tmp_path = tmpdir.path().to_str().unwrap();
|
let tmp_path = tmpdir.path().to_str().unwrap();
|
||||||
|
|
||||||
// Test that terrain/debug are SetTrue
|
// Test that terrain/debug are SetTrue
|
||||||
let cmd = [
|
let cmd = [
|
||||||
"arnis",
|
"arnis",
|
||||||
"--path",
|
"--output-dir",
|
||||||
tmp_path,
|
tmp_path,
|
||||||
"--bbox",
|
"--bbox",
|
||||||
"1,2,3,4",
|
"1,2,3,4",
|
||||||
@@ -110,24 +136,81 @@ mod tests {
|
|||||||
assert!(args.debug);
|
assert!(args.debug);
|
||||||
assert!(args.terrain);
|
assert!(args.terrain);
|
||||||
|
|
||||||
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
|
let cmd = ["arnis", "--output-dir", tmp_path, "--bbox", "1,2,3,4"];
|
||||||
let args = Args::parse_from(cmd.iter());
|
let args = Args::parse_from(cmd.iter());
|
||||||
assert!(!args.debug);
|
assert!(!args.debug);
|
||||||
assert!(!args.terrain);
|
assert!(!args.terrain);
|
||||||
|
assert!(!args.bedrock);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bedrock_flag() {
|
||||||
|
// Bedrock mode doesn't require --output-dir
|
||||||
|
let cmd = ["arnis", "--bedrock", "--bbox", "1,2,3,4"];
|
||||||
|
let args = Args::parse_from(cmd.iter());
|
||||||
|
assert!(args.bedrock);
|
||||||
|
assert!(args.path.is_none());
|
||||||
|
assert!(validate_args(&args).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_java_requires_path() {
|
||||||
|
let cmd = ["arnis", "--bbox", "1,2,3,4"];
|
||||||
|
let args = Args::parse_from(cmd.iter());
|
||||||
|
assert!(!args.bedrock);
|
||||||
|
assert!(args.path.is_none());
|
||||||
|
assert!(validate_args(&args).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_java_path_must_exist() {
|
||||||
|
let cmd = [
|
||||||
|
"arnis",
|
||||||
|
"--output-dir",
|
||||||
|
"/nonexistent/path",
|
||||||
|
"--bbox",
|
||||||
|
"1,2,3,4",
|
||||||
|
];
|
||||||
|
let args = Args::parse_from(cmd.iter());
|
||||||
|
let result = validate_args(&args);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bedrock_path_must_exist() {
|
||||||
|
let cmd = [
|
||||||
|
"arnis",
|
||||||
|
"--bedrock",
|
||||||
|
"--output-dir",
|
||||||
|
"/nonexistent/path",
|
||||||
|
"--bbox",
|
||||||
|
"1,2,3,4",
|
||||||
|
];
|
||||||
|
let args = Args::parse_from(cmd.iter());
|
||||||
|
let result = validate_args(&args);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("does not exist"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_options() {
|
fn test_required_options() {
|
||||||
let tmpdir = minecraft_tmpdir();
|
let tmpdir = tempfile::tempdir().unwrap();
|
||||||
let tmp_path = tmpdir.path().to_str().unwrap();
|
let tmp_path = tmpdir.path().to_str().unwrap();
|
||||||
|
|
||||||
let cmd = ["arnis"];
|
let cmd = ["arnis"];
|
||||||
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
||||||
|
|
||||||
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
|
let cmd = ["arnis", "--output-dir", tmp_path, "--bbox", "1,2,3,4"];
|
||||||
assert!(Args::try_parse_from(cmd.iter()).is_ok());
|
let args = Args::try_parse_from(cmd.iter()).unwrap();
|
||||||
|
assert!(validate_args(&args).is_ok());
|
||||||
|
|
||||||
let cmd = ["arnis", "--path", tmp_path, "--file", ""];
|
// Verify --path still works as a deprecated alias
|
||||||
|
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
|
||||||
|
let args = Args::try_parse_from(cmd.iter()).unwrap();
|
||||||
|
assert!(validate_args(&args).is_ok());
|
||||||
|
|
||||||
|
let cmd = ["arnis", "--output-dir", tmp_path, "--file", ""];
|
||||||
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
||||||
|
|
||||||
// The --gui flag isn't used here, ugh. TODO clean up main.rs and its argparse usage.
|
// The --gui flag isn't used here, ugh. TODO clean up main.rs and its argparse usage.
|
||||||
|
|||||||
@@ -129,6 +129,81 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
|||||||
)],
|
)],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Dark oak log with axis
|
||||||
|
"dark_oak_log" => BedrockBlock::with_states(
|
||||||
|
"dark_oak_log",
|
||||||
|
vec![(
|
||||||
|
"pillar_axis",
|
||||||
|
BedrockBlockStateValue::String("y".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Jungle log with axis
|
||||||
|
"jungle_log" => BedrockBlock::with_states(
|
||||||
|
"jungle_log",
|
||||||
|
vec![(
|
||||||
|
"pillar_axis",
|
||||||
|
BedrockBlockStateValue::String("y".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Acacia log with axis
|
||||||
|
"acacia_log" => BedrockBlock::with_states(
|
||||||
|
"acacia_log",
|
||||||
|
vec![(
|
||||||
|
"pillar_axis",
|
||||||
|
BedrockBlockStateValue::String("y".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Spruce leaves with persistence
|
||||||
|
"spruce_leaves" => BedrockBlock::with_states(
|
||||||
|
"leaves",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"old_leaf_type",
|
||||||
|
BedrockBlockStateValue::String("spruce".to_string()),
|
||||||
|
),
|
||||||
|
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Dark oak leaves with persistence
|
||||||
|
"dark_oak_leaves" => BedrockBlock::with_states(
|
||||||
|
"leaves2",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"new_leaf_type",
|
||||||
|
BedrockBlockStateValue::String("dark_oak".to_string()),
|
||||||
|
),
|
||||||
|
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Jungle leaves with persistence
|
||||||
|
"jungle_leaves" => BedrockBlock::with_states(
|
||||||
|
"leaves",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"old_leaf_type",
|
||||||
|
BedrockBlockStateValue::String("jungle".to_string()),
|
||||||
|
),
|
||||||
|
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Acacia leaves with persistence
|
||||||
|
"acacia_leaves" => BedrockBlock::with_states(
|
||||||
|
"leaves2",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"new_leaf_type",
|
||||||
|
BedrockBlockStateValue::String("acacia".to_string()),
|
||||||
|
),
|
||||||
|
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
// Stone slab (bottom half by default)
|
// Stone slab (bottom half by default)
|
||||||
"stone_slab" => BedrockBlock::with_states(
|
"stone_slab" => BedrockBlock::with_states(
|
||||||
"stone_block_slab",
|
"stone_block_slab",
|
||||||
@@ -215,6 +290,13 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
|||||||
BedrockBlockStateValue::String("stone_brick".to_string()),
|
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||||
)],
|
)],
|
||||||
),
|
),
|
||||||
|
"brick_wall" => BedrockBlock::with_states(
|
||||||
|
"cobblestone_wall",
|
||||||
|
vec![(
|
||||||
|
"wall_block_type",
|
||||||
|
BedrockBlockStateValue::String("brick".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
// Flowers - poppy is just "red_flower" in Bedrock
|
// Flowers - poppy is just "red_flower" in Bedrock
|
||||||
"poppy" => BedrockBlock::with_states(
|
"poppy" => BedrockBlock::with_states(
|
||||||
@@ -321,6 +403,10 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
|||||||
"concrete",
|
"concrete",
|
||||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||||
),
|
),
|
||||||
|
"green_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||||
|
),
|
||||||
|
|
||||||
// Terracotta colors
|
// Terracotta colors
|
||||||
"white_terracotta" => BedrockBlock::with_states(
|
"white_terracotta" => BedrockBlock::with_states(
|
||||||
@@ -372,6 +458,13 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
|||||||
"stained_hardened_clay",
|
"stained_hardened_clay",
|
||||||
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||||
),
|
),
|
||||||
|
"light_gray_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("silver".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
// Plain terracotta
|
// Plain terracotta
|
||||||
"terracotta" => BedrockBlock::simple("hardened_clay"),
|
"terracotta" => BedrockBlock::simple("hardened_clay"),
|
||||||
|
|
||||||
@@ -403,6 +496,17 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
|||||||
BedrockBlockStateValue::String("yellow".to_string()),
|
BedrockBlockStateValue::String("yellow".to_string()),
|
||||||
)],
|
)],
|
||||||
),
|
),
|
||||||
|
"orange_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("orange".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"blue_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||||
|
),
|
||||||
|
|
||||||
// Carpets
|
// Carpets
|
||||||
"white_carpet" => BedrockBlock::with_states(
|
"white_carpet" => BedrockBlock::with_states(
|
||||||
@@ -434,6 +538,54 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
|||||||
"stained_glass",
|
"stained_glass",
|
||||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||||
),
|
),
|
||||||
|
"cyan_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||||
|
),
|
||||||
|
"blue_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||||
|
),
|
||||||
|
"light_blue_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"red_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||||
|
),
|
||||||
|
"yellow_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("yellow".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"purple_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("purple".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"orange_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("orange".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"magenta_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("magenta".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"daylight_detector" => BedrockBlock::simple("daylight_detector"),
|
||||||
|
|
||||||
// Planks - Bedrock uses single "planks" block with wood_type state
|
// Planks - Bedrock uses single "planks" block with wood_type state
|
||||||
"oak_planks" => BedrockBlock::with_states(
|
"oak_planks" => BedrockBlock::with_states(
|
||||||
@@ -539,8 +691,34 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
|||||||
// Oak items mapped to dark_oak in Bedrock (or generic equivalents)
|
// Oak items mapped to dark_oak in Bedrock (or generic equivalents)
|
||||||
"oak_pressure_plate" => BedrockBlock::simple("wooden_pressure_plate"),
|
"oak_pressure_plate" => BedrockBlock::simple("wooden_pressure_plate"),
|
||||||
"oak_door" => BedrockBlock::simple("wooden_door"),
|
"oak_door" => BedrockBlock::simple("wooden_door"),
|
||||||
|
"spruce_door" => BedrockBlock::simple("spruce_door"),
|
||||||
|
"dark_oak_door" => BedrockBlock::simple("dark_oak_door"),
|
||||||
"oak_trapdoor" => BedrockBlock::simple("trapdoor"),
|
"oak_trapdoor" => BedrockBlock::simple("trapdoor"),
|
||||||
|
|
||||||
|
// Vegetation with different Bedrock names
|
||||||
|
"fern" => BedrockBlock::with_states(
|
||||||
|
"tallgrass",
|
||||||
|
vec![(
|
||||||
|
"tall_grass_type",
|
||||||
|
BedrockBlockStateValue::String("fern".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"large_fern" => BedrockBlock::with_states(
|
||||||
|
"double_plant",
|
||||||
|
vec![(
|
||||||
|
"double_plant_type",
|
||||||
|
BedrockBlockStateValue::String("fern".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"cobweb" => BedrockBlock::simple("web"),
|
||||||
|
|
||||||
|
// Potted plants (Bedrock uses "flower_pot" for all variants;
|
||||||
|
// the contained plant is a block entity, not a block state)
|
||||||
|
"potted_poppy" => BedrockBlock::simple("flower_pot"),
|
||||||
|
"potted_red_tulip" => BedrockBlock::simple("flower_pot"),
|
||||||
|
"potted_dandelion" => BedrockBlock::simple("flower_pot"),
|
||||||
|
"potted_blue_orchid" => BedrockBlock::simple("flower_pot"),
|
||||||
|
|
||||||
// Bed (Bedrock uses single "bed" block with color state)
|
// Bed (Bedrock uses single "bed" block with color state)
|
||||||
"red_bed" => BedrockBlock::with_states(
|
"red_bed" => BedrockBlock::with_states(
|
||||||
"bed",
|
"bed",
|
||||||
@@ -564,8 +742,14 @@ pub fn to_bedrock_block_with_properties(
|
|||||||
) -> BedrockBlock {
|
) -> BedrockBlock {
|
||||||
let java_name = block.name();
|
let java_name = block.name();
|
||||||
|
|
||||||
|
// If no stored properties were passed, fall back to block.properties()
|
||||||
|
// so that blocks placed via set_block_absolute (e.g. doors with half=upper/lower)
|
||||||
|
// still get their default properties forwarded to the Bedrock converter.
|
||||||
|
let fallback_props = block.properties();
|
||||||
|
let effective_properties = java_properties.or(fallback_props.as_ref());
|
||||||
|
|
||||||
// Extract Java properties as a map if present
|
// Extract Java properties as a map if present
|
||||||
let props_map = java_properties.and_then(|v| {
|
let props_map = effective_properties.and_then(|v| {
|
||||||
if let fastnbt::Value::Compound(map) = v {
|
if let fastnbt::Value::Compound(map) = v {
|
||||||
Some(map)
|
Some(map)
|
||||||
} else {
|
} else {
|
||||||
@@ -578,6 +762,11 @@ pub fn to_bedrock_block_with_properties(
|
|||||||
return convert_stairs(java_name, props_map);
|
return convert_stairs(java_name, props_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle barrel facing direction
|
||||||
|
if java_name == "barrel" {
|
||||||
|
return convert_barrel(java_name, props_map);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle slabs with type property (top/bottom/double)
|
// Handle slabs with type property (top/bottom/double)
|
||||||
if java_name.ends_with("_slab") {
|
if java_name.ends_with("_slab") {
|
||||||
return convert_slab(java_name, props_map);
|
return convert_slab(java_name, props_map);
|
||||||
@@ -588,6 +777,16 @@ pub fn to_bedrock_block_with_properties(
|
|||||||
return convert_log(java_name, props_map);
|
return convert_log(java_name, props_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle doors with half property (upper/lower → upper_block_bit)
|
||||||
|
if java_name.ends_with("_door") && java_name != "iron_door" {
|
||||||
|
return convert_door(java_name, props_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trapdoors with facing/open/half properties
|
||||||
|
if java_name.ends_with("_trapdoor") {
|
||||||
|
return convert_trapdoor(java_name, props_map);
|
||||||
|
}
|
||||||
|
|
||||||
// Fall back to basic conversion without properties
|
// Fall back to basic conversion without properties
|
||||||
to_bedrock_block(block)
|
to_bedrock_block(block)
|
||||||
}
|
}
|
||||||
@@ -650,6 +849,46 @@ fn convert_stairs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert Java barrel to Bedrock format with facing direction.
|
||||||
|
fn convert_barrel(
|
||||||
|
java_name: &str,
|
||||||
|
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||||
|
) -> BedrockBlock {
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(props) = props {
|
||||||
|
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||||
|
let facing_direction = match facing.as_str() {
|
||||||
|
"down" => 0,
|
||||||
|
"up" => 1,
|
||||||
|
"north" => 2,
|
||||||
|
"south" => 3,
|
||||||
|
"west" => 4,
|
||||||
|
"east" => 5,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
states.insert(
|
||||||
|
"facing_direction".to_string(),
|
||||||
|
BedrockBlockStateValue::Int(facing_direction),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !states.contains_key("facing_direction") {
|
||||||
|
states.insert(
|
||||||
|
"facing_direction".to_string(),
|
||||||
|
BedrockBlockStateValue::Int(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
states.insert("open_bit".to_string(), BedrockBlockStateValue::Bool(false));
|
||||||
|
|
||||||
|
BedrockBlock {
|
||||||
|
name: format!("minecraft:{java_name}"),
|
||||||
|
states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert Java slab block to Bedrock format with proper type.
|
/// Convert Java slab block to Bedrock format with proper type.
|
||||||
fn convert_slab(
|
fn convert_slab(
|
||||||
java_name: &str,
|
java_name: &str,
|
||||||
@@ -750,6 +989,152 @@ fn convert_log(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert Java door block to Bedrock format with upper_block_bit.
|
||||||
|
///
|
||||||
|
/// Java doors use `half=upper/lower`, Bedrock uses `upper_block_bit` (bool).
|
||||||
|
/// Also maps door names: `oak_door` → `wooden_door`, others keep their names.
|
||||||
|
fn convert_door(
|
||||||
|
java_name: &str,
|
||||||
|
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||||
|
) -> BedrockBlock {
|
||||||
|
let bedrock_name = match java_name {
|
||||||
|
"oak_door" => "wooden_door",
|
||||||
|
_ => java_name, // spruce_door, dark_oak_door, etc. keep their name
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(props) = props {
|
||||||
|
// Convert half: Java "upper"/"lower" → Bedrock upper_block_bit true/false
|
||||||
|
if let Some(fastnbt::Value::String(half)) = props.get("half") {
|
||||||
|
let is_upper = half == "upper";
|
||||||
|
states.insert(
|
||||||
|
"upper_block_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(is_upper),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert facing if present
|
||||||
|
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||||
|
let direction = match facing.as_str() {
|
||||||
|
"east" => 0,
|
||||||
|
"south" => 1,
|
||||||
|
"west" => 2,
|
||||||
|
"north" => 3,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
states.insert(
|
||||||
|
"direction".to_string(),
|
||||||
|
BedrockBlockStateValue::Int(direction),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hinge if present
|
||||||
|
if let Some(fastnbt::Value::String(hinge)) = props.get("hinge") {
|
||||||
|
let door_hinge = hinge == "right";
|
||||||
|
states.insert(
|
||||||
|
"door_hinge_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(door_hinge),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert open if present
|
||||||
|
if let Some(fastnbt::Value::String(open)) = props.get("open") {
|
||||||
|
let is_open = open == "true";
|
||||||
|
states.insert(
|
||||||
|
"open_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(is_open),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults if no properties were set
|
||||||
|
if !states.contains_key("upper_block_bit") {
|
||||||
|
states.insert(
|
||||||
|
"upper_block_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !states.contains_key("direction") {
|
||||||
|
states.insert("direction".to_string(), BedrockBlockStateValue::Int(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
BedrockBlock {
|
||||||
|
name: format!("minecraft:{bedrock_name}"),
|
||||||
|
states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Java trapdoor block to Bedrock format with facing/open/half states.
|
||||||
|
fn convert_trapdoor(
|
||||||
|
java_name: &str,
|
||||||
|
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||||
|
) -> BedrockBlock {
|
||||||
|
// Map Java trapdoor names to Bedrock equivalents
|
||||||
|
let bedrock_name = match java_name {
|
||||||
|
"oak_trapdoor" => "trapdoor",
|
||||||
|
"iron_trapdoor" => "iron_trapdoor",
|
||||||
|
_ => java_name, // spruce_trapdoor, dark_oak_trapdoor, birch_trapdoor, etc.
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(props) = props {
|
||||||
|
// Convert facing: Java "north/south/east/west" → Bedrock "direction" (0-3)
|
||||||
|
// Bedrock trapdoor: 0=south, 1=north, 2=east, 3=west
|
||||||
|
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||||
|
let direction = match facing.as_str() {
|
||||||
|
"south" => 0,
|
||||||
|
"north" => 1,
|
||||||
|
"east" => 2,
|
||||||
|
"west" => 3,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
states.insert(
|
||||||
|
"direction".to_string(),
|
||||||
|
BedrockBlockStateValue::Int(direction),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert open: Java "true"/"false" → Bedrock open_bit
|
||||||
|
if let Some(fastnbt::Value::String(open)) = props.get("open") {
|
||||||
|
let is_open = open == "true";
|
||||||
|
states.insert(
|
||||||
|
"open_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(is_open),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert half: Java "top"/"bottom" → Bedrock upside_down_bit
|
||||||
|
if let Some(fastnbt::Value::String(half)) = props.get("half") {
|
||||||
|
let upside_down = half == "top";
|
||||||
|
states.insert(
|
||||||
|
"upside_down_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(upside_down),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults if no properties were set
|
||||||
|
if !states.contains_key("direction") {
|
||||||
|
states.insert("direction".to_string(), BedrockBlockStateValue::Int(0));
|
||||||
|
}
|
||||||
|
if !states.contains_key("open_bit") {
|
||||||
|
states.insert("open_bit".to_string(), BedrockBlockStateValue::Bool(false));
|
||||||
|
}
|
||||||
|
if !states.contains_key("upside_down_bit") {
|
||||||
|
states.insert(
|
||||||
|
"upside_down_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BedrockBlock {
|
||||||
|
name: format!("minecraft:{bedrock_name}"),
|
||||||
|
states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -266,7 +266,67 @@ impl Block {
|
|||||||
185 => "quartz_stairs",
|
185 => "quartz_stairs",
|
||||||
186 => "polished_andesite_stairs",
|
186 => "polished_andesite_stairs",
|
||||||
187 => "nether_brick_stairs",
|
187 => "nether_brick_stairs",
|
||||||
188 => "fern",
|
188 => "barrel",
|
||||||
|
189 => "fern",
|
||||||
|
190 => "cobweb",
|
||||||
|
191 => "chiseled_bookshelf",
|
||||||
|
192 => "chiseled_bookshelf",
|
||||||
|
193 => "chiseled_bookshelf",
|
||||||
|
194 => "chiseled_bookshelf",
|
||||||
|
195 => "chipped_anvil",
|
||||||
|
196 => "damaged_anvil",
|
||||||
|
197 => "large_fern",
|
||||||
|
198 => "large_fern",
|
||||||
|
199 => "chain",
|
||||||
|
200 => "end_rod",
|
||||||
|
201 => "lightning_rod",
|
||||||
|
202 => "gold_block",
|
||||||
|
203 => "sea_lantern",
|
||||||
|
204 => "orange_concrete",
|
||||||
|
205 => "orange_wool",
|
||||||
|
206 => "blue_wool",
|
||||||
|
207 => "green_concrete",
|
||||||
|
208 => "brick_wall",
|
||||||
|
209 => "redstone_block",
|
||||||
|
210 => "chain",
|
||||||
|
211 => "chain",
|
||||||
|
212 => "spruce_door",
|
||||||
|
213 => "spruce_door",
|
||||||
|
214 => "smooth_stone_slab",
|
||||||
|
215 => "glass_pane",
|
||||||
|
216 => "light_gray_terracotta",
|
||||||
|
217 => "oak_slab",
|
||||||
|
218 => "oak_door",
|
||||||
|
219 => "dark_oak_log",
|
||||||
|
220 => "dark_oak_leaves",
|
||||||
|
221 => "jungle_log",
|
||||||
|
222 => "jungle_leaves",
|
||||||
|
223 => "acacia_log",
|
||||||
|
224 => "acacia_leaves",
|
||||||
|
225 => "spruce_leaves",
|
||||||
|
226 => "cyan_stained_glass",
|
||||||
|
227 => "blue_stained_glass",
|
||||||
|
228 => "light_blue_stained_glass",
|
||||||
|
229 => "daylight_detector",
|
||||||
|
230 => "red_stained_glass",
|
||||||
|
231 => "yellow_stained_glass",
|
||||||
|
232 => "purple_stained_glass",
|
||||||
|
233 => "orange_stained_glass",
|
||||||
|
234 => "magenta_stained_glass",
|
||||||
|
235 => "potted_poppy",
|
||||||
|
236 => "oak_trapdoor",
|
||||||
|
237 => "oak_trapdoor",
|
||||||
|
238 => "oak_trapdoor",
|
||||||
|
239 => "oak_trapdoor",
|
||||||
|
240 => "quartz_slab",
|
||||||
|
241 => "dark_oak_trapdoor",
|
||||||
|
242 => "spruce_trapdoor",
|
||||||
|
243 => "birch_trapdoor",
|
||||||
|
244 => "mud_brick_slab",
|
||||||
|
245 => "brick_slab",
|
||||||
|
246 => "potted_red_tulip",
|
||||||
|
247 => "potted_dandelion",
|
||||||
|
248 => "potted_blue_orchid",
|
||||||
_ => panic!("Invalid id"),
|
_ => panic!("Invalid id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,6 +385,13 @@ impl Block {
|
|||||||
map
|
map
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
// Oak door lower
|
||||||
|
159 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
|
||||||
116 => Some(Value::Compound({
|
116 => Some(Value::Compound({
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(
|
map.insert(
|
||||||
@@ -464,6 +531,140 @@ impl Block {
|
|||||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||||
map
|
map
|
||||||
})),
|
})),
|
||||||
|
191 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
192 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
193 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
194 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
|
||||||
|
197 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
198 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
|
||||||
|
210 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("axis".to_string(), Value::String("x".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
211 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("axis".to_string(), Value::String("z".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Spruce door lower
|
||||||
|
212 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Spruce door upper
|
||||||
|
213 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Smooth stone slab (bottom by default)
|
||||||
|
214 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("type".to_string(), Value::String("bottom".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Oak slab top
|
||||||
|
217 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("type".to_string(), Value::String("top".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Oak door upper
|
||||||
|
218 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Dark oak leaves
|
||||||
|
220 => Some(Value::Compound({
|
||||||
|
let mut map: HashMap<String, Value> = HashMap::new();
|
||||||
|
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Jungle leaves
|
||||||
|
222 => Some(Value::Compound({
|
||||||
|
let mut map: HashMap<String, Value> = HashMap::new();
|
||||||
|
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Acacia leaves
|
||||||
|
224 => Some(Value::Compound({
|
||||||
|
let mut map: HashMap<String, Value> = HashMap::new();
|
||||||
|
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Spruce leaves
|
||||||
|
225 => Some(Value::Compound({
|
||||||
|
let mut map: HashMap<String, Value> = HashMap::new();
|
||||||
|
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Quartz slab (top half) used as window sill
|
||||||
|
240 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("type".to_string(), Value::String("top".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Open oak trapdoor facing north (hangs flat against wall, looks like shutter)
|
||||||
|
236 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||||
|
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||||
|
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Open oak trapdoor facing south
|
||||||
|
237 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||||
|
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||||
|
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Open oak trapdoor facing east
|
||||||
|
238 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||||
|
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||||
|
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
|
// Open oak trapdoor facing west
|
||||||
|
239 => Some(Value::Compound({
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||||
|
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||||
|
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||||
|
map
|
||||||
|
})),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,7 +899,69 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
|||||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
||||||
pub const FERN: Block = Block::new(188);
|
pub const BARREL: Block = Block::new(188);
|
||||||
|
pub const FERN: Block = Block::new(189);
|
||||||
|
pub const COBWEB: Block = Block::new(190);
|
||||||
|
pub const CHISELLED_BOOKSHELF_NORTH: Block = Block::new(191);
|
||||||
|
pub const CHISELLED_BOOKSHELF_EAST: Block = Block::new(192);
|
||||||
|
pub const CHISELLED_BOOKSHELF_SOUTH: Block = Block::new(193);
|
||||||
|
pub const CHISELLED_BOOKSHELF_WEST: Block = Block::new(194);
|
||||||
|
// Backwards-compatible alias (defaults to north-facing)
|
||||||
|
pub const CHISELLED_BOOKSHELF: Block = CHISELLED_BOOKSHELF_NORTH;
|
||||||
|
pub const CHIPPED_ANVIL: Block = Block::new(195);
|
||||||
|
pub const DAMAGED_ANVIL: Block = Block::new(196);
|
||||||
|
pub const LARGE_FERN_LOWER: Block = Block::new(197);
|
||||||
|
pub const LARGE_FERN_UPPER: Block = Block::new(198);
|
||||||
|
pub const CHAIN: Block = Block::new(199);
|
||||||
|
pub const END_ROD: Block = Block::new(200);
|
||||||
|
pub const LIGHTNING_ROD: Block = Block::new(201);
|
||||||
|
pub const GOLD_BLOCK: Block = Block::new(202);
|
||||||
|
pub const SEA_LANTERN: Block = Block::new(203);
|
||||||
|
pub const ORANGE_CONCRETE: Block = Block::new(204);
|
||||||
|
pub const ORANGE_WOOL: Block = Block::new(205);
|
||||||
|
pub const BLUE_WOOL: Block = Block::new(206);
|
||||||
|
pub const GREEN_CONCRETE: Block = Block::new(207);
|
||||||
|
pub const BRICK_WALL: Block = Block::new(208);
|
||||||
|
pub const REDSTONE_BLOCK: Block = Block::new(209);
|
||||||
|
pub const CHAIN_X: Block = Block::new(210);
|
||||||
|
pub const CHAIN_Z: Block = Block::new(211);
|
||||||
|
pub const SPRUCE_DOOR_LOWER: Block = Block::new(212);
|
||||||
|
pub const SPRUCE_DOOR_UPPER: Block = Block::new(213);
|
||||||
|
pub const SMOOTH_STONE_SLAB: Block = Block::new(214);
|
||||||
|
pub const GLASS_PANE: Block = Block::new(215);
|
||||||
|
pub const LIGHT_GRAY_TERRACOTTA: Block = Block::new(216);
|
||||||
|
pub const OAK_SLAB_TOP: Block = Block::new(217);
|
||||||
|
pub const OAK_DOOR_UPPER: Block = Block::new(218);
|
||||||
|
pub const DARK_OAK_LOG: Block = Block::new(219);
|
||||||
|
pub const DARK_OAK_LEAVES: Block = Block::new(220);
|
||||||
|
pub const JUNGLE_LOG: Block = Block::new(221);
|
||||||
|
pub const JUNGLE_LEAVES: Block = Block::new(222);
|
||||||
|
pub const ACACIA_LOG: Block = Block::new(223);
|
||||||
|
pub const ACACIA_LEAVES: Block = Block::new(224);
|
||||||
|
pub const SPRUCE_LEAVES: Block = Block::new(225);
|
||||||
|
pub const CYAN_STAINED_GLASS: Block = Block::new(226);
|
||||||
|
pub const BLUE_STAINED_GLASS: Block = Block::new(227);
|
||||||
|
pub const LIGHT_BLUE_STAINED_GLASS: Block = Block::new(228);
|
||||||
|
pub const DAYLIGHT_DETECTOR: Block = Block::new(229);
|
||||||
|
pub const RED_STAINED_GLASS: Block = Block::new(230);
|
||||||
|
pub const YELLOW_STAINED_GLASS: Block = Block::new(231);
|
||||||
|
pub const PURPLE_STAINED_GLASS: Block = Block::new(232);
|
||||||
|
pub const ORANGE_STAINED_GLASS: Block = Block::new(233);
|
||||||
|
pub const MAGENTA_STAINED_GLASS: Block = Block::new(234);
|
||||||
|
pub const FLOWER_POT: Block = Block::new(235);
|
||||||
|
pub const OAK_TRAPDOOR_OPEN_NORTH: Block = Block::new(236);
|
||||||
|
pub const OAK_TRAPDOOR_OPEN_SOUTH: Block = Block::new(237);
|
||||||
|
pub const OAK_TRAPDOOR_OPEN_EAST: Block = Block::new(238);
|
||||||
|
pub const OAK_TRAPDOOR_OPEN_WEST: Block = Block::new(239);
|
||||||
|
pub const QUARTZ_SLAB_TOP: Block = Block::new(240);
|
||||||
|
pub const DARK_OAK_TRAPDOOR: Block = Block::new(241);
|
||||||
|
pub const SPRUCE_TRAPDOOR: Block = Block::new(242);
|
||||||
|
pub const BIRCH_TRAPDOOR: Block = Block::new(243);
|
||||||
|
pub const MUD_BRICK_SLAB: Block = Block::new(244);
|
||||||
|
pub const BRICK_SLAB: Block = Block::new(245);
|
||||||
|
pub const POTTED_RED_TULIP: Block = Block::new(246);
|
||||||
|
pub const POTTED_DANDELION: Block = Block::new(247);
|
||||||
|
pub const POTTED_BLUE_ORCHID: Block = Block::new(248);
|
||||||
|
|
||||||
/// Maps a block to its corresponding stair variant
|
/// Maps a block to its corresponding stair variant
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -750,58 +1013,80 @@ pub static WINDOW_VARIATIONS: [Block; 7] = [
|
|||||||
TINTED_GLASS,
|
TINTED_GLASS,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Window types for different building styles
|
// Residential window options
|
||||||
|
pub static RESIDENTIAL_WINDOW_OPTIONS: [Block; 4] = [
|
||||||
|
GLASS,
|
||||||
|
WHITE_STAINED_GLASS,
|
||||||
|
LIGHT_GRAY_STAINED_GLASS,
|
||||||
|
BROWN_STAINED_GLASS,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Institutional window options (hospital, school, etc.)
|
||||||
|
pub static INSTITUTIONAL_WINDOW_OPTIONS: [Block; 3] =
|
||||||
|
[GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS];
|
||||||
|
|
||||||
|
// Hospitality window options (hotel, restaurant)
|
||||||
|
pub static HOSPITALITY_WINDOW_OPTIONS: [Block; 2] = [GLASS, WHITE_STAINED_GLASS];
|
||||||
|
|
||||||
|
// Industrial window options
|
||||||
|
pub static INDUSTRIAL_WINDOW_OPTIONS: [Block; 4] = [
|
||||||
|
GLASS,
|
||||||
|
GRAY_STAINED_GLASS,
|
||||||
|
LIGHT_GRAY_STAINED_GLASS,
|
||||||
|
BROWN_STAINED_GLASS,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Window types for different building styles (non-deterministic, for backwards compatibility)
|
||||||
pub fn get_window_block_for_building_type(building_type: &str) -> Block {
|
pub fn get_window_block_for_building_type(building_type: &str) -> Block {
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
get_window_block_for_building_type_with_rng(building_type, &mut rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic window block selection using provided RNG
|
||||||
|
pub fn get_window_block_for_building_type_with_rng(
|
||||||
|
building_type: &str,
|
||||||
|
rng: &mut impl rand::Rng,
|
||||||
|
) -> Block {
|
||||||
match building_type {
|
match building_type {
|
||||||
"residential" | "house" | "apartment" => {
|
"residential" | "house" | "apartment" | "apartments" => {
|
||||||
let residential_windows = [
|
RESIDENTIAL_WINDOW_OPTIONS[rng.gen_range(0..RESIDENTIAL_WINDOW_OPTIONS.len())]
|
||||||
GLASS,
|
|
||||||
WHITE_STAINED_GLASS,
|
|
||||||
LIGHT_GRAY_STAINED_GLASS,
|
|
||||||
BROWN_STAINED_GLASS,
|
|
||||||
];
|
|
||||||
residential_windows[rng.gen_range(0..residential_windows.len())]
|
|
||||||
}
|
}
|
||||||
"hospital" | "school" | "university" => {
|
"hospital" | "school" | "university" => {
|
||||||
let institutional_windows = [GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS];
|
INSTITUTIONAL_WINDOW_OPTIONS[rng.gen_range(0..INSTITUTIONAL_WINDOW_OPTIONS.len())]
|
||||||
institutional_windows[rng.gen_range(0..institutional_windows.len())]
|
|
||||||
}
|
}
|
||||||
"hotel" | "restaurant" => {
|
"hotel" | "restaurant" => {
|
||||||
let hospitality_windows = [GLASS, WHITE_STAINED_GLASS];
|
HOSPITALITY_WINDOW_OPTIONS[rng.gen_range(0..HOSPITALITY_WINDOW_OPTIONS.len())]
|
||||||
hospitality_windows[rng.gen_range(0..hospitality_windows.len())]
|
|
||||||
}
|
}
|
||||||
"industrial" | "warehouse" => {
|
"industrial" | "warehouse" => {
|
||||||
let industrial_windows = [
|
INDUSTRIAL_WINDOW_OPTIONS[rng.gen_range(0..INDUSTRIAL_WINDOW_OPTIONS.len())]
|
||||||
GLASS,
|
|
||||||
GRAY_STAINED_GLASS,
|
|
||||||
LIGHT_GRAY_STAINED_GLASS,
|
|
||||||
BROWN_STAINED_GLASS,
|
|
||||||
];
|
|
||||||
industrial_windows[rng.gen_range(0..industrial_windows.len())]
|
|
||||||
}
|
}
|
||||||
_ => WINDOW_VARIATIONS[rng.gen_range(0..WINDOW_VARIATIONS.len())],
|
_ => WINDOW_VARIATIONS[rng.gen_range(0..WINDOW_VARIATIONS.len())],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random floor block selection
|
// Floor block options for buildings
|
||||||
|
pub static FLOOR_BLOCK_OPTIONS: [Block; 8] = [
|
||||||
|
WHITE_CONCRETE,
|
||||||
|
GRAY_CONCRETE,
|
||||||
|
LIGHT_GRAY_CONCRETE,
|
||||||
|
POLISHED_ANDESITE,
|
||||||
|
SMOOTH_STONE,
|
||||||
|
STONE_BRICKS,
|
||||||
|
MUD_BRICKS,
|
||||||
|
OAK_PLANKS,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Random floor block selection (non-deterministic, for backwards compatibility)
|
||||||
pub fn get_random_floor_block() -> Block {
|
pub fn get_random_floor_block() -> Block {
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
FLOOR_BLOCK_OPTIONS[rng.gen_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||||
|
}
|
||||||
|
|
||||||
let floor_options = [
|
/// Deterministic floor block selection using provided RNG
|
||||||
WHITE_CONCRETE,
|
pub fn get_floor_block_with_rng(rng: &mut impl rand::Rng) -> Block {
|
||||||
GRAY_CONCRETE,
|
FLOOR_BLOCK_OPTIONS[rng.gen_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||||
LIGHT_GRAY_CONCRETE,
|
|
||||||
POLISHED_ANDESITE,
|
|
||||||
SMOOTH_STONE,
|
|
||||||
STONE_BRICKS,
|
|
||||||
MUD_BRICKS,
|
|
||||||
OAK_PLANKS,
|
|
||||||
];
|
|
||||||
floor_options[rng.gen_range(0..floor_options.len())]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define all predefined colors with their blocks
|
// Define all predefined colors with their blocks
|
||||||
@@ -983,7 +1268,6 @@ pub fn get_fallback_building_block() -> Block {
|
|||||||
STONE_BRICKS,
|
STONE_BRICKS,
|
||||||
WHITE_CONCRETE,
|
WHITE_CONCRETE,
|
||||||
WHITE_TERRACOTTA,
|
WHITE_TERRACOTTA,
|
||||||
OAK_PLANKS,
|
|
||||||
];
|
];
|
||||||
fallback_options[rng.gen_range(0..fallback_options.len())]
|
fallback_options[rng.gen_range(0..fallback_options.len())]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get way ID for ID generation
|
||||||
|
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
||||||
|
|
||||||
let is_closed = is_closed_polygon(nodes);
|
let is_closed = is_closed_polygon(nodes);
|
||||||
|
|
||||||
if !is_closed {
|
if !is_closed {
|
||||||
@@ -54,12 +57,13 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
|
|||||||
}
|
}
|
||||||
|
|
||||||
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
let polygon = remove_consecutive_duplicates(polygon);
|
let polygon = remove_consecutive_duplicates(polygon);
|
||||||
|
|
||||||
if polygon.len() < 3 {
|
if polygon.len() < 3 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
|
||||||
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
|
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,12 +500,15 @@ fn find_bbox_intersections(
|
|||||||
|
|
||||||
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
|
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
|
||||||
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
|
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
|
||||||
let eps = 0.5;
|
// Use a slightly larger epsilon to handle floating-point errors from Sutherland-Hodgman.
|
||||||
|
// Points should be clamped to bbox before this function is called, so any point
|
||||||
|
// at or very near the boundary should be considered ON that edge.
|
||||||
|
let eps = 1.0;
|
||||||
|
|
||||||
let on_left = (point.0 - min_x).abs() < eps;
|
let on_left = (point.0 - min_x).abs() <= eps;
|
||||||
let on_right = (point.0 - max_x).abs() < eps;
|
let on_right = (point.0 - max_x).abs() <= eps;
|
||||||
let on_bottom = (point.1 - min_z).abs() < eps;
|
let on_bottom = (point.1 - min_z).abs() <= eps;
|
||||||
let on_top = (point.1 - max_z).abs() < eps;
|
let on_top = (point.1 - max_z).abs() <= eps;
|
||||||
|
|
||||||
// Handle corners (assign to edge in counter-clockwise order)
|
// Handle corners (assign to edge in counter-clockwise order)
|
||||||
if on_bottom && on_left {
|
if on_bottom && on_left {
|
||||||
@@ -556,20 +563,21 @@ fn get_corners_between_edges(
|
|||||||
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
|
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
|
||||||
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
|
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
|
||||||
|
|
||||||
// Opposite edges: don't insert corners
|
// For opposite edges (distance = 2), we need to pick a direction.
|
||||||
if ccw_dist == 2 && cw_dist == 2 {
|
// Use counter-clockwise by default to ensure corners are inserted.
|
||||||
return Vec::new();
|
// This prevents diagonal lines when polygon spans opposite bbox edges.
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
if ccw_dist <= cw_dist {
|
if ccw_dist <= cw_dist {
|
||||||
|
// Go counter-clockwise
|
||||||
let mut current = edge1;
|
let mut current = edge1;
|
||||||
for _ in 0..ccw_dist {
|
for _ in 0..ccw_dist {
|
||||||
result.push(corners[current as usize]);
|
result.push(corners[current as usize]);
|
||||||
current = (current + 1) % 4;
|
current = (current + 1) % 4;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Go clockwise
|
||||||
let mut current = edge1;
|
let mut current = edge1;
|
||||||
for _ in 0..cw_dist {
|
for _ in 0..cw_dist {
|
||||||
current = (current + 4 - 1) % 4;
|
current = (current + 4 - 1) % 4;
|
||||||
@@ -580,6 +588,12 @@ fn get_corners_between_edges(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if two points are approximately equal (within epsilon tolerance).
|
||||||
|
fn points_approx_equal(p1: (f64, f64), p2: (f64, f64)) -> bool {
|
||||||
|
let eps = 1.0;
|
||||||
|
(p1.0 - p2.0).abs() <= eps && (p1.1 - p2.1).abs() <= eps
|
||||||
|
}
|
||||||
|
|
||||||
/// Inserts bbox corners where polygon transitions between different bbox edges.
|
/// Inserts bbox corners where polygon transitions between different bbox edges.
|
||||||
fn insert_bbox_corners(
|
fn insert_bbox_corners(
|
||||||
polygon: Vec<(f64, f64)>,
|
polygon: Vec<(f64, f64)>,
|
||||||
@@ -604,8 +618,13 @@ fn insert_bbox_corners(
|
|||||||
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
|
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
|
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
|
||||||
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
|
let corners = get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z);
|
||||||
result.push(corner);
|
|
||||||
|
// Filter out corners that match the current point or the next point
|
||||||
|
for corner in corners {
|
||||||
|
if !points_approx_equal(corner, current) && !points_approx_equal(corner, next) {
|
||||||
|
result.push(corner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, SMOOTH_STONE, STONE};
|
||||||
use crate::coordinate_system::cartesian::XZBBox;
|
use crate::coordinate_system::cartesian::XZBBox;
|
||||||
use crate::coordinate_system::geographic::LLBBox;
|
use crate::coordinate_system::geographic::LLBBox;
|
||||||
use crate::element_processing::*;
|
use crate::element_processing::*;
|
||||||
@@ -10,6 +10,7 @@ use crate::osm_parser::ProcessedElement;
|
|||||||
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
|
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
use crate::telemetry::{send_log, LogLevel};
|
use crate::telemetry::{send_log, LogLevel};
|
||||||
|
use crate::urban_ground;
|
||||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
use crate::world_editor::{WorldEditor, WorldFormat};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
@@ -27,23 +28,6 @@ pub struct GenerationOptions {
|
|||||||
pub spawn_point: Option<(i32, i32)>,
|
pub spawn_point: Option<(i32, i32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_world(
|
|
||||||
elements: Vec<ProcessedElement>,
|
|
||||||
xzbbox: XZBBox,
|
|
||||||
llbbox: LLBBox,
|
|
||||||
ground: Ground,
|
|
||||||
args: &Args,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
// Default to Java format when called from CLI
|
|
||||||
let options = GenerationOptions {
|
|
||||||
path: args.path.clone(),
|
|
||||||
format: WorldFormat::JavaAnvil,
|
|
||||||
level_name: None,
|
|
||||||
spawn_point: None,
|
|
||||||
};
|
|
||||||
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate world with explicit format options (used by GUI for Bedrock support)
|
/// Generate world with explicit format options (used by GUI for Bedrock support)
|
||||||
pub fn generate_world_with_options(
|
pub fn generate_world_with_options(
|
||||||
elements: Vec<ProcessedElement>,
|
elements: Vec<ProcessedElement>,
|
||||||
@@ -85,9 +69,16 @@ pub fn generate_world_with_options(
|
|||||||
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
|
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
|
||||||
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
|
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
|
||||||
|
|
||||||
// Process data
|
// Collect building centroids for urban ground generation (only if enabled)
|
||||||
|
// This must be done before the processing loop clears the flood fill cache
|
||||||
|
let building_centroids = if args.city_boundaries {
|
||||||
|
flood_fill_cache.collect_building_centroids(&elements)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process all elements (no longer need to partition boundaries)
|
||||||
let elements_count: usize = elements.len();
|
let elements_count: usize = elements.len();
|
||||||
let mut elements = elements; // Take ownership for consuming
|
|
||||||
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
||||||
process_pb.set_style(ProgressStyle::default_bar()
|
process_pb.set_style(ProgressStyle::default_bar()
|
||||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
||||||
@@ -98,8 +89,8 @@ pub fn generate_world_with_options(
|
|||||||
let mut current_progress_prcs: f64 = 25.0;
|
let mut current_progress_prcs: f64 = 25.0;
|
||||||
let mut last_emitted_progress: f64 = current_progress_prcs;
|
let mut last_emitted_progress: f64 = current_progress_prcs;
|
||||||
|
|
||||||
// Process elements by draining in insertion order
|
// Process all elements
|
||||||
for element in elements.drain(..) {
|
for element in elements.into_iter() {
|
||||||
process_pb.inc(1);
|
process_pb.inc(1);
|
||||||
current_progress_prcs += progress_increment_prcs;
|
current_progress_prcs += progress_increment_prcs;
|
||||||
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
||||||
@@ -177,6 +168,10 @@ pub fn generate_world_with_options(
|
|||||||
highways::generate_siding(&mut editor, way);
|
highways::generate_siding(&mut editor, way);
|
||||||
} else if way.tags.contains_key("man_made") {
|
} else if way.tags.contains_key("man_made") {
|
||||||
man_made::generate_man_made(&mut editor, &element, args);
|
man_made::generate_man_made(&mut editor, &element, args);
|
||||||
|
} else if way.tags.contains_key("power") {
|
||||||
|
power::generate_power(&mut editor, &element);
|
||||||
|
} else if way.tags.contains_key("place") {
|
||||||
|
landuse::generate_place(&mut editor, way, args, &flood_fill_cache);
|
||||||
}
|
}
|
||||||
// Release flood fill cache entry for this way
|
// Release flood fill cache entry for this way
|
||||||
flood_fill_cache.remove_way(way.id);
|
flood_fill_cache.remove_way(way.id);
|
||||||
@@ -210,6 +205,14 @@ pub fn generate_world_with_options(
|
|||||||
tourisms::generate_tourisms(&mut editor, node);
|
tourisms::generate_tourisms(&mut editor, node);
|
||||||
} else if node.tags.contains_key("man_made") {
|
} else if node.tags.contains_key("man_made") {
|
||||||
man_made::generate_man_made_nodes(&mut editor, node);
|
man_made::generate_man_made_nodes(&mut editor, node);
|
||||||
|
} else if node.tags.contains_key("power") {
|
||||||
|
power::generate_power_nodes(&mut editor, node);
|
||||||
|
} else if node.tags.contains_key("historic") {
|
||||||
|
historic::generate_historic(&mut editor, node);
|
||||||
|
} else if node.tags.contains_key("emergency") {
|
||||||
|
emergency::generate_emergency(&mut editor, node);
|
||||||
|
} else if node.tags.contains_key("advertising") {
|
||||||
|
advertising::generate_advertising(&mut editor, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProcessedElement::Relation(rel) => {
|
ProcessedElement::Relation(rel) => {
|
||||||
@@ -265,6 +268,16 @@ pub fn generate_world_with_options(
|
|||||||
|
|
||||||
process_pb.finish();
|
process_pb.finish();
|
||||||
|
|
||||||
|
// Compute urban ground lookup (if enabled)
|
||||||
|
// Uses a compact cell-based representation instead of storing all coordinates.
|
||||||
|
// Memory usage: ~270 KB vs ~560 MB for coordinate-based approach.
|
||||||
|
let urban_lookup = if args.city_boundaries && !building_centroids.is_empty() {
|
||||||
|
urban_ground::compute_urban_ground_lookup(building_centroids, &xzbbox)
|
||||||
|
} else {
|
||||||
|
urban_ground::UrbanGroundLookup::empty()
|
||||||
|
};
|
||||||
|
let has_urban_ground = !urban_lookup.is_empty();
|
||||||
|
|
||||||
// Drop remaining caches
|
// Drop remaining caches
|
||||||
drop(highway_connectivity);
|
drop(highway_connectivity);
|
||||||
drop(flood_fill_cache);
|
drop(flood_fill_cache);
|
||||||
@@ -321,9 +334,18 @@ pub fn generate_world_with_options(
|
|||||||
args.ground_level
|
args.ground_level
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if this coordinate is in an urban area (O(1) lookup)
|
||||||
|
let is_urban = has_urban_ground && urban_lookup.is_urban(x, z);
|
||||||
|
|
||||||
// Add default dirt and grass layer if there isn't a stone layer already
|
// Add default dirt and grass layer if there isn't a stone layer already
|
||||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
if is_urban {
|
||||||
|
// Urban area: smooth stone ground
|
||||||
|
editor.set_block_absolute(SMOOTH_STONE, x, ground_y, z, None, None);
|
||||||
|
} else {
|
||||||
|
// Rural/natural area: grass and dirt
|
||||||
|
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||||
|
}
|
||||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
||||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
||||||
}
|
}
|
||||||
@@ -397,16 +419,18 @@ pub fn generate_world_with_options(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Always update spawn Y since we now always set a spawn point (user-selected or default)
|
// Always update spawn Y since we now always set a spawn point (user-selected or default)
|
||||||
if let Err(e) = update_player_spawn_y_after_generation(
|
if let Some(ref world_path) = args.path {
|
||||||
&args.path,
|
if let Err(e) = update_player_spawn_y_after_generation(
|
||||||
bbox_string,
|
world_path,
|
||||||
args.scale,
|
bbox_string,
|
||||||
ground.as_ref(),
|
args.scale,
|
||||||
) {
|
ground.as_ref(),
|
||||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
) {
|
||||||
eprintln!("Warning: {}", warning_msg);
|
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||||
#[cfg(feature = "gui")]
|
eprintln!("Warning: {}", warning_msg);
|
||||||
send_log(LogLevel::Warning, &warning_msg);
|
#[cfg(feature = "gui")]
|
||||||
|
send_log(LogLevel::Warning, &warning_msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
120
src/element_processing/advertising.rs
Normal file
120
src/element_processing/advertising.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
//! Processing of advertising elements.
|
||||||
|
//!
|
||||||
|
//! This module handles advertising-related OSM elements including:
|
||||||
|
//! - `advertising=column` - Cylindrical advertising columns (Litfaßsäule)
|
||||||
|
//! - `advertising=flag` - Advertising flags on poles
|
||||||
|
//! - `advertising=poster_box` - Illuminated poster display boxes
|
||||||
|
|
||||||
|
use crate::block_definitions::*;
|
||||||
|
use crate::deterministic_rng::element_rng;
|
||||||
|
use crate::osm_parser::ProcessedNode;
|
||||||
|
use crate::world_editor::WorldEditor;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
/// Generate advertising structures from node elements
|
||||||
|
pub fn generate_advertising(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
// Skip if 'layer' or 'level' is negative in the tags
|
||||||
|
if let Some(layer) = node.tags.get("layer") {
|
||||||
|
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(level) = node.tags.get("level") {
|
||||||
|
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(advertising_type) = node.tags.get("advertising") {
|
||||||
|
match advertising_type.as_str() {
|
||||||
|
"column" => generate_advertising_column(editor, node),
|
||||||
|
"flag" => generate_advertising_flag(editor, node),
|
||||||
|
"poster_box" => generate_poster_box(editor, node),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an advertising column (Litfaßsäule)
|
||||||
|
///
|
||||||
|
/// Creates a simple advertising column.
|
||||||
|
fn generate_advertising_column(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let x = node.x;
|
||||||
|
let z = node.z;
|
||||||
|
|
||||||
|
// Two green concrete blocks stacked
|
||||||
|
editor.set_block(GREEN_CONCRETE, x, 1, z, None, None);
|
||||||
|
editor.set_block(GREEN_CONCRETE, x, 2, z, None, None);
|
||||||
|
|
||||||
|
// Stone brick slab on top
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x, 3, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an advertising flag
|
||||||
|
///
|
||||||
|
/// Creates a flagpole with a banner/flag for advertising.
|
||||||
|
fn generate_advertising_flag(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let x = node.x;
|
||||||
|
let z = node.z;
|
||||||
|
|
||||||
|
// Use deterministic RNG for flag color
|
||||||
|
let mut rng = element_rng(node.id);
|
||||||
|
|
||||||
|
// Get height from tags or default
|
||||||
|
let height = node
|
||||||
|
.tags
|
||||||
|
.get("height")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.unwrap_or(6)
|
||||||
|
.clamp(4, 12);
|
||||||
|
|
||||||
|
// Flagpole
|
||||||
|
for y in 1..=height {
|
||||||
|
editor.set_block(IRON_BARS, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag/banner at top (using colored wool)
|
||||||
|
// Random bright advertising colors
|
||||||
|
let flag_colors = [
|
||||||
|
RED_WOOL,
|
||||||
|
YELLOW_WOOL,
|
||||||
|
BLUE_WOOL,
|
||||||
|
GREEN_WOOL,
|
||||||
|
ORANGE_WOOL,
|
||||||
|
WHITE_WOOL,
|
||||||
|
];
|
||||||
|
let flag_block = flag_colors[rng.gen_range(0..flag_colors.len())];
|
||||||
|
|
||||||
|
// Flag extends to one side (2-3 blocks)
|
||||||
|
let flag_length = 3;
|
||||||
|
for dx in 1..=flag_length {
|
||||||
|
editor.set_block(flag_block, x + dx, height, z, None, None);
|
||||||
|
editor.set_block(flag_block, x + dx, height - 1, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finial at top
|
||||||
|
editor.set_block(IRON_BLOCK, x, height + 1, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a poster box (city light / lollipop display)
|
||||||
|
///
|
||||||
|
/// Creates an illuminated poster display box on a pole.
|
||||||
|
fn generate_poster_box(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let x = node.x;
|
||||||
|
let z = node.z;
|
||||||
|
|
||||||
|
// Y=1: Two iron bars next to each other
|
||||||
|
editor.set_block(IRON_BARS, x, 1, z, None, None);
|
||||||
|
editor.set_block(IRON_BARS, x + 1, 1, z, None, None);
|
||||||
|
|
||||||
|
// Y=2 and Y=3: Two sea lanterns
|
||||||
|
editor.set_block(SEA_LANTERN, x, 2, z, None, None);
|
||||||
|
editor.set_block(SEA_LANTERN, x + 1, 2, z, None, None);
|
||||||
|
editor.set_block(SEA_LANTERN, x, 3, z, None, None);
|
||||||
|
editor.set_block(SEA_LANTERN, x + 1, 3, z, None, None);
|
||||||
|
|
||||||
|
// Y=4: Two polished stone brick slabs
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x + 1, 4, z, None, None);
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
|||||||
use crate::floodfill_cache::FloodFillCache;
|
use crate::floodfill_cache::FloodFillCache;
|
||||||
use crate::osm_parser::ProcessedElement;
|
use crate::osm_parser::ProcessedElement;
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use rand::Rng;
|
use fastnbt::Value;
|
||||||
|
use rand::{seq::SliceRandom, Rng};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
pub fn generate_amenities(
|
pub fn generate_amenities(
|
||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
@@ -34,6 +36,49 @@ pub fn generate_amenities(
|
|||||||
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
||||||
.next();
|
.next();
|
||||||
match amenity_type.as_str() {
|
match amenity_type.as_str() {
|
||||||
|
"recycling" => {
|
||||||
|
let is_container = element
|
||||||
|
.tags()
|
||||||
|
.get("recycling_type")
|
||||||
|
.is_some_and(|value| value == "container");
|
||||||
|
|
||||||
|
if !is_container {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pt) = first_node {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let loot_pool = build_recycling_loot_pool(element.tags());
|
||||||
|
let items = build_recycling_items(&loot_pool, &mut rng);
|
||||||
|
|
||||||
|
let properties = Value::Compound(recycling_barrel_properties());
|
||||||
|
let barrel_block = BlockWithProperties::new(BARREL, Some(properties));
|
||||||
|
let absolute_y = editor.get_absolute_y(pt.x, 1, pt.z);
|
||||||
|
|
||||||
|
editor.set_block_entity_with_items(
|
||||||
|
barrel_block,
|
||||||
|
pt.x,
|
||||||
|
1,
|
||||||
|
pt.z,
|
||||||
|
"minecraft:barrel",
|
||||||
|
items,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(category) = single_loot_category(&loot_pool) {
|
||||||
|
if let Some(display_item) =
|
||||||
|
build_display_item_for_category(category, &mut rng)
|
||||||
|
{
|
||||||
|
place_item_frame_on_random_side(
|
||||||
|
editor,
|
||||||
|
pt.x,
|
||||||
|
absolute_y,
|
||||||
|
pt.z,
|
||||||
|
display_item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"waste_disposal" | "waste_basket" => {
|
"waste_disposal" | "waste_basket" => {
|
||||||
// Place a cauldron for waste disposal or waste basket
|
// Place a cauldron for waste disposal or waste basket
|
||||||
if let Some(pt) = first_node {
|
if let Some(pt) = first_node {
|
||||||
@@ -263,3 +308,420 @@ pub fn generate_amenities(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum RecyclingLootKind {
|
||||||
|
GlassBottle,
|
||||||
|
Paper,
|
||||||
|
GlassBlock,
|
||||||
|
GlassPane,
|
||||||
|
LeatherArmor,
|
||||||
|
EmptyBucket,
|
||||||
|
LeatherBoots,
|
||||||
|
ScrapMetal,
|
||||||
|
GreenWaste,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum LeatherPiece {
|
||||||
|
Helmet,
|
||||||
|
Chestplate,
|
||||||
|
Leggings,
|
||||||
|
Boots,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
enum LootCategory {
|
||||||
|
GlassBottle,
|
||||||
|
Paper,
|
||||||
|
Glass,
|
||||||
|
Leather,
|
||||||
|
EmptyBucket,
|
||||||
|
ScrapMetal,
|
||||||
|
GreenWaste,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recycling_barrel_properties() -> HashMap<String, Value> {
|
||||||
|
let mut props = HashMap::new();
|
||||||
|
props.insert("facing".to_string(), Value::String("up".to_string()));
|
||||||
|
props
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_recycling_loot_pool(tags: &HashMap<String, String>) -> Vec<RecyclingLootKind> {
|
||||||
|
let mut loot_pool: Vec<RecyclingLootKind> = Vec::new();
|
||||||
|
|
||||||
|
if tag_enabled(tags, "recycling:glass_bottles") {
|
||||||
|
loot_pool.push(RecyclingLootKind::GlassBottle);
|
||||||
|
}
|
||||||
|
if tag_enabled(tags, "recycling:paper") {
|
||||||
|
loot_pool.push(RecyclingLootKind::Paper);
|
||||||
|
}
|
||||||
|
if tag_enabled(tags, "recycling:glass") {
|
||||||
|
loot_pool.push(RecyclingLootKind::GlassBlock);
|
||||||
|
loot_pool.push(RecyclingLootKind::GlassPane);
|
||||||
|
}
|
||||||
|
if tag_enabled(tags, "recycling:clothes") {
|
||||||
|
loot_pool.push(RecyclingLootKind::LeatherArmor);
|
||||||
|
}
|
||||||
|
if tag_enabled(tags, "recycling:cans") {
|
||||||
|
loot_pool.push(RecyclingLootKind::EmptyBucket);
|
||||||
|
}
|
||||||
|
if tag_enabled(tags, "recycling:shoes") {
|
||||||
|
loot_pool.push(RecyclingLootKind::LeatherBoots);
|
||||||
|
}
|
||||||
|
if tag_enabled(tags, "recycling:scrap_metal") {
|
||||||
|
loot_pool.push(RecyclingLootKind::ScrapMetal);
|
||||||
|
}
|
||||||
|
if tag_enabled(tags, "recycling:green_waste") {
|
||||||
|
loot_pool.push(RecyclingLootKind::GreenWaste);
|
||||||
|
}
|
||||||
|
|
||||||
|
loot_pool
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_recycling_items(
|
||||||
|
loot_pool: &[RecyclingLootKind],
|
||||||
|
rng: &mut impl Rng,
|
||||||
|
) -> Vec<HashMap<String, Value>> {
|
||||||
|
if loot_pool.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
for slot in 0..27 {
|
||||||
|
if rng.gen_bool(0.2) {
|
||||||
|
let kind = loot_pool[rng.gen_range(0..loot_pool.len())];
|
||||||
|
if let Some(item) = build_item_for_kind(kind, slot as i8, rng) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind_to_category(kind: RecyclingLootKind) -> LootCategory {
|
||||||
|
match kind {
|
||||||
|
RecyclingLootKind::GlassBottle => LootCategory::GlassBottle,
|
||||||
|
RecyclingLootKind::Paper => LootCategory::Paper,
|
||||||
|
RecyclingLootKind::GlassBlock | RecyclingLootKind::GlassPane => LootCategory::Glass,
|
||||||
|
RecyclingLootKind::LeatherArmor | RecyclingLootKind::LeatherBoots => LootCategory::Leather,
|
||||||
|
RecyclingLootKind::EmptyBucket => LootCategory::EmptyBucket,
|
||||||
|
RecyclingLootKind::ScrapMetal => LootCategory::ScrapMetal,
|
||||||
|
RecyclingLootKind::GreenWaste => LootCategory::GreenWaste,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn single_loot_category(loot_pool: &[RecyclingLootKind]) -> Option<LootCategory> {
|
||||||
|
let mut categories: HashSet<LootCategory> = HashSet::new();
|
||||||
|
for kind in loot_pool {
|
||||||
|
categories.insert(kind_to_category(*kind));
|
||||||
|
if categories.len() > 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories.iter().next().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_display_item_for_category(
|
||||||
|
category: LootCategory,
|
||||||
|
rng: &mut impl Rng,
|
||||||
|
) -> Option<HashMap<String, Value>> {
|
||||||
|
match category {
|
||||||
|
LootCategory::GlassBottle => Some(make_display_item("minecraft:glass_bottle", 1)),
|
||||||
|
LootCategory::Paper => Some(make_display_item("minecraft:paper", rng.gen_range(1..=4))),
|
||||||
|
LootCategory::Glass => Some(make_display_item("minecraft:glass", 1)),
|
||||||
|
LootCategory::Leather => Some(build_leather_display_item(rng)),
|
||||||
|
LootCategory::EmptyBucket => Some(make_display_item("minecraft:bucket", 1)),
|
||||||
|
LootCategory::ScrapMetal => {
|
||||||
|
let metals = [
|
||||||
|
"minecraft:copper_ingot",
|
||||||
|
"minecraft:iron_ingot",
|
||||||
|
"minecraft:gold_ingot",
|
||||||
|
];
|
||||||
|
let metal = metals.choose(rng)?;
|
||||||
|
Some(make_display_item(metal, rng.gen_range(1..=2)))
|
||||||
|
}
|
||||||
|
LootCategory::GreenWaste => {
|
||||||
|
let options = [
|
||||||
|
"minecraft:oak_sapling",
|
||||||
|
"minecraft:birch_sapling",
|
||||||
|
"minecraft:tall_grass",
|
||||||
|
"minecraft:sweet_berries",
|
||||||
|
"minecraft:wheat_seeds",
|
||||||
|
];
|
||||||
|
let choice = options.choose(rng)?;
|
||||||
|
Some(make_display_item(choice, rng.gen_range(1..=3)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn place_item_frame_on_random_side(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
x: i32,
|
||||||
|
barrel_absolute_y: i32,
|
||||||
|
z: i32,
|
||||||
|
item: HashMap<String, Value>,
|
||||||
|
) {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let mut directions = [
|
||||||
|
((0, 0, -1), 2), // North
|
||||||
|
((0, 0, 1), 3), // South
|
||||||
|
((-1, 0, 0), 4), // West
|
||||||
|
((1, 0, 0), 5), // East
|
||||||
|
];
|
||||||
|
directions.shuffle(&mut rng);
|
||||||
|
|
||||||
|
let (min_x, min_z) = editor.get_min_coords();
|
||||||
|
let (max_x, max_z) = editor.get_max_coords();
|
||||||
|
|
||||||
|
let ((dx, _dy, dz), facing) = directions
|
||||||
|
.into_iter()
|
||||||
|
.find(|((dx, _dy, dz), _)| {
|
||||||
|
let target_x = x + dx;
|
||||||
|
let target_z = z + dz;
|
||||||
|
target_x >= min_x && target_x <= max_x && target_z >= min_z && target_z <= max_z
|
||||||
|
})
|
||||||
|
.unwrap_or(((0, 0, 1), 3)); // Fallback south if all directions are out of bounds
|
||||||
|
|
||||||
|
let target_x = x + dx;
|
||||||
|
let target_y = barrel_absolute_y;
|
||||||
|
let target_z = z + dz;
|
||||||
|
|
||||||
|
let ground_y = editor.get_absolute_y(target_x, 0, target_z);
|
||||||
|
|
||||||
|
let mut extra = HashMap::new();
|
||||||
|
extra.insert("Facing".to_string(), Value::Byte(facing)); // 2=north, 3=south, 4=west, 5=east
|
||||||
|
extra.insert("ItemRotation".to_string(), Value::Byte(0));
|
||||||
|
extra.insert("Item".to_string(), Value::Compound(item));
|
||||||
|
extra.insert("ItemDropChance".to_string(), Value::Float(1.0));
|
||||||
|
extra.insert(
|
||||||
|
"block_pos".to_string(),
|
||||||
|
Value::List(vec![
|
||||||
|
Value::Int(target_x),
|
||||||
|
Value::Int(target_y),
|
||||||
|
Value::Int(target_z),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
extra.insert("TileX".to_string(), Value::Int(target_x));
|
||||||
|
extra.insert("TileY".to_string(), Value::Int(target_y));
|
||||||
|
extra.insert("TileZ".to_string(), Value::Int(target_z));
|
||||||
|
extra.insert("Fixed".to_string(), Value::Byte(1));
|
||||||
|
|
||||||
|
let relative_y = target_y - ground_y;
|
||||||
|
editor.add_entity(
|
||||||
|
"minecraft:item_frame",
|
||||||
|
target_x,
|
||||||
|
relative_y,
|
||||||
|
target_z,
|
||||||
|
Some(extra),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_display_item(id: &str, count: i8) -> HashMap<String, Value> {
|
||||||
|
let mut item = HashMap::new();
|
||||||
|
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||||
|
item.insert("Count".to_string(), Value::Byte(count));
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_leather_display_item(rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||||
|
let mut item = make_display_item("minecraft:leather_chestplate", 1);
|
||||||
|
let damage = biased_damage(80, rng);
|
||||||
|
|
||||||
|
let mut tag = HashMap::new();
|
||||||
|
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||||
|
|
||||||
|
if let Some(color) = maybe_leather_color(rng) {
|
||||||
|
let mut display = HashMap::new();
|
||||||
|
display.insert("color".to_string(), Value::Int(color));
|
||||||
|
tag.insert("display".to_string(), Value::Compound(display));
|
||||||
|
}
|
||||||
|
|
||||||
|
item.insert("tag".to_string(), Value::Compound(tag));
|
||||||
|
|
||||||
|
let mut components = HashMap::new();
|
||||||
|
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||||
|
item.insert("components".to_string(), Value::Compound(components));
|
||||||
|
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_item_for_kind(
|
||||||
|
kind: RecyclingLootKind,
|
||||||
|
slot: i8,
|
||||||
|
rng: &mut impl Rng,
|
||||||
|
) -> Option<HashMap<String, Value>> {
|
||||||
|
match kind {
|
||||||
|
RecyclingLootKind::GlassBottle => Some(make_basic_item(
|
||||||
|
"minecraft:glass_bottle",
|
||||||
|
slot,
|
||||||
|
rng.gen_range(1..=4),
|
||||||
|
)),
|
||||||
|
RecyclingLootKind::Paper => Some(make_basic_item(
|
||||||
|
"minecraft:paper",
|
||||||
|
slot,
|
||||||
|
rng.gen_range(1..=10),
|
||||||
|
)),
|
||||||
|
RecyclingLootKind::GlassBlock => Some(build_glass_item(false, slot, rng)),
|
||||||
|
RecyclingLootKind::GlassPane => Some(build_glass_item(true, slot, rng)),
|
||||||
|
RecyclingLootKind::LeatherArmor => {
|
||||||
|
Some(build_leather_item(random_leather_piece(rng), slot, rng))
|
||||||
|
}
|
||||||
|
RecyclingLootKind::EmptyBucket => Some(make_basic_item("minecraft:bucket", slot, 1)),
|
||||||
|
RecyclingLootKind::LeatherBoots => Some(build_leather_item(LeatherPiece::Boots, slot, rng)),
|
||||||
|
RecyclingLootKind::ScrapMetal => Some(build_scrap_metal_item(slot, rng)),
|
||||||
|
RecyclingLootKind::GreenWaste => Some(build_green_waste_item(slot, rng)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_scrap_metal_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||||
|
let metals = ["copper_ingot", "iron_ingot", "gold_ingot"];
|
||||||
|
let metal = metals.choose(rng).expect("scrap metal list is non-empty");
|
||||||
|
let count = rng.gen_range(1..=3);
|
||||||
|
make_basic_item(&format!("minecraft:{metal}"), slot, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_green_waste_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||||
|
#[allow(clippy::match_same_arms)]
|
||||||
|
let (id, count) = match rng.gen_range(0..8) {
|
||||||
|
0 => ("minecraft:tall_grass", rng.gen_range(1..=4)),
|
||||||
|
1 => ("minecraft:sweet_berries", rng.gen_range(2..=6)),
|
||||||
|
2 => ("minecraft:oak_sapling", rng.gen_range(1..=2)),
|
||||||
|
3 => ("minecraft:birch_sapling", rng.gen_range(1..=2)),
|
||||||
|
4 => ("minecraft:spruce_sapling", rng.gen_range(1..=2)),
|
||||||
|
5 => ("minecraft:jungle_sapling", rng.gen_range(1..=2)),
|
||||||
|
6 => ("minecraft:acacia_sapling", rng.gen_range(1..=2)),
|
||||||
|
_ => ("minecraft:dark_oak_sapling", rng.gen_range(1..=2)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 25% chance to replace with seeds instead
|
||||||
|
let id = if rng.gen_bool(0.25) {
|
||||||
|
match rng.gen_range(0..4) {
|
||||||
|
0 => "minecraft:wheat_seeds",
|
||||||
|
1 => "minecraft:pumpkin_seeds",
|
||||||
|
2 => "minecraft:melon_seeds",
|
||||||
|
_ => "minecraft:beetroot_seeds",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id
|
||||||
|
};
|
||||||
|
|
||||||
|
make_basic_item(id, slot, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||||
|
const GLASS_COLORS: &[&str] = &[
|
||||||
|
"white",
|
||||||
|
"orange",
|
||||||
|
"magenta",
|
||||||
|
"light_blue",
|
||||||
|
"yellow",
|
||||||
|
"lime",
|
||||||
|
"pink",
|
||||||
|
"gray",
|
||||||
|
"light_gray",
|
||||||
|
"cyan",
|
||||||
|
"purple",
|
||||||
|
"blue",
|
||||||
|
"brown",
|
||||||
|
"green",
|
||||||
|
"red",
|
||||||
|
"black",
|
||||||
|
];
|
||||||
|
|
||||||
|
let use_colorless = rng.gen_bool(0.7);
|
||||||
|
|
||||||
|
let id = if use_colorless {
|
||||||
|
if is_pane {
|
||||||
|
"minecraft:glass_pane".to_string()
|
||||||
|
} else {
|
||||||
|
"minecraft:glass".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let color = GLASS_COLORS
|
||||||
|
.choose(rng)
|
||||||
|
.expect("glass color array is non-empty");
|
||||||
|
if is_pane {
|
||||||
|
format!("minecraft:{color}_stained_glass_pane")
|
||||||
|
} else {
|
||||||
|
format!("minecraft:{color}_stained_glass")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let count = if is_pane {
|
||||||
|
rng.gen_range(4..=16)
|
||||||
|
} else {
|
||||||
|
rng.gen_range(1..=6)
|
||||||
|
};
|
||||||
|
|
||||||
|
make_basic_item(&id, slot, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_leather_item(piece: LeatherPiece, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||||
|
let (id, max_damage) = match piece {
|
||||||
|
LeatherPiece::Helmet => ("minecraft:leather_helmet", 55),
|
||||||
|
LeatherPiece::Chestplate => ("minecraft:leather_chestplate", 80),
|
||||||
|
LeatherPiece::Leggings => ("minecraft:leather_leggings", 75),
|
||||||
|
LeatherPiece::Boots => ("minecraft:leather_boots", 65),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut item = make_basic_item(id, slot, 1);
|
||||||
|
let damage = biased_damage(max_damage, rng);
|
||||||
|
|
||||||
|
let mut tag = HashMap::new();
|
||||||
|
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||||
|
|
||||||
|
if let Some(color) = maybe_leather_color(rng) {
|
||||||
|
let mut display = HashMap::new();
|
||||||
|
display.insert("color".to_string(), Value::Int(color));
|
||||||
|
tag.insert("display".to_string(), Value::Compound(display));
|
||||||
|
}
|
||||||
|
|
||||||
|
item.insert("tag".to_string(), Value::Compound(tag));
|
||||||
|
|
||||||
|
let mut components = HashMap::new();
|
||||||
|
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||||
|
item.insert("components".to_string(), Value::Compound(components));
|
||||||
|
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
|
fn biased_damage(max_damage: i32, rng: &mut impl Rng) -> i32 {
|
||||||
|
let safe_max = max_damage.max(1);
|
||||||
|
let upper = safe_max.saturating_sub(1);
|
||||||
|
let lower = (safe_max / 2).min(upper);
|
||||||
|
|
||||||
|
let heavy_wear = rng.gen_range(lower..=upper);
|
||||||
|
let random_wear = rng.gen_range(0..=upper);
|
||||||
|
heavy_wear.max(random_wear)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_leather_color(rng: &mut impl Rng) -> Option<i32> {
|
||||||
|
if rng.gen_bool(0.3) {
|
||||||
|
Some(rng.gen_range(0..=0x00FF_FFFF))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_leather_piece(rng: &mut impl Rng) -> LeatherPiece {
|
||||||
|
match rng.gen_range(0..4) {
|
||||||
|
0 => LeatherPiece::Helmet,
|
||||||
|
1 => LeatherPiece::Chestplate,
|
||||||
|
2 => LeatherPiece::Leggings,
|
||||||
|
_ => LeatherPiece::Boots,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_basic_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||||
|
let mut item = HashMap::new();
|
||||||
|
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||||
|
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||||
|
item.insert("Count".to_string(), Value::Byte(count));
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_enabled(tags: &HashMap<String, String>, key: &str) -> bool {
|
||||||
|
tags.get(key).is_some_and(|value| value == "yes")
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
|
|||||||
barrier_material = LIGHT_GRAY_CONCRETE;
|
barrier_material = LIGHT_GRAY_CONCRETE;
|
||||||
}
|
}
|
||||||
if barrier_mat == "metal" {
|
if barrier_mat == "metal" {
|
||||||
barrier_material = STONE_BRICK_WALL; // IRON_BARS
|
barrier_material = STONE_BRICK_WALL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +80,8 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
|
|||||||
.get("height")
|
.get("height")
|
||||||
.and_then(|height: &String| height.parse::<f32>().ok())
|
.and_then(|height: &String| height.parse::<f32>().ok())
|
||||||
.map(|height: f32| height.round() as i32)
|
.map(|height: f32| height.round() as i32)
|
||||||
.unwrap_or(barrier_height);
|
.unwrap_or(barrier_height)
|
||||||
|
.max(2); // Minimum height of 2
|
||||||
|
|
||||||
// Process nodes to create the barrier wall
|
// Process nodes to create the barrier wall
|
||||||
for i in 1..way.nodes.len() {
|
for i in 1..way.nodes.len() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
55
src/element_processing/emergency.rs
Normal file
55
src/element_processing/emergency.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//! Processing of emergency infrastructure elements.
|
||||||
|
//!
|
||||||
|
//! This module handles emergency-related OSM elements including:
|
||||||
|
//! - `emergency=fire_hydrant` - Fire hydrants
|
||||||
|
|
||||||
|
use crate::block_definitions::*;
|
||||||
|
use crate::osm_parser::ProcessedNode;
|
||||||
|
use crate::world_editor::WorldEditor;
|
||||||
|
|
||||||
|
/// Generate emergency infrastructure from node elements
|
||||||
|
pub fn generate_emergency(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
// Skip if 'layer' or 'level' is negative in the tags
|
||||||
|
if let Some(layer) = node.tags.get("layer") {
|
||||||
|
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(level) = node.tags.get("level") {
|
||||||
|
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(emergency_type) = node.tags.get("emergency") {
|
||||||
|
if emergency_type.as_str() == "fire_hydrant" {
|
||||||
|
generate_fire_hydrant(editor, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a fire hydrant
|
||||||
|
///
|
||||||
|
/// Creates a simple fire hydrant structure using brick wall with redstone block on top.
|
||||||
|
/// Skips underground, wall-mounted, and pond hydrant types.
|
||||||
|
fn generate_fire_hydrant(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let x = node.x;
|
||||||
|
let z = node.z;
|
||||||
|
|
||||||
|
// Get hydrant type - skip underground, wall, and pond types
|
||||||
|
let hydrant_type = node
|
||||||
|
.tags
|
||||||
|
.get("fire_hydrant:type")
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("pillar");
|
||||||
|
|
||||||
|
// Skip non-visible hydrant types
|
||||||
|
if matches!(hydrant_type, "underground" | "wall" | "pond") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple hydrant: brick wall with redstone block on top
|
||||||
|
editor.set_block(BRICK_WALL, x, 1, z, None, None);
|
||||||
|
editor.set_block(REDSTONE_BLOCK, x, 2, z, None, None);
|
||||||
|
}
|
||||||
207
src/element_processing/historic.rs
Normal file
207
src/element_processing/historic.rs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
//! Processing of historic elements.
|
||||||
|
//!
|
||||||
|
//! This module handles historic OSM elements including:
|
||||||
|
//! - `historic=memorial` - Memorials, monuments, and commemorative structures
|
||||||
|
|
||||||
|
use crate::block_definitions::*;
|
||||||
|
use crate::deterministic_rng::element_rng;
|
||||||
|
use crate::osm_parser::ProcessedNode;
|
||||||
|
use crate::world_editor::WorldEditor;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
/// Generate historic structures from node elements
|
||||||
|
pub fn generate_historic(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
// Skip if 'layer' or 'level' is negative in the tags
|
||||||
|
if let Some(layer) = node.tags.get("layer") {
|
||||||
|
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(level) = node.tags.get("level") {
|
||||||
|
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(historic_type) = node.tags.get("historic") {
|
||||||
|
match historic_type.as_str() {
|
||||||
|
"memorial" => generate_memorial(editor, node),
|
||||||
|
"monument" => generate_monument(editor, node),
|
||||||
|
"wayside_cross" => generate_wayside_cross(editor, node),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a memorial structure
|
||||||
|
///
|
||||||
|
/// Memorials come in many forms. We determine the type from the `memorial` tag:
|
||||||
|
/// - plaque: Simple wall-mounted or standing plaque
|
||||||
|
/// - statue: A statue on a pedestal
|
||||||
|
/// - sculpture: Artistic sculpture
|
||||||
|
/// - stone/stolperstein: Memorial stone
|
||||||
|
/// - bench: Memorial bench (already handled by amenity=bench typically)
|
||||||
|
/// - cross: Memorial cross
|
||||||
|
/// - obelisk: Tall pointed pillar
|
||||||
|
/// - stele: Upright stone slab
|
||||||
|
/// - bust: Bust on a pedestal
|
||||||
|
/// - Default: A general monument/pillar
|
||||||
|
fn generate_memorial(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let x = node.x;
|
||||||
|
let z = node.z;
|
||||||
|
|
||||||
|
// Use deterministic RNG for consistent results
|
||||||
|
let mut rng = element_rng(node.id);
|
||||||
|
|
||||||
|
// Get memorial subtype
|
||||||
|
let memorial_type = node
|
||||||
|
.tags
|
||||||
|
.get("memorial")
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("yes");
|
||||||
|
|
||||||
|
match memorial_type {
|
||||||
|
"plaque" => {
|
||||||
|
// Simple plaque on a small stand
|
||||||
|
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x, 2, z, None, None);
|
||||||
|
}
|
||||||
|
"statue" | "sculpture" | "bust" => {
|
||||||
|
// Statue on a pedestal
|
||||||
|
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||||
|
editor.set_block(CHISELED_STONE_BRICKS, x, 2, z, None, None);
|
||||||
|
|
||||||
|
// Use polished andesite for bronze/metal statue appearance
|
||||||
|
let statue_block = if rng.gen_bool(0.5) {
|
||||||
|
POLISHED_ANDESITE
|
||||||
|
} else {
|
||||||
|
POLISHED_DIORITE
|
||||||
|
};
|
||||||
|
editor.set_block(statue_block, x, 3, z, None, None);
|
||||||
|
editor.set_block(statue_block, x, 4, z, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_WALL, x, 5, z, None, None);
|
||||||
|
}
|
||||||
|
"stone" | "stolperstein" => {
|
||||||
|
// Simple memorial stone embedded in ground
|
||||||
|
let stone_block = if memorial_type == "stolperstein" {
|
||||||
|
GOLD_BLOCK // Stolpersteine are brass/gold colored
|
||||||
|
} else {
|
||||||
|
STONE
|
||||||
|
};
|
||||||
|
editor.set_block(stone_block, x, 0, z, None, None);
|
||||||
|
}
|
||||||
|
"cross" | "war_memorial" => {
|
||||||
|
// Memorial cross
|
||||||
|
generate_cross(editor, x, z, 5);
|
||||||
|
}
|
||||||
|
"obelisk" => {
|
||||||
|
// Tall pointed pillar with fixed height
|
||||||
|
// Base layer at Y=1
|
||||||
|
for dx in -1..=1 {
|
||||||
|
for dz in -1..=1 {
|
||||||
|
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second base layer at Y=2
|
||||||
|
for dx in -1..=1 {
|
||||||
|
for dz in -1..=1 {
|
||||||
|
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Stone brick slabs on the 4 corners at Y=3 (on top of corner blocks)
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z - 1, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z - 1, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z + 1, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z + 1, None, None);
|
||||||
|
|
||||||
|
// Main shaft, fixed height of 4 blocks (Y=3 to Y=6)
|
||||||
|
for y in 3..=6 {
|
||||||
|
editor.set_block(SMOOTH_QUARTZ, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x, 7, z, None, None);
|
||||||
|
}
|
||||||
|
"stele" => {
|
||||||
|
// Upright stone slab
|
||||||
|
// Base
|
||||||
|
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||||
|
|
||||||
|
// Upright slab (using wall blocks for thin appearance)
|
||||||
|
for y in 2..=4 {
|
||||||
|
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x, 5, z, None, None);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Default: simple stone pillar monument
|
||||||
|
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||||
|
editor.set_block(STONE_BRICKS, x, 2, z, None, None);
|
||||||
|
editor.set_block(CHISELED_STONE_BRICKS, x, 3, z, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a monument (larger than memorial)
|
||||||
|
fn generate_monument(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let x = node.x;
|
||||||
|
let z = node.z;
|
||||||
|
|
||||||
|
// Monuments are typically larger structures
|
||||||
|
let height = node
|
||||||
|
.tags
|
||||||
|
.get("height")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.unwrap_or(10)
|
||||||
|
.clamp(5, 20);
|
||||||
|
|
||||||
|
// Large base platform
|
||||||
|
for dx in -2..=2 {
|
||||||
|
for dz in -2..=2 {
|
||||||
|
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for dx in -1..=1 {
|
||||||
|
for dz in -1..=1 {
|
||||||
|
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main structure
|
||||||
|
for y in 3..height {
|
||||||
|
editor.set_block(POLISHED_ANDESITE, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorative top
|
||||||
|
editor.set_block(CHISELED_STONE_BRICKS, x, height, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a wayside cross
|
||||||
|
fn generate_wayside_cross(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let x = node.x;
|
||||||
|
let z = node.z;
|
||||||
|
|
||||||
|
// Simple roadside cross
|
||||||
|
generate_cross(editor, x, z, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to generate a cross structure
|
||||||
|
fn generate_cross(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
|
||||||
|
// Base
|
||||||
|
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||||
|
|
||||||
|
// Vertical beam
|
||||||
|
for y in 2..=height {
|
||||||
|
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal beam (cross arm) at approximately 2/3 height, but at least 2 and at most height-1
|
||||||
|
let arm_y = ((height * 2 + 2) / 3).clamp(2, height - 1);
|
||||||
|
// Only place horizontal arms if height allows for them (height >= 3)
|
||||||
|
if height >= 3 {
|
||||||
|
editor.set_block(STONE_BRICK_WALL, x - 1, arm_y, z, None, None);
|
||||||
|
editor.set_block(STONE_BRICK_WALL, x + 1, arm_y, z, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::deterministic_rng::element_rng;
|
use crate::deterministic_rng::element_rng;
|
||||||
use crate::element_processing::tree::Tree;
|
use crate::element_processing::tree::{Tree, TreeType};
|
||||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
pub fn generate_landuse(
|
pub fn generate_landuse(
|
||||||
@@ -58,6 +59,29 @@ pub fn generate_landuse(
|
|||||||
let floor_area: Vec<(i32, i32)> =
|
let floor_area: Vec<(i32, i32)> =
|
||||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||||
|
|
||||||
|
let trees_ok_to_generate: Vec<TreeType> = {
|
||||||
|
let mut trees: Vec<TreeType> = vec![];
|
||||||
|
if let Some(leaf_type) = element.tags.get("leaf_type") {
|
||||||
|
match leaf_type.as_str() {
|
||||||
|
"broadleaved" => {
|
||||||
|
trees.push(TreeType::Oak);
|
||||||
|
trees.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
"needleleaved" => trees.push(TreeType::Spruce),
|
||||||
|
_ => {
|
||||||
|
trees.push(TreeType::Oak);
|
||||||
|
trees.push(TreeType::Spruce);
|
||||||
|
trees.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trees.push(TreeType::Oak);
|
||||||
|
trees.push(TreeType::Spruce);
|
||||||
|
trees.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
trees
|
||||||
|
};
|
||||||
|
|
||||||
for (x, z) in floor_area {
|
for (x, z) in floor_area {
|
||||||
// Apply per-block randomness for certain landuse types
|
// Apply per-block randomness for certain landuse types
|
||||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||||
@@ -134,6 +158,11 @@ pub fn generate_landuse(
|
|||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} else if random_choice < 35 {
|
} else if random_choice < 35 {
|
||||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||||
|
} else if random_choice < 37 {
|
||||||
|
editor.set_block(FERN, x, 1, z, None, None);
|
||||||
|
} else if random_choice < 41 {
|
||||||
|
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
|
||||||
|
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,13 +170,22 @@ pub fn generate_landuse(
|
|||||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||||
let random_choice: i32 = rng.gen_range(0..30);
|
let random_choice: i32 = rng.gen_range(0..30);
|
||||||
if random_choice == 20 {
|
if random_choice == 20 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
let tree_type = *trees_ok_to_generate
|
||||||
|
.choose(&mut rng)
|
||||||
|
.unwrap_or(&TreeType::Oak);
|
||||||
|
Tree::create_of_type(
|
||||||
|
editor,
|
||||||
|
(x, 1, z),
|
||||||
|
tree_type,
|
||||||
|
Some(building_footprints),
|
||||||
|
);
|
||||||
} else if random_choice == 2 {
|
} else if random_choice == 2 {
|
||||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
let flower_block: Block = match rng.gen_range(1..=6) {
|
||||||
1 => OAK_LEAVES,
|
1 => OAK_LEAVES,
|
||||||
2 => RED_FLOWER,
|
2 => RED_FLOWER,
|
||||||
3 => BLUE_FLOWER,
|
3 => BLUE_FLOWER,
|
||||||
4 => YELLOW_FLOWER,
|
4 => YELLOW_FLOWER,
|
||||||
|
5 => FERN,
|
||||||
_ => WHITE_FLOWER,
|
_ => WHITE_FLOWER,
|
||||||
};
|
};
|
||||||
editor.set_block(flower_block, x, 1, z, None, None);
|
editor.set_block(flower_block, x, 1, z, None, None);
|
||||||
@@ -269,7 +307,7 @@ pub fn generate_landuse(
|
|||||||
match rng.gen_range(0..200) {
|
match rng.gen_range(0..200) {
|
||||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||||
3..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
3..=16 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,7 +323,10 @@ pub fn generate_landuse(
|
|||||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||||
} else if random_choice < 40 {
|
} else if random_choice < 40 {
|
||||||
editor.set_block(FERN, x, 1, z, None, None);
|
editor.set_block(FERN, x, 1, z, None, None);
|
||||||
} else if random_choice < 800 {
|
} else if random_choice < 65 {
|
||||||
|
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
|
||||||
|
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
|
||||||
|
} else if random_choice < 825 {
|
||||||
editor.set_block(GRASS, x, 1, z, None, None);
|
editor.set_block(GRASS, x, 1, z, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,44 +376,53 @@ pub fn generate_landuse_from_relation(
|
|||||||
building_footprints: &BuildingFootprintBitmap,
|
building_footprints: &BuildingFootprintBitmap,
|
||||||
) {
|
) {
|
||||||
if rel.tags.contains_key("landuse") {
|
if rel.tags.contains_key("landuse") {
|
||||||
// Generate individual ways with their original tags
|
// Process each outer member way individually using cached flood fill.
|
||||||
|
// We intentionally do not combine all outer nodes into one mega-way,
|
||||||
|
// because that creates a nonsensical polygon spanning the whole relation
|
||||||
|
// extent, misses the flood fill cache, and can cause multi-GB allocations.
|
||||||
for member in &rel.members {
|
for member in &rel.members {
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
|
// Use relation tags so the member inherits the relation's landuse=* type
|
||||||
|
let way_with_rel_tags = ProcessedWay {
|
||||||
|
id: member.way.id,
|
||||||
|
nodes: member.way.nodes.clone(),
|
||||||
|
tags: rel.tags.clone(),
|
||||||
|
};
|
||||||
generate_landuse(
|
generate_landuse(
|
||||||
editor,
|
editor,
|
||||||
&member.way.clone(),
|
&way_with_rel_tags,
|
||||||
args,
|
args,
|
||||||
flood_fill_cache,
|
flood_fill_cache,
|
||||||
building_footprints,
|
building_footprints,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Combine all outer ways into one with relation tags
|
}
|
||||||
let mut combined_nodes = Vec::new();
|
|
||||||
for member in &rel.members {
|
/// Generates ground blocks for place=* areas (squares, neighbourhoods, etc.)
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
pub fn generate_place(
|
||||||
combined_nodes.extend(member.way.nodes.clone());
|
editor: &mut WorldEditor,
|
||||||
}
|
element: &ProcessedWay,
|
||||||
}
|
args: &Args,
|
||||||
|
flood_fill_cache: &FloodFillCache,
|
||||||
// Only process if we have nodes
|
) {
|
||||||
if !combined_nodes.is_empty() {
|
let binding = String::new();
|
||||||
// Create combined way with relation tags
|
let place_tag = element.tags.get("place").unwrap_or(&binding);
|
||||||
let combined_way = ProcessedWay {
|
|
||||||
id: rel.id,
|
// Determine block type based on place tag
|
||||||
nodes: combined_nodes,
|
let block_type = match place_tag.as_str() {
|
||||||
tags: rel.tags.clone(),
|
"square" => STONE_BRICKS,
|
||||||
};
|
"neighbourhood" | "city_block" | "quarter" | "suburb" => SMOOTH_STONE,
|
||||||
|
_ => return,
|
||||||
// Generate landuse area from combined way
|
};
|
||||||
generate_landuse(
|
|
||||||
editor,
|
// Get the area using flood fill cache
|
||||||
&combined_way,
|
let floor_area: Vec<(i32, i32)> =
|
||||||
args,
|
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
// Place ground blocks
|
||||||
);
|
for (x, z) in floor_area {
|
||||||
}
|
editor.set_block(block_type, x, 0, z, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,14 +100,16 @@ pub fn generate_leisure(
|
|||||||
|
|
||||||
match random_choice {
|
match random_choice {
|
||||||
0..30 => {
|
0..30 => {
|
||||||
// Flowers
|
// Plants
|
||||||
let flower_choice = match random_choice {
|
let plant_choice = match random_choice {
|
||||||
0..10 => RED_FLOWER,
|
0..5 => RED_FLOWER,
|
||||||
10..20 => YELLOW_FLOWER,
|
5..10 => YELLOW_FLOWER,
|
||||||
20..30 => BLUE_FLOWER,
|
10..16 => BLUE_FLOWER,
|
||||||
_ => WHITE_FLOWER,
|
16..22 => WHITE_FLOWER,
|
||||||
|
22..30 => FERN,
|
||||||
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
editor.set_block(flower_choice, x, 1, z, None, None);
|
editor.set_block(plant_choice, x, 1, z, None, None);
|
||||||
}
|
}
|
||||||
30..90 => {
|
30..90 => {
|
||||||
// Grass
|
// Grass
|
||||||
@@ -183,41 +185,26 @@ pub fn generate_leisure_from_relation(
|
|||||||
building_footprints: &BuildingFootprintBitmap,
|
building_footprints: &BuildingFootprintBitmap,
|
||||||
) {
|
) {
|
||||||
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||||
// First generate individual ways with their original tags
|
// Process each outer member way individually using cached flood fill.
|
||||||
|
// We intentionally do not combine all outer nodes into one mega-way,
|
||||||
|
// because that creates a nonsensical polygon spanning the whole relation
|
||||||
|
// extent, misses the flood fill cache, and can cause multi-GB allocations.
|
||||||
for member in &rel.members {
|
for member in &rel.members {
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
|
// Use relation tags so the member inherits the relation's leisure=* type
|
||||||
|
let way_with_rel_tags = ProcessedWay {
|
||||||
|
id: member.way.id,
|
||||||
|
nodes: member.way.nodes.clone(),
|
||||||
|
tags: rel.tags.clone(),
|
||||||
|
};
|
||||||
generate_leisure(
|
generate_leisure(
|
||||||
editor,
|
editor,
|
||||||
&member.way,
|
&way_with_rel_tags,
|
||||||
args,
|
args,
|
||||||
flood_fill_cache,
|
flood_fill_cache,
|
||||||
building_footprints,
|
building_footprints,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then combine all outer ways into one
|
|
||||||
let mut combined_nodes = Vec::new();
|
|
||||||
for member in &rel.members {
|
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
|
||||||
combined_nodes.extend(member.way.nodes.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create combined way with relation tags
|
|
||||||
let combined_way = ProcessedWay {
|
|
||||||
id: rel.id,
|
|
||||||
nodes: combined_nodes,
|
|
||||||
tags: rel.tags.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate leisure area from combined way
|
|
||||||
generate_leisure(
|
|
||||||
editor,
|
|
||||||
&combined_way,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,122 @@
|
|||||||
|
pub mod advertising;
|
||||||
pub mod amenities;
|
pub mod amenities;
|
||||||
pub mod barriers;
|
pub mod barriers;
|
||||||
pub mod bridges;
|
pub mod bridges;
|
||||||
pub mod buildings;
|
pub mod buildings;
|
||||||
pub mod doors;
|
pub mod doors;
|
||||||
|
pub mod emergency;
|
||||||
pub mod highways;
|
pub mod highways;
|
||||||
|
pub mod historic;
|
||||||
pub mod landuse;
|
pub mod landuse;
|
||||||
pub mod leisure;
|
pub mod leisure;
|
||||||
pub mod man_made;
|
pub mod man_made;
|
||||||
pub mod natural;
|
pub mod natural;
|
||||||
|
pub mod power;
|
||||||
pub mod railways;
|
pub mod railways;
|
||||||
pub mod subprocessor;
|
pub mod subprocessor;
|
||||||
pub mod tourisms;
|
pub mod tourisms;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
pub mod water_areas;
|
pub mod water_areas;
|
||||||
pub mod waterways;
|
pub mod waterways;
|
||||||
|
|
||||||
|
use crate::osm_parser::ProcessedNode;
|
||||||
|
|
||||||
|
/// Merges way segments that share endpoints into closed rings.
|
||||||
|
/// Used by water_areas.rs and boundaries.rs for assembling relation members.
|
||||||
|
pub fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
||||||
|
let mut removed: Vec<usize> = vec![];
|
||||||
|
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||||
|
|
||||||
|
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
||||||
|
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||||
|
if a.id == b.id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let dx = (a.x - b.x).abs();
|
||||||
|
let dz = (a.z - b.z).abs();
|
||||||
|
dx <= 1 && dz <= 1
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..rings.len() {
|
||||||
|
for j in 0..rings.len() {
|
||||||
|
if i == j {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if removed.contains(&i) || removed.contains(&j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x: &Vec<ProcessedNode> = &rings[i];
|
||||||
|
let y: &Vec<ProcessedNode> = &rings[j];
|
||||||
|
|
||||||
|
// Skip empty rings (can happen after clipping)
|
||||||
|
if x.is_empty() || y.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x_first = &x[0];
|
||||||
|
let x_last = x.last().unwrap();
|
||||||
|
let y_first = &y[0];
|
||||||
|
let y_last = y.last().unwrap();
|
||||||
|
|
||||||
|
// Skip already-closed rings
|
||||||
|
if nodes_match(x_first, x_last) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes_match(y_first, y_last) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes_match(x_first, y_first) {
|
||||||
|
removed.push(i);
|
||||||
|
removed.push(j);
|
||||||
|
|
||||||
|
let mut x: Vec<ProcessedNode> = x.clone();
|
||||||
|
x.reverse();
|
||||||
|
x.extend(y.iter().skip(1).cloned());
|
||||||
|
merged.push(x);
|
||||||
|
} else if nodes_match(x_last, y_last) {
|
||||||
|
removed.push(i);
|
||||||
|
removed.push(j);
|
||||||
|
|
||||||
|
let mut x: Vec<ProcessedNode> = x.clone();
|
||||||
|
x.extend(y.iter().rev().skip(1).cloned());
|
||||||
|
|
||||||
|
merged.push(x);
|
||||||
|
} else if nodes_match(x_first, y_last) {
|
||||||
|
removed.push(i);
|
||||||
|
removed.push(j);
|
||||||
|
|
||||||
|
let mut y: Vec<ProcessedNode> = y.clone();
|
||||||
|
y.extend(x.iter().skip(1).cloned());
|
||||||
|
|
||||||
|
merged.push(y);
|
||||||
|
} else if nodes_match(x_last, y_first) {
|
||||||
|
removed.push(i);
|
||||||
|
removed.push(j);
|
||||||
|
|
||||||
|
let mut x: Vec<ProcessedNode> = x.clone();
|
||||||
|
x.extend(y.iter().skip(1).cloned());
|
||||||
|
|
||||||
|
merged.push(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removed.sort();
|
||||||
|
|
||||||
|
for r in removed.iter().rev() {
|
||||||
|
rings.remove(*r);
|
||||||
|
}
|
||||||
|
|
||||||
|
let merged_len: usize = merged.len();
|
||||||
|
for m in merged {
|
||||||
|
rings.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
if merged_len > 0 {
|
||||||
|
merge_way_segments(rings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use crate::args::Args;
|
|||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::bresenham::bresenham_line;
|
use crate::bresenham::bresenham_line;
|
||||||
use crate::deterministic_rng::element_rng;
|
use crate::deterministic_rng::element_rng;
|
||||||
use crate::element_processing::tree::Tree;
|
use crate::element_processing::tree::{Tree, TreeType};
|
||||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
pub fn generate_natural(
|
pub fn generate_natural(
|
||||||
@@ -21,7 +22,66 @@ pub fn generate_natural(
|
|||||||
let x: i32 = node.x;
|
let x: i32 = node.x;
|
||||||
let z: i32 = node.z;
|
let z: i32 = node.z;
|
||||||
|
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
let mut trees_ok_to_generate: Vec<TreeType> = vec![];
|
||||||
|
if let Some(species) = element.tags().get("species") {
|
||||||
|
if species.contains("Betula") {
|
||||||
|
trees_ok_to_generate.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
if species.contains("Quercus") {
|
||||||
|
trees_ok_to_generate.push(TreeType::Oak);
|
||||||
|
}
|
||||||
|
if species.contains("Picea") {
|
||||||
|
trees_ok_to_generate.push(TreeType::Spruce);
|
||||||
|
}
|
||||||
|
} else if let Some(genus_wikidata) = element.tags().get("genus:wikidata") {
|
||||||
|
match genus_wikidata.as_str() {
|
||||||
|
"Q12004" => trees_ok_to_generate.push(TreeType::Birch),
|
||||||
|
"Q26782" => trees_ok_to_generate.push(TreeType::Oak),
|
||||||
|
"Q25243" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||||
|
_ => {
|
||||||
|
trees_ok_to_generate.push(TreeType::Oak);
|
||||||
|
trees_ok_to_generate.push(TreeType::Spruce);
|
||||||
|
trees_ok_to_generate.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(genus) = element.tags().get("genus") {
|
||||||
|
match genus.as_str() {
|
||||||
|
"Betula" => trees_ok_to_generate.push(TreeType::Birch),
|
||||||
|
"Quercus" => trees_ok_to_generate.push(TreeType::Oak),
|
||||||
|
"Picea" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||||
|
_ => trees_ok_to_generate.push(TreeType::Oak),
|
||||||
|
}
|
||||||
|
} else if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||||
|
match leaf_type.as_str() {
|
||||||
|
"broadleaved" => {
|
||||||
|
trees_ok_to_generate.push(TreeType::Oak);
|
||||||
|
trees_ok_to_generate.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
"needleleaved" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||||
|
_ => {
|
||||||
|
trees_ok_to_generate.push(TreeType::Oak);
|
||||||
|
trees_ok_to_generate.push(TreeType::Spruce);
|
||||||
|
trees_ok_to_generate.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trees_ok_to_generate.push(TreeType::Oak);
|
||||||
|
trees_ok_to_generate.push(TreeType::Spruce);
|
||||||
|
trees_ok_to_generate.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if trees_ok_to_generate.is_empty() {
|
||||||
|
trees_ok_to_generate.push(TreeType::Oak);
|
||||||
|
trees_ok_to_generate.push(TreeType::Spruce);
|
||||||
|
trees_ok_to_generate.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rng = element_rng(element.id());
|
||||||
|
let tree_type = *trees_ok_to_generate
|
||||||
|
.choose(&mut rng)
|
||||||
|
.unwrap_or(&TreeType::Oak);
|
||||||
|
|
||||||
|
Tree::create_of_type(editor, (x, 1, z), tree_type, Some(building_footprints));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut previous_node: Option<(i32, i32)> = None;
|
let mut previous_node: Option<(i32, i32)> = None;
|
||||||
@@ -81,6 +141,29 @@ pub fn generate_natural(
|
|||||||
let filled_area: Vec<(i32, i32)> =
|
let filled_area: Vec<(i32, i32)> =
|
||||||
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
||||||
|
|
||||||
|
let trees_ok_to_generate: Vec<TreeType> = {
|
||||||
|
let mut trees: Vec<TreeType> = vec![];
|
||||||
|
if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||||
|
match leaf_type.as_str() {
|
||||||
|
"broadleaved" => {
|
||||||
|
trees.push(TreeType::Oak);
|
||||||
|
trees.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
"needleleaved" => trees.push(TreeType::Spruce),
|
||||||
|
_ => {
|
||||||
|
trees.push(TreeType::Oak);
|
||||||
|
trees.push(TreeType::Spruce);
|
||||||
|
trees.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trees.push(TreeType::Oak);
|
||||||
|
trees.push(TreeType::Spruce);
|
||||||
|
trees.push(TreeType::Birch);
|
||||||
|
}
|
||||||
|
trees
|
||||||
|
};
|
||||||
|
|
||||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||||
let mut rng = element_rng(way.id);
|
let mut rng = element_rng(way.id);
|
||||||
|
|
||||||
@@ -164,7 +247,15 @@ pub fn generate_natural(
|
|||||||
}
|
}
|
||||||
let random_choice: i32 = rng.gen_range(0..30);
|
let random_choice: i32 = rng.gen_range(0..30);
|
||||||
if random_choice == 0 {
|
if random_choice == 0 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
let tree_type = *trees_ok_to_generate
|
||||||
|
.choose(&mut rng)
|
||||||
|
.unwrap_or(&TreeType::Oak);
|
||||||
|
Tree::create_of_type(
|
||||||
|
editor,
|
||||||
|
(x, 1, z),
|
||||||
|
tree_type,
|
||||||
|
Some(building_footprints),
|
||||||
|
);
|
||||||
} else if random_choice == 1 {
|
} else if random_choice == 1 {
|
||||||
let flower_block = match rng.gen_range(1..=4) {
|
let flower_block = match rng.gen_range(1..=4) {
|
||||||
1 => RED_FLOWER,
|
1 => RED_FLOWER,
|
||||||
@@ -460,44 +551,26 @@ pub fn generate_natural_from_relation(
|
|||||||
building_footprints: &BuildingFootprintBitmap,
|
building_footprints: &BuildingFootprintBitmap,
|
||||||
) {
|
) {
|
||||||
if rel.tags.contains_key("natural") {
|
if rel.tags.contains_key("natural") {
|
||||||
// Generate individual ways with their original tags
|
// Process each outer member way individually using cached flood fill.
|
||||||
|
// We intentionally do not combine all outer nodes into one mega-way,
|
||||||
|
// because that creates a nonsensical polygon spanning the whole relation
|
||||||
|
// extent, misses the flood fill cache, and can cause multi-GB allocations.
|
||||||
for member in &rel.members {
|
for member in &rel.members {
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
|
// Use relation tags so the member inherits the relation's natural=* type
|
||||||
|
let way_with_rel_tags = ProcessedWay {
|
||||||
|
id: member.way.id,
|
||||||
|
nodes: member.way.nodes.clone(),
|
||||||
|
tags: rel.tags.clone(),
|
||||||
|
};
|
||||||
generate_natural(
|
generate_natural(
|
||||||
editor,
|
editor,
|
||||||
&ProcessedElement::Way((*member.way).clone()),
|
&ProcessedElement::Way(way_with_rel_tags),
|
||||||
args,
|
args,
|
||||||
flood_fill_cache,
|
flood_fill_cache,
|
||||||
building_footprints,
|
building_footprints,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine all outer ways into one with relation tags
|
|
||||||
let mut combined_nodes = Vec::new();
|
|
||||||
for member in &rel.members {
|
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
|
||||||
combined_nodes.extend(member.way.nodes.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process if we have nodes
|
|
||||||
if !combined_nodes.is_empty() {
|
|
||||||
// Create combined way with relation tags
|
|
||||||
let combined_way = ProcessedWay {
|
|
||||||
id: rel.id,
|
|
||||||
nodes: combined_nodes,
|
|
||||||
tags: rel.tags.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate natural area from combined way
|
|
||||||
generate_natural(
|
|
||||||
editor,
|
|
||||||
&ProcessedElement::Way(combined_way),
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
385
src/element_processing/power.rs
Normal file
385
src/element_processing/power.rs
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
//! Processing of power infrastructure elements.
|
||||||
|
//!
|
||||||
|
//! This module handles power-related OSM elements including:
|
||||||
|
//! - `power=tower` - Large electricity pylons
|
||||||
|
//! - `power=pole` - Smaller wooden/concrete poles
|
||||||
|
//! - `power=line` - Power lines connecting towers/poles
|
||||||
|
|
||||||
|
use crate::block_definitions::*;
|
||||||
|
use crate::bresenham::bresenham_line;
|
||||||
|
use crate::osm_parser::{ProcessedElement, ProcessedNode, ProcessedWay};
|
||||||
|
use crate::world_editor::WorldEditor;
|
||||||
|
|
||||||
|
/// Generate power infrastructure from way elements (power lines)
|
||||||
|
pub fn generate_power(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||||
|
// Skip if 'layer' or 'level' is negative in the tags
|
||||||
|
if let Some(layer) = element.tags().get("layer") {
|
||||||
|
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(level) = element.tags().get("level") {
|
||||||
|
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip underground power infrastructure
|
||||||
|
if element
|
||||||
|
.tags()
|
||||||
|
.get("location")
|
||||||
|
.map(|v| v == "underground" || v == "underwater")
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if element
|
||||||
|
.tags()
|
||||||
|
.get("tunnel")
|
||||||
|
.map(|v| v == "yes")
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(power_type) = element.tags().get("power") {
|
||||||
|
match power_type.as_str() {
|
||||||
|
"line" | "minor_line" => {
|
||||||
|
if let ProcessedElement::Way(way) = element {
|
||||||
|
generate_power_line(editor, way);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"tower" => generate_power_tower(editor, element),
|
||||||
|
"pole" => generate_power_pole(editor, element),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate power infrastructure from node elements
|
||||||
|
pub fn generate_power_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
// Skip if 'layer' or 'level' is negative in the tags
|
||||||
|
if let Some(layer) = node.tags.get("layer") {
|
||||||
|
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(level) = node.tags.get("level") {
|
||||||
|
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip underground power infrastructure
|
||||||
|
if node
|
||||||
|
.tags
|
||||||
|
.get("location")
|
||||||
|
.map(|v| v == "underground" || v == "underwater")
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if node.tags.get("tunnel").map(|v| v == "yes").unwrap_or(false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(power_type) = node.tags.get("power") {
|
||||||
|
match power_type.as_str() {
|
||||||
|
"tower" => generate_power_tower_from_node(editor, node),
|
||||||
|
"pole" => generate_power_pole_from_node(editor, node),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a high-voltage transmission tower (pylon) from a ProcessedElement
|
||||||
|
fn generate_power_tower(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||||
|
let Some(first_node) = element.nodes().next() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let height = element
|
||||||
|
.tags()
|
||||||
|
.get("height")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.unwrap_or(25)
|
||||||
|
.clamp(15, 40);
|
||||||
|
generate_power_tower_impl(editor, first_node.x, first_node.z, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a high-voltage transmission tower (pylon) from a ProcessedNode
|
||||||
|
fn generate_power_tower_from_node(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let height = node
|
||||||
|
.tags
|
||||||
|
.get("height")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.unwrap_or(25)
|
||||||
|
.clamp(15, 40);
|
||||||
|
generate_power_tower_impl(editor, node.x, node.z, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a high-voltage transmission tower (pylon)
|
||||||
|
///
|
||||||
|
/// Creates a realistic lattice tower structure using iron bars and iron blocks.
|
||||||
|
/// The design is a tapered lattice tower with cross-bracing and insulators.
|
||||||
|
fn generate_power_tower_impl(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
|
||||||
|
// Tower design constants
|
||||||
|
let base_width = 3; // Half-width at base (so 7x7 footprint)
|
||||||
|
let top_width = 1; // Half-width at top (so 3x3)
|
||||||
|
let arm_height = height - 4; // Height where arms extend
|
||||||
|
let arm_length = 5; // How far arms extend horizontally
|
||||||
|
|
||||||
|
// Build the four corner legs with tapering
|
||||||
|
for y in 1..=height {
|
||||||
|
// Calculate taper: legs get closer together as we go up
|
||||||
|
let progress = y as f32 / height as f32;
|
||||||
|
let current_width = base_width - ((base_width - top_width) as f32 * progress) as i32;
|
||||||
|
|
||||||
|
// Four corner positions
|
||||||
|
let corners = [
|
||||||
|
(x - current_width, z - current_width),
|
||||||
|
(x + current_width, z - current_width),
|
||||||
|
(x - current_width, z + current_width),
|
||||||
|
(x + current_width, z + current_width),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (cx, cz) in corners {
|
||||||
|
editor.set_block(IRON_BLOCK, cx, y, cz, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add horizontal cross-bracing every 5 blocks
|
||||||
|
if y % 5 == 0 && y < height - 2 {
|
||||||
|
// Connect corners horizontally
|
||||||
|
for dx in -current_width..=current_width {
|
||||||
|
editor.set_block(IRON_BLOCK, x + dx, y, z - current_width, None, None);
|
||||||
|
editor.set_block(IRON_BLOCK, x + dx, y, z + current_width, None, None);
|
||||||
|
}
|
||||||
|
for dz in -current_width..=current_width {
|
||||||
|
editor.set_block(IRON_BLOCK, x - current_width, y, z + dz, None, None);
|
||||||
|
editor.set_block(IRON_BLOCK, x + current_width, y, z + dz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add diagonal bracing between cross-brace levels
|
||||||
|
if y % 5 >= 1 && y % 5 <= 4 && y > 1 && y < height - 2 {
|
||||||
|
let prev_width = base_width
|
||||||
|
- ((base_width - top_width) as f32 * ((y - 1) as f32 / height as f32)) as i32;
|
||||||
|
|
||||||
|
// Only add center vertical support if the width changed
|
||||||
|
if current_width != prev_width || y % 5 == 2 {
|
||||||
|
editor.set_block(IRON_BARS, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the cross-arms at arm_height for holding power lines
|
||||||
|
// These extend outward in two directions (perpendicular to typical line direction)
|
||||||
|
for arm_offset in [-arm_length, arm_length] {
|
||||||
|
// Main arm beam (iron blocks for strength)
|
||||||
|
for dx in 0..=arm_length {
|
||||||
|
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
|
||||||
|
editor.set_block(IRON_BLOCK, arm_x, arm_height, z, None, None);
|
||||||
|
// Add second arm perpendicular
|
||||||
|
editor.set_block(
|
||||||
|
IRON_BLOCK,
|
||||||
|
x,
|
||||||
|
arm_height,
|
||||||
|
z + if arm_offset < 0 { -dx } else { dx },
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insulators hanging from arm ends (end rods to simulate ceramic insulators)
|
||||||
|
let end_x = if arm_offset < 0 {
|
||||||
|
x - arm_length
|
||||||
|
} else {
|
||||||
|
x + arm_length
|
||||||
|
};
|
||||||
|
editor.set_block(END_ROD, end_x, arm_height - 1, z, None, None);
|
||||||
|
editor.set_block(END_ROD, x, arm_height - 1, z + arm_offset, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a second, smaller arm set lower for additional circuits
|
||||||
|
let lower_arm_height = arm_height - 6;
|
||||||
|
if lower_arm_height > 5 {
|
||||||
|
let lower_arm_length = arm_length - 1;
|
||||||
|
for arm_offset in [-lower_arm_length, lower_arm_length] {
|
||||||
|
for dx in 0..=lower_arm_length {
|
||||||
|
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
|
||||||
|
editor.set_block(IRON_BLOCK, arm_x, lower_arm_height, z, None, None);
|
||||||
|
}
|
||||||
|
let end_x = if arm_offset < 0 {
|
||||||
|
x - lower_arm_length
|
||||||
|
} else {
|
||||||
|
x + lower_arm_length
|
||||||
|
};
|
||||||
|
editor.set_block(END_ROD, end_x, lower_arm_height - 1, z, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top finial/lightning rod
|
||||||
|
editor.set_block(IRON_BLOCK, x, height, z, None, None);
|
||||||
|
editor.set_block(LIGHTNING_ROD, x, height + 1, z, None, None);
|
||||||
|
|
||||||
|
// Concrete foundation at base
|
||||||
|
for dx in -3..=3 {
|
||||||
|
for dz in -3..=3 {
|
||||||
|
editor.set_block(GRAY_CONCRETE, x + dx, 0, z + dz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a wooden/concrete power pole from a ProcessedElement
|
||||||
|
fn generate_power_pole(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||||
|
let Some(first_node) = element.nodes().next() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let height = element
|
||||||
|
.tags()
|
||||||
|
.get("height")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.unwrap_or(10)
|
||||||
|
.clamp(6, 15);
|
||||||
|
let pole_material = element
|
||||||
|
.tags()
|
||||||
|
.get("material")
|
||||||
|
.map(|m| m.as_str())
|
||||||
|
.unwrap_or("wood");
|
||||||
|
generate_power_pole_impl(editor, first_node.x, first_node.z, height, pole_material);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a wooden/concrete power pole from a ProcessedNode
|
||||||
|
fn generate_power_pole_from_node(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||||
|
let height = node
|
||||||
|
.tags
|
||||||
|
.get("height")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.unwrap_or(10)
|
||||||
|
.clamp(6, 15);
|
||||||
|
let pole_material = node
|
||||||
|
.tags
|
||||||
|
.get("material")
|
||||||
|
.map(|m| m.as_str())
|
||||||
|
.unwrap_or("wood");
|
||||||
|
generate_power_pole_impl(editor, node.x, node.z, height, pole_material);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a wooden/concrete power pole
|
||||||
|
///
|
||||||
|
/// Creates a simpler single-pole structure for lower voltage distribution lines.
|
||||||
|
fn generate_power_pole_impl(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
x: i32,
|
||||||
|
z: i32,
|
||||||
|
height: i32,
|
||||||
|
pole_material: &str,
|
||||||
|
) {
|
||||||
|
let pole_block = match pole_material {
|
||||||
|
"concrete" => LIGHT_GRAY_CONCRETE,
|
||||||
|
"steel" | "metal" => IRON_BLOCK,
|
||||||
|
_ => OAK_LOG, // Default to wood
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the main pole
|
||||||
|
for y in 1..=height {
|
||||||
|
editor.set_block(pole_block, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-arm at top (perpendicular beam for wires)
|
||||||
|
let arm_length = 2;
|
||||||
|
for dx in -arm_length..=arm_length {
|
||||||
|
editor.set_block(OAK_FENCE, x + dx, height, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insulators at arm ends
|
||||||
|
editor.set_block(END_ROD, x - arm_length, height + 1, z, None, None);
|
||||||
|
editor.set_block(END_ROD, x + arm_length, height + 1, z, None, None);
|
||||||
|
editor.set_block(END_ROD, x, height + 1, z, None, None); // Center insulator
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate power lines connecting towers/poles
|
||||||
|
///
|
||||||
|
/// Creates a catenary-like curve (simplified) between nodes to simulate
|
||||||
|
/// the natural sag of power cables.
|
||||||
|
fn generate_power_line(editor: &mut WorldEditor, way: &ProcessedWay) {
|
||||||
|
if way.nodes.len() < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine line height based on voltage (higher voltage = taller structures)
|
||||||
|
let base_height = way
|
||||||
|
.tags
|
||||||
|
.get("voltage")
|
||||||
|
.and_then(|v| v.parse::<i32>().ok())
|
||||||
|
.map(|voltage| {
|
||||||
|
if voltage >= 220000 {
|
||||||
|
22 // High voltage transmission
|
||||||
|
} else if voltage >= 110000 {
|
||||||
|
18
|
||||||
|
} else if voltage >= 33000 {
|
||||||
|
14
|
||||||
|
} else {
|
||||||
|
10 // Distribution lines
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(15);
|
||||||
|
|
||||||
|
// Process consecutive node pairs
|
||||||
|
for i in 1..way.nodes.len() {
|
||||||
|
let start = &way.nodes[i - 1];
|
||||||
|
let end = &way.nodes[i];
|
||||||
|
|
||||||
|
// Calculate distance between nodes
|
||||||
|
let dx = (end.x - start.x) as f64;
|
||||||
|
let dz = (end.z - start.z) as f64;
|
||||||
|
let distance = (dx * dx + dz * dz).sqrt();
|
||||||
|
|
||||||
|
// Calculate sag based on span length (longer spans = more sag)
|
||||||
|
let max_sag = (distance / 15.0).clamp(1.0, 6.0) as i32;
|
||||||
|
|
||||||
|
// Determine chain orientation based on line direction
|
||||||
|
// If the line runs more along X-axis, use CHAIN_X; if more along Z-axis, use CHAIN_Z
|
||||||
|
let chain_block = if dx.abs() >= dz.abs() {
|
||||||
|
CHAIN_X // Line runs primarily along X-axis
|
||||||
|
} else {
|
||||||
|
CHAIN_Z // Line runs primarily along Z-axis
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate points along the line using Bresenham
|
||||||
|
let line_points = bresenham_line(start.x, 0, start.z, end.x, 0, end.z);
|
||||||
|
|
||||||
|
for (idx, (lx, _, lz)) in line_points.iter().enumerate() {
|
||||||
|
// Calculate position along the span (0.0 to 1.0)
|
||||||
|
// Use len-1 as denominator so last point reaches t=1.0
|
||||||
|
let denom = (line_points.len().saturating_sub(1)).max(1) as f64;
|
||||||
|
let t = idx as f64 / denom;
|
||||||
|
|
||||||
|
// Catenary approximation: sag is maximum at center, zero at ends
|
||||||
|
// Using parabola: sag = 4 * max_sag * t * (1 - t)
|
||||||
|
let sag = (4.0 * max_sag as f64 * t * (1.0 - t)) as i32;
|
||||||
|
|
||||||
|
// Ensure wire doesn't go underground (minimum height of 3 blocks above ground)
|
||||||
|
let wire_y = (base_height - sag).max(3);
|
||||||
|
|
||||||
|
// Place the wire block (chain aligned with line direction)
|
||||||
|
editor.set_block(chain_block, *lx, wire_y, *lz, None, None);
|
||||||
|
|
||||||
|
// For high voltage lines, add parallel wires offset to sides
|
||||||
|
if base_height >= 18 {
|
||||||
|
// Three-phase power: 3 parallel lines
|
||||||
|
// Offset perpendicular to the line direction
|
||||||
|
if dx.abs() >= dz.abs() {
|
||||||
|
// Line runs along X, offset in Z
|
||||||
|
editor.set_block(chain_block, *lx, wire_y, *lz + 1, None, None);
|
||||||
|
editor.set_block(chain_block, *lx, wire_y, *lz - 1, None, None);
|
||||||
|
} else {
|
||||||
|
// Line runs along Z, offset in X
|
||||||
|
editor.set_block(chain_block, *lx + 1, wire_y, *lz, None, None);
|
||||||
|
editor.set_block(chain_block, *lx - 1, wire_y, *lz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ const INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
|||||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Interior layout for building level floors (1nd layer above floor)
|
/// Interior layout for building level floors (1st layer above floor)
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||||
@@ -114,6 +114,119 @@ const INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
|||||||
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
|
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Generic Abandoned Building Interiors
|
||||||
|
/// Interior layout for building ground floors (1st layer above floor)
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const ABANDONED_INTERIOR1_LAYER1: [[char; 23]; 23] = [
|
||||||
|
['1', 'U', ' ', 'W', 'C', ' ', ' ', ' ', 'S', 'S', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['2', ' ', ' ', 'W', 'F', ' ', ' ', ' ', 'U', 'U', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', 'B', 'W',],
|
||||||
|
[' ', ' ', ' ', 'W', 'F', ' ', ' ', ' ', ' ', ' ', 'W', 'b', 'T', 'T', 'd', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||||
|
['W', 'W', 'D', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'M', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'c', 'c', 'c', ' ', ' ', 'J', 'W', ' ', ' ', ' ', 'd', 'W', 'W', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', 'T', 'S', 'S', 'T', ' ', ' ', 'W', 'S', 'S', ' ', 'd', 'W', 'W', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'T', 'T', 'T', 'T', ' ', ' ', 'W', 'U', 'U', ' ', 'd', 'W', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', 'T', 'T', 'T', 'T', ' ', 'B', 'W', ' ', ' ', ' ', 'd', 'W', ' ', ' ',],
|
||||||
|
['L', ' ', 'M', 'L', 'W', 'W', ' ', ' ', 'W', 'J', 'U', 'U', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||||
|
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||||
|
['c', 'c', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||||
|
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||||
|
[' ', '6', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['U', '5', ' ', 'W', ' ', ' ', 'W', 'C', 'F', 'F', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', 'W', 'M', ' ', 'b', 'W', ' ', ' ', 'W',],
|
||||||
|
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'b', 'W', 'J', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'U', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||||
|
['J', ' ', ' ', 'C', 'a', 'a', 'W', 'L', 'F', ' ', 'W', 'F', ' ', 'W', 'L', 'W', '7', '8', ' ', 'W', 'B', ' ', 'W',],
|
||||||
|
['B', ' ', ' ', 'd', 'W', 'W', 'W', 'W', 'W', ' ', 'W', 'M', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'C', ' ', 'W',],
|
||||||
|
['B', ' ', ' ', 'd', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'C', ' ', ' ', 'W', 'W', 'c', 'c', 'c', 'c', 'W', 'D', 'W',],
|
||||||
|
['W', 'W', 'D', 'W', 'C', ' ', ' ', ' ', 'W', 'W', 'W', 'b', 'T', 'T', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Interior layout for building ground floors (2nd layer above floor)
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const ABANDONED_INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||||
|
[' ', 'P', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'P', 'P', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'B', 'W',],
|
||||||
|
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||||
|
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', 'P', ' ', 'B', 'W', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
|
||||||
|
[' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', ' ', 'P', 'P', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||||
|
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||||
|
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||||
|
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||||
|
[' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['P', ' ', ' ', 'W', ' ', ' ', 'W', 'N', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', ' ', ' ', 'W',],
|
||||||
|
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'C', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'P', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||||
|
[' ', ' ', ' ', ' ', 'B', 'B', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'P', 'W', ' ', ' ', ' ', 'W', 'B', ' ', 'W',],
|
||||||
|
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W',],
|
||||||
|
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'N', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
|
||||||
|
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Interior layout for building level floors (1st layer above floor)
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const ABANDONED_INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||||
|
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||||
|
['U', ' ', ' ', ' ', ' ', ' ', 'C', 'W', 'L', ' ', ' ', 'L', 'W', 'M', 'M', 'W', ' ', ' ', ' ', ' ', ' ', 'L', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'S', 'S', 'S', ' ', 'W',],
|
||||||
|
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', 'C', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'J', ' ', 'U', 'U', 'U', ' ', 'D',],
|
||||||
|
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||||
|
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', 'T', 'T', 'W', ' ', ' ', ' ', ' ', ' ', 'U', 'W', ' ', 'L', 'W',],
|
||||||
|
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'T', 'J', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', 'L', ' ', 'W',],
|
||||||
|
['J', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||||
|
[' ', 'M', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', 'd', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||||
|
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'U', ' ', ' ', ' ', 'D', ' ', ' ', 'F', 'F', 'W', 'M', 'M', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'U', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', 'C', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
['C', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'L', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'U', 'U', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'U', 'U', ' ', 'Q', 'b', ' ', 'U', 'U', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['S', 'S', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||||
|
['U', 'U', ' ', ' ', ' ', 'L', 'a', 'a', 'a', ' ', ' ', 'Q', 'B', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', 'D', 'W',],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Interior layout for building level floors (2nd layer above floor)
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const ABANDONED_INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||||
|
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||||
|
['P', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'O', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'O', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', 'P', 'P', ' ', 'D',],
|
||||||
|
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||||
|
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'P', 'W', ' ', 'P', 'W',],
|
||||||
|
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||||
|
[' ', ' ', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', 'd', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||||
|
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'P', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'O', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'P', 'P', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'P', 'P', ' ', 'Q', 'b', ' ', 'P', 'P', 'c', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||||
|
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||||
|
['P', 'P', ' ', ' ', ' ', 'O', 'a', 'a', 'a', ' ', ' ', 'Q', 'b', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', ' ', 'D',],
|
||||||
|
];
|
||||||
|
|
||||||
/// Maps interior layout characters to actual block types for different floor layers
|
/// Maps interior layout characters to actual block types for different floor layers
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
|
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
|
||||||
@@ -145,12 +258,19 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
|||||||
Some(DARK_OAK_DOOR_LOWER)
|
Some(DARK_OAK_DOOR_LOWER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'J' => Some(NOTE_BLOCK), // Note block
|
'J' => Some(NOTE_BLOCK), // Note block
|
||||||
'G' => Some(GLOWSTONE), // Glowstone
|
'G' => Some(GLOWSTONE), // Glowstone
|
||||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||||
'T' => Some(WHITE_CARPET), // White Carpet
|
'T' => Some(WHITE_CARPET), // White Carpet
|
||||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||||
_ => None, // Default case for unknown characters
|
'O' => Some(COBWEB), // Cobweb
|
||||||
|
'a' => Some(CHISELLED_BOOKSHELF_NORTH), // Chiseled Bookshelf
|
||||||
|
'b' => Some(CHISELLED_BOOKSHELF_EAST), // Chiseled Bookshelf East
|
||||||
|
'c' => Some(CHISELLED_BOOKSHELF_SOUTH), // Chiseled Bookshelf South
|
||||||
|
'd' => Some(CHISELLED_BOOKSHELF_WEST), // Chiseled Bookshelf West
|
||||||
|
'M' => Some(DAMAGED_ANVIL), // Damaged Anvil
|
||||||
|
'Q' => Some(SCAFFOLDING), // Scaffolding
|
||||||
|
_ => None, // Default case for unknown characters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +290,7 @@ pub fn generate_building_interior(
|
|||||||
args: &crate::args::Args,
|
args: &crate::args::Args,
|
||||||
element: &crate::osm_parser::ProcessedWay,
|
element: &crate::osm_parser::ProcessedWay,
|
||||||
abs_terrain_offset: i32,
|
abs_terrain_offset: i32,
|
||||||
|
is_abandoned_building: bool,
|
||||||
) {
|
) {
|
||||||
// Skip interior generation for very small buildings
|
// Skip interior generation for very small buildings
|
||||||
let width = max_x - min_x + 1;
|
let width = max_x - min_x + 1;
|
||||||
@@ -214,7 +335,13 @@ pub fn generate_building_interior(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Choose the appropriate interior pattern based on floor number
|
// Choose the appropriate interior pattern based on floor number
|
||||||
let (layer1, layer2) = if floor_index == 0 {
|
let (layer1, layer2) = if is_abandoned_building {
|
||||||
|
if floor_index == 0 {
|
||||||
|
(&ABANDONED_INTERIOR1_LAYER1, &ABANDONED_INTERIOR1_LAYER2)
|
||||||
|
} else {
|
||||||
|
(&ABANDONED_INTERIOR2_LAYER1, &ABANDONED_INTERIOR2_LAYER2)
|
||||||
|
}
|
||||||
|
} else if floor_index == 0 {
|
||||||
// Ground floor uses INTERIOR1 patterns
|
// Ground floor uses INTERIOR1 patterns
|
||||||
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -83,6 +83,33 @@ const BIRCH_LEAVES_FILL: [(Coord, Coord); 5] = [
|
|||||||
((0, 7, 0), (0, 8, 0)),
|
((0, 7, 0), (0, 8, 0)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Dark oak: short but wide canopy, leaves start at y=3 up to y=6 with a cap
|
||||||
|
const DARK_OAK_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||||
|
((-1, 3, 0), (-1, 6, 0)),
|
||||||
|
((1, 3, 0), (1, 6, 0)),
|
||||||
|
((0, 3, -1), (0, 6, -1)),
|
||||||
|
((0, 3, 1), (0, 6, 1)),
|
||||||
|
((0, 6, 0), (0, 7, 0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Jungle: tall tree with canopy only near the top, leaves from y=7 to y=11
|
||||||
|
const JUNGLE_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||||
|
((-1, 7, 0), (-1, 11, 0)),
|
||||||
|
((1, 7, 0), (1, 11, 0)),
|
||||||
|
((0, 7, -1), (0, 11, -1)),
|
||||||
|
((0, 7, 1), (0, 11, 1)),
|
||||||
|
((0, 11, 0), (0, 12, 0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Acacia: umbrella-shaped canopy with a gentle dome, leaves from y=5 to y=8
|
||||||
|
const ACACIA_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||||
|
((-1, 5, 0), (-1, 8, 0)),
|
||||||
|
((1, 5, 0), (1, 8, 0)),
|
||||||
|
((0, 5, -1), (0, 8, -1)),
|
||||||
|
((0, 5, 1), (0, 8, 1)),
|
||||||
|
((0, 8, 0), (0, 9, 0)),
|
||||||
|
];
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
|
|
||||||
/// Helper function to set blocks in various patterns.
|
/// Helper function to set blocks in various patterns.
|
||||||
@@ -92,10 +119,14 @@ fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_patt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub enum TreeType {
|
pub enum TreeType {
|
||||||
Oak,
|
Oak,
|
||||||
Spruce,
|
Spruce,
|
||||||
Birch,
|
Birch,
|
||||||
|
DarkOak,
|
||||||
|
Jungle,
|
||||||
|
Acacia,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO what should be moved in, and what should be referenced?
|
// TODO what should be moved in, and what should be referenced?
|
||||||
@@ -120,6 +151,30 @@ impl Tree<'_> {
|
|||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
(x, y, z): Coord,
|
(x, y, z): Coord,
|
||||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||||
|
) {
|
||||||
|
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||||
|
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||||
|
let mut rng = coord_rng(x, z, 0);
|
||||||
|
|
||||||
|
let tree_type = match rng.gen_range(1..=10) {
|
||||||
|
1..=3 => TreeType::Oak,
|
||||||
|
4..=5 => TreeType::Spruce,
|
||||||
|
6..=7 => TreeType::Birch,
|
||||||
|
8 => TreeType::DarkOak,
|
||||||
|
9 => TreeType::Jungle,
|
||||||
|
10 => TreeType::Acacia,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::create_of_type(editor, (x, y, z), tree_type, building_footprints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a tree of a specific type at the specified coordinates.
|
||||||
|
pub fn create_of_type(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
(x, y, z): Coord,
|
||||||
|
tree_type: TreeType,
|
||||||
|
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||||
) {
|
) {
|
||||||
// Skip if this coordinate is inside a building
|
// Skip if this coordinate is inside a building
|
||||||
if let Some(footprints) = building_footprints {
|
if let Some(footprints) = building_footprints {
|
||||||
@@ -135,16 +190,7 @@ impl Tree<'_> {
|
|||||||
blacklist.extend(Self::get_functional_blocks());
|
blacklist.extend(Self::get_functional_blocks());
|
||||||
blacklist.push(WATER);
|
blacklist.push(WATER);
|
||||||
|
|
||||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
let tree = Self::get_tree(tree_type);
|
||||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
|
||||||
let mut rng = coord_rng(x, z, 0);
|
|
||||||
|
|
||||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
|
||||||
1 => TreeType::Oak,
|
|
||||||
2 => TreeType::Spruce,
|
|
||||||
3 => TreeType::Birch,
|
|
||||||
_ => unreachable!(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build the logs
|
// Build the logs
|
||||||
editor.fill_blocks(
|
editor.fill_blocks(
|
||||||
@@ -201,9 +247,9 @@ impl Tree<'_> {
|
|||||||
// kind,
|
// kind,
|
||||||
log_block: SPRUCE_LOG,
|
log_block: SPRUCE_LOG,
|
||||||
log_height: 9,
|
log_height: 9,
|
||||||
leaves_block: BIRCH_LEAVES, // TODO Is this correct?
|
leaves_block: SPRUCE_LEAVES,
|
||||||
leaves_fill: &SPRUCE_LEAVES_FILL,
|
leaves_fill: &SPRUCE_LEAVES_FILL,
|
||||||
// TODO can I omit the third empty vec? May cause issues with iter zip
|
// Conical shape: wide at bottom, narrow at top
|
||||||
round_ranges: [vec![9, 7, 6, 4, 3], vec![6, 3], vec![]],
|
round_ranges: [vec![9, 7, 6, 4, 3], vec![6, 3], vec![]],
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -215,6 +261,44 @@ impl Tree<'_> {
|
|||||||
leaves_fill: &BIRCH_LEAVES_FILL,
|
leaves_fill: &BIRCH_LEAVES_FILL,
|
||||||
round_ranges: [(2..=6).rev().collect(), (2..=4).collect(), vec![]],
|
round_ranges: [(2..=6).rev().collect(), (2..=4).collect(), vec![]],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
TreeType::DarkOak => Self {
|
||||||
|
// Short trunk with a very wide, bushy canopy
|
||||||
|
log_block: DARK_OAK_LOG,
|
||||||
|
log_height: 5,
|
||||||
|
leaves_block: DARK_OAK_LEAVES,
|
||||||
|
leaves_fill: &DARK_OAK_LEAVES_FILL,
|
||||||
|
// All 3 round patterns used for maximum width
|
||||||
|
round_ranges: [
|
||||||
|
(3..=6).rev().collect(),
|
||||||
|
(3..=5).rev().collect(),
|
||||||
|
(4..=5).rev().collect(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
TreeType::Jungle => Self {
|
||||||
|
// Tall trunk, canopy clustered at the top
|
||||||
|
log_block: JUNGLE_LOG,
|
||||||
|
log_height: 10,
|
||||||
|
leaves_block: JUNGLE_LEAVES,
|
||||||
|
leaves_fill: &JUNGLE_LEAVES_FILL,
|
||||||
|
// Canopy only near the top of the tree
|
||||||
|
round_ranges: [(7..=11).rev().collect(), (8..=10).rev().collect(), vec![]],
|
||||||
|
},
|
||||||
|
|
||||||
|
TreeType::Acacia => Self {
|
||||||
|
// Medium trunk with umbrella-shaped canopy, domed center
|
||||||
|
log_block: ACACIA_LOG,
|
||||||
|
log_height: 6,
|
||||||
|
leaves_block: ACACIA_LEAVES,
|
||||||
|
leaves_fill: &ACACIA_LEAVES_FILL,
|
||||||
|
// Inner rounds reach higher → gentle dome, outer stays low → wide brim
|
||||||
|
round_ranges: [
|
||||||
|
(5..=8).rev().collect(),
|
||||||
|
(5..=7).rev().collect(),
|
||||||
|
(6..=7).rev().collect(),
|
||||||
|
],
|
||||||
|
},
|
||||||
} // match
|
} // match
|
||||||
} // fn get_tree
|
} // fn get_tree
|
||||||
|
|
||||||
@@ -350,6 +434,9 @@ impl Tree<'_> {
|
|||||||
GRAY_STAINED_GLASS,
|
GRAY_STAINED_GLASS,
|
||||||
LIGHT_GRAY_STAINED_GLASS,
|
LIGHT_GRAY_STAINED_GLASS,
|
||||||
BROWN_STAINED_GLASS,
|
BROWN_STAINED_GLASS,
|
||||||
|
CYAN_STAINED_GLASS,
|
||||||
|
BLUE_STAINED_GLASS,
|
||||||
|
LIGHT_BLUE_STAINED_GLASS,
|
||||||
TINTED_GLASS,
|
TINTED_GLASS,
|
||||||
// Carpets
|
// Carpets
|
||||||
WHITE_CARPET,
|
WHITE_CARPET,
|
||||||
|
|||||||
@@ -58,14 +58,14 @@ pub fn generate_water_areas_from_relation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preserve OSM-defined outer/inner roles without modification
|
// Preserve OSM-defined outer/inner roles without modification
|
||||||
merge_way_segments(&mut outers);
|
super::merge_way_segments(&mut outers);
|
||||||
|
|
||||||
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
|
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
|
||||||
outers = outers
|
outers = outers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||||
.collect();
|
.collect();
|
||||||
merge_way_segments(&mut inners);
|
super::merge_way_segments(&mut inners);
|
||||||
inners = inners
|
inners = inners
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||||
@@ -112,7 +112,7 @@ pub fn generate_water_areas_from_relation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
merge_way_segments(&mut inners);
|
super::merge_way_segments(&mut inners);
|
||||||
if !verify_closed_rings(&inners) {
|
if !verify_closed_rings(&inners) {
|
||||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||||
return;
|
return;
|
||||||
@@ -166,105 +166,6 @@ fn generate_water_areas(
|
|||||||
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
|
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merges way segments that share endpoints into closed rings.
|
|
||||||
fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
|
||||||
let mut removed: Vec<usize> = vec![];
|
|
||||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
|
||||||
|
|
||||||
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
|
||||||
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
|
||||||
if a.id == b.id {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let dx = (a.x - b.x).abs();
|
|
||||||
let dz = (a.z - b.z).abs();
|
|
||||||
dx <= 1 && dz <= 1
|
|
||||||
};
|
|
||||||
|
|
||||||
for i in 0..rings.len() {
|
|
||||||
for j in 0..rings.len() {
|
|
||||||
if i == j {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if removed.contains(&i) || removed.contains(&j) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let x: &Vec<ProcessedNode> = &rings[i];
|
|
||||||
let y: &Vec<ProcessedNode> = &rings[j];
|
|
||||||
|
|
||||||
// Skip empty rings (can happen after clipping)
|
|
||||||
if x.is_empty() || y.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let x_first = &x[0];
|
|
||||||
let x_last = x.last().unwrap();
|
|
||||||
let y_first = &y[0];
|
|
||||||
let y_last = y.last().unwrap();
|
|
||||||
|
|
||||||
// Skip already-closed rings
|
|
||||||
if nodes_match(x_first, x_last) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if nodes_match(y_first, y_last) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if nodes_match(x_first, y_first) {
|
|
||||||
removed.push(i);
|
|
||||||
removed.push(j);
|
|
||||||
|
|
||||||
let mut x: Vec<ProcessedNode> = x.clone();
|
|
||||||
x.reverse();
|
|
||||||
x.extend(y.iter().skip(1).cloned());
|
|
||||||
merged.push(x);
|
|
||||||
} else if nodes_match(x_last, y_last) {
|
|
||||||
removed.push(i);
|
|
||||||
removed.push(j);
|
|
||||||
|
|
||||||
let mut x: Vec<ProcessedNode> = x.clone();
|
|
||||||
x.extend(y.iter().rev().skip(1).cloned());
|
|
||||||
|
|
||||||
merged.push(x);
|
|
||||||
} else if nodes_match(x_first, y_last) {
|
|
||||||
removed.push(i);
|
|
||||||
removed.push(j);
|
|
||||||
|
|
||||||
let mut y: Vec<ProcessedNode> = y.clone();
|
|
||||||
y.extend(x.iter().skip(1).cloned());
|
|
||||||
|
|
||||||
merged.push(y);
|
|
||||||
} else if nodes_match(x_last, y_first) {
|
|
||||||
removed.push(i);
|
|
||||||
removed.push(j);
|
|
||||||
|
|
||||||
let mut x: Vec<ProcessedNode> = x.clone();
|
|
||||||
x.extend(y.iter().skip(1).cloned());
|
|
||||||
|
|
||||||
merged.push(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removed.sort();
|
|
||||||
|
|
||||||
for r in removed.iter().rev() {
|
|
||||||
rings.remove(*r);
|
|
||||||
}
|
|
||||||
|
|
||||||
let merged_len: usize = merged.len();
|
|
||||||
for m in merged {
|
|
||||||
rings.push(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
if merged_len > 0 {
|
|
||||||
merge_way_segments(rings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies all rings are properly closed (first node matches last).
|
/// Verifies all rings are properly closed (first node matches last).
|
||||||
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
||||||
let mut valid = true;
|
let mut valid = true;
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ use itertools::Itertools;
|
|||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// Maximum bounding box area (in blocks) for flood fill.
|
||||||
|
/// Polygons exceeding this are skipped to prevent multi-GB memory allocations.
|
||||||
|
/// 25 million blocks ≈ 5000×5000; HashSet would use ~700 MB at this size.
|
||||||
|
const MAX_FLOOD_FILL_AREA: i64 = 25_000_000;
|
||||||
|
|
||||||
/// Main flood fill function with automatic algorithm selection
|
/// Main flood fill function with automatic algorithm selection
|
||||||
/// Chooses the best algorithm based on polygon size and complexity
|
/// Chooses the best algorithm based on polygon size and complexity
|
||||||
pub fn flood_fill_area(
|
pub fn flood_fill_area(
|
||||||
@@ -29,6 +34,13 @@ pub fn flood_fill_area(
|
|||||||
|
|
||||||
let area = (max_x - min_x + 1) as i64 * (max_z - min_z + 1) as i64;
|
let area = (max_x - min_x + 1) as i64 * (max_z - min_z + 1) as i64;
|
||||||
|
|
||||||
|
// Safety cap: reject polygons whose bounding box is too large.
|
||||||
|
// This prevents multi-GB memory allocations when ocean-adjacent elements
|
||||||
|
// (e.g. natural=water, large landuse) produce huge clipped polygons.
|
||||||
|
if area > MAX_FLOOD_FILL_AREA {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
// For small and medium areas, use optimized flood fill with span filling
|
// For small and medium areas, use optimized flood fill with span filling
|
||||||
if area < 50000 {
|
if area < 50000 {
|
||||||
optimized_flood_fill_area(polygon_coords, timeout, min_x, max_x, min_z, max_z)
|
optimized_flood_fill_area(polygon_coords, timeout, min_x, max_x, min_z, max_z)
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ use fnv::FnvHashMap;
|
|||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// A memory-efficient bitmap for storing building footprint coordinates.
|
/// A memory-efficient bitmap for storing coordinates.
|
||||||
///
|
///
|
||||||
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
|
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
|
||||||
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
|
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
|
||||||
///
|
///
|
||||||
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
|
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
|
||||||
pub struct BuildingFootprintBitmap {
|
pub struct CoordinateBitmap {
|
||||||
/// The bitmap data, where each bit represents one (x, z) coordinate
|
/// The bitmap data, where each bit represents one (x, z) coordinate
|
||||||
bits: Vec<u8>,
|
bits: Vec<u8>,
|
||||||
/// Minimum x coordinate (offset for indexing)
|
/// Minimum x coordinate (offset for indexing)
|
||||||
@@ -27,12 +27,13 @@ pub struct BuildingFootprintBitmap {
|
|||||||
/// Width of the world (max_x - min_x + 1)
|
/// Width of the world (max_x - min_x + 1)
|
||||||
width: usize,
|
width: usize,
|
||||||
/// Height of the world (max_z - min_z + 1)
|
/// Height of the world (max_z - min_z + 1)
|
||||||
|
#[allow(dead_code)]
|
||||||
height: usize,
|
height: usize,
|
||||||
/// Number of coordinates marked as building footprints
|
/// Number of coordinates marked
|
||||||
count: usize,
|
count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildingFootprintBitmap {
|
impl CoordinateBitmap {
|
||||||
/// Creates a new empty bitmap covering the given world bounds.
|
/// Creates a new empty bitmap covering the given world bounds.
|
||||||
pub fn new(xzbbox: &XZBBox) -> Self {
|
pub fn new(xzbbox: &XZBBox) -> Self {
|
||||||
let min_x = xzbbox.min_x();
|
let min_x = xzbbox.min_x();
|
||||||
@@ -44,7 +45,7 @@ impl BuildingFootprintBitmap {
|
|||||||
// Calculate number of bytes needed (round up to nearest byte)
|
// Calculate number of bytes needed (round up to nearest byte)
|
||||||
let total_bits = width
|
let total_bits = width
|
||||||
.checked_mul(height)
|
.checked_mul(height)
|
||||||
.expect("BuildingFootprintBitmap: world size too large (width * height overflowed)");
|
.expect("CoordinateBitmap: world size too large (width * height overflowed)");
|
||||||
let num_bytes = total_bits.div_ceil(8);
|
let num_bytes = total_bits.div_ceil(8);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -79,7 +80,7 @@ impl BuildingFootprintBitmap {
|
|||||||
Some(local_z * self.width + local_x)
|
Some(local_z * self.width + local_x)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a coordinate as part of a building footprint.
|
/// Sets a coordinate.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set(&mut self, x: i32, z: i32) {
|
pub fn set(&mut self, x: i32, z: i32) {
|
||||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||||
@@ -96,7 +97,7 @@ impl BuildingFootprintBitmap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a coordinate is part of a building footprint.
|
/// Checks if a coordinate is set.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn contains(&self, x: i32, z: i32) -> bool {
|
pub fn contains(&self, x: i32, z: i32) -> bool {
|
||||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||||
@@ -111,12 +112,119 @@ impl BuildingFootprintBitmap {
|
|||||||
|
|
||||||
/// Returns true if no coordinates are marked.
|
/// Returns true if no coordinates are marked.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(dead_code)] // Standard API method for collection-like types
|
#[allow(dead_code)]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.count == 0
|
self.count == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the number of coordinates that are set.
|
||||||
|
#[inline]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counts how many coordinates from the given iterator are set in this bitmap.
|
||||||
|
#[inline]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn count_contained<'a, I>(&self, coords: I) -> usize
|
||||||
|
where
|
||||||
|
I: Iterator<Item = &'a (i32, i32)>,
|
||||||
|
{
|
||||||
|
coords.filter(|(x, z)| self.contains(*x, *z)).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counts the number of set bits in a rectangular range.
|
||||||
|
///
|
||||||
|
/// This is optimized to iterate row-by-row and use `count_ones()` on bytes
|
||||||
|
/// where possible, which is much faster than checking individual coordinates.
|
||||||
|
///
|
||||||
|
/// Returns `(urban_count, total_count)` for the given range.
|
||||||
|
#[inline]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn count_in_range(&self, min_x: i32, min_z: i32, max_x: i32, max_z: i32) -> (usize, usize) {
|
||||||
|
let mut urban_count = 0usize;
|
||||||
|
let mut total_count = 0usize;
|
||||||
|
|
||||||
|
for z in min_z..=max_z {
|
||||||
|
// Calculate local z coordinate
|
||||||
|
let local_z = i64::from(z) - i64::from(self.min_z);
|
||||||
|
if local_z < 0 || local_z >= self.height as i64 {
|
||||||
|
// Row is out of bounds, still counts toward total
|
||||||
|
total_count += (i64::from(max_x) - i64::from(min_x) + 1) as usize;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let local_z = local_z as usize;
|
||||||
|
|
||||||
|
// Calculate x range in local coordinates
|
||||||
|
let local_min_x = (i64::from(min_x) - i64::from(self.min_x)).max(0) as usize;
|
||||||
|
let local_max_x =
|
||||||
|
((i64::from(max_x) - i64::from(self.min_x)) as usize).min(self.width - 1);
|
||||||
|
|
||||||
|
// Count out-of-bounds x coordinates toward total
|
||||||
|
let x_start_offset = (i64::from(self.min_x) - i64::from(min_x)).max(0) as usize;
|
||||||
|
let x_end_offset = (i64::from(max_x) - i64::from(self.min_x) - (self.width as i64 - 1))
|
||||||
|
.max(0) as usize;
|
||||||
|
total_count += x_start_offset + x_end_offset;
|
||||||
|
|
||||||
|
if local_min_x > local_max_x {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process this row
|
||||||
|
let row_start_bit = local_z * self.width + local_min_x;
|
||||||
|
let row_end_bit = local_z * self.width + local_max_x;
|
||||||
|
let num_bits = row_end_bit - row_start_bit + 1;
|
||||||
|
total_count += num_bits;
|
||||||
|
|
||||||
|
// Count set bits using byte-wise popcount where possible
|
||||||
|
let start_byte = row_start_bit / 8;
|
||||||
|
let end_byte = row_end_bit / 8;
|
||||||
|
let start_bit_in_byte = row_start_bit % 8;
|
||||||
|
let end_bit_in_byte = row_end_bit % 8;
|
||||||
|
|
||||||
|
if start_byte == end_byte {
|
||||||
|
// All bits are in the same byte
|
||||||
|
let byte = self.bits[start_byte];
|
||||||
|
// Create mask for bits from start_bit to end_bit (inclusive)
|
||||||
|
let num_bits_in_mask = end_bit_in_byte - start_bit_in_byte + 1;
|
||||||
|
let mask = if num_bits_in_mask >= 8 {
|
||||||
|
0xFFu8
|
||||||
|
} else {
|
||||||
|
((1u16 << num_bits_in_mask) - 1) as u8
|
||||||
|
};
|
||||||
|
let masked = (byte >> start_bit_in_byte) & mask;
|
||||||
|
urban_count += masked.count_ones() as usize;
|
||||||
|
} else {
|
||||||
|
// First partial byte
|
||||||
|
let first_byte = self.bits[start_byte];
|
||||||
|
let first_mask = !((1u8 << start_bit_in_byte) - 1); // bits from start_bit to 7
|
||||||
|
urban_count += (first_byte & first_mask).count_ones() as usize;
|
||||||
|
|
||||||
|
// Full bytes in between
|
||||||
|
for byte_idx in (start_byte + 1)..end_byte {
|
||||||
|
urban_count += self.bits[byte_idx].count_ones() as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last partial byte
|
||||||
|
let last_byte = self.bits[end_byte];
|
||||||
|
// Handle case where end_bit_in_byte is 7 (would overflow 1u8 << 8)
|
||||||
|
let last_mask = if end_bit_in_byte >= 7 {
|
||||||
|
0xFFu8
|
||||||
|
} else {
|
||||||
|
(1u8 << (end_bit_in_byte + 1)) - 1
|
||||||
|
};
|
||||||
|
urban_count += (last_byte & last_mask).count_ones() as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(urban_count, total_count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Type alias for building footprint bitmap (for backwards compatibility).
|
||||||
|
pub type BuildingFootprintBitmap = CoordinateBitmap;
|
||||||
|
|
||||||
/// A cache of pre-computed flood fill results, keyed by element ID.
|
/// A cache of pre-computed flood fill results, keyed by element ID.
|
||||||
pub struct FloodFillCache {
|
pub struct FloodFillCache {
|
||||||
/// Cached results: element_id -> filled coordinates
|
/// Cached results: element_id -> filled coordinates
|
||||||
@@ -222,6 +330,7 @@ impl FloodFillCache {
|
|||||||
fn way_needs_flood_fill(way: &ProcessedWay) -> bool {
|
fn way_needs_flood_fill(way: &ProcessedWay) -> bool {
|
||||||
way.tags.contains_key("building")
|
way.tags.contains_key("building")
|
||||||
|| way.tags.contains_key("building:part")
|
|| way.tags.contains_key("building:part")
|
||||||
|
|| way.tags.contains_key("boundary")
|
||||||
|| way.tags.contains_key("landuse")
|
|| way.tags.contains_key("landuse")
|
||||||
|| way.tags.contains_key("leisure")
|
|| way.tags.contains_key("leisure")
|
||||||
|| way.tags.contains_key("amenity")
|
|| way.tags.contains_key("amenity")
|
||||||
@@ -282,6 +391,61 @@ impl FloodFillCache {
|
|||||||
footprints
|
footprints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects centroids of all buildings from the pre-computed cache.
|
||||||
|
///
|
||||||
|
/// This is used for urban ground detection - building clusters are identified
|
||||||
|
/// using their centroids, and a concave hull is computed around dense clusters
|
||||||
|
/// to determine where city ground (smooth stone) should be placed.
|
||||||
|
///
|
||||||
|
/// Returns a vector of (x, z) centroid coordinates for all buildings.
|
||||||
|
pub fn collect_building_centroids(&self, elements: &[ProcessedElement]) -> Vec<(i32, i32)> {
|
||||||
|
let mut centroids = Vec::new();
|
||||||
|
|
||||||
|
for element in elements {
|
||||||
|
match element {
|
||||||
|
ProcessedElement::Way(way) => {
|
||||||
|
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||||
|
if let Some(cached) = self.way_cache.get(&way.id) {
|
||||||
|
if let Some(centroid) = Self::compute_centroid(cached) {
|
||||||
|
centroids.push(centroid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProcessedElement::Relation(rel) => {
|
||||||
|
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||||
|
// For building relations, compute centroid from outer ways
|
||||||
|
let mut all_coords = Vec::new();
|
||||||
|
for member in &rel.members {
|
||||||
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
|
if let Some(cached) = self.way_cache.get(&member.way.id) {
|
||||||
|
all_coords.extend(cached.iter().copied());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(centroid) = Self::compute_centroid(&all_coords) {
|
||||||
|
centroids.push(centroid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
centroids
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the centroid of a set of coordinates.
|
||||||
|
fn compute_centroid(coords: &[(i32, i32)]) -> Option<(i32, i32)> {
|
||||||
|
if coords.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sum_x: i64 = coords.iter().map(|(x, _)| i64::from(*x)).sum();
|
||||||
|
let sum_z: i64 = coords.iter().map(|(_, z)| i64::from(*z)).sum();
|
||||||
|
let len = coords.len() as i64;
|
||||||
|
Some(((sum_x / len) as i32, (sum_z / len) as i32))
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes a way's cached flood fill result, freeing memory.
|
/// Removes a way's cached flood fill result, freeing memory.
|
||||||
///
|
///
|
||||||
/// Call this after processing an element to release its cached data.
|
/// Call this after processing an element to release its cached data.
|
||||||
|
|||||||
290
src/gui.rs
290
src/gui.rs
@@ -62,24 +62,6 @@ impl Drop for SessionLock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Desktop directory for Bedrock .mcworld file output.
|
|
||||||
fn get_bedrock_output_directory() -> PathBuf {
|
|
||||||
dirs::desktop_dir()
|
|
||||||
.or_else(dirs::home_dir)
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the area name for a given bounding box using the center point
|
|
||||||
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
|
||||||
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
|
||||||
let center_lon = (bbox.min().lng() + bbox.max().lng()) / 2.0;
|
|
||||||
|
|
||||||
match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
|
||||||
Ok(Some(name)) => name,
|
|
||||||
_ => "Unknown Location".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_gui() {
|
pub fn run_gui() {
|
||||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
// Configure thread pool with 90% CPU cap to keep system responsive
|
||||||
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
|
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
|
||||||
@@ -123,7 +105,10 @@ pub fn run_gui() {
|
|||||||
)
|
)
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
gui_select_world,
|
gui_create_world,
|
||||||
|
gui_get_default_save_path,
|
||||||
|
gui_set_save_path,
|
||||||
|
gui_pick_save_directory,
|
||||||
gui_start_generation,
|
gui_start_generation,
|
||||||
gui_get_version,
|
gui_get_version,
|
||||||
gui_check_for_updates,
|
gui_check_for_updates,
|
||||||
@@ -141,15 +126,17 @@ pub fn run_gui() {
|
|||||||
.expect("Error while starting the application UI (Tauri)");
|
.expect("Error while starting the application UI (Tauri)");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
/// Detects the default Minecraft Java Edition saves directory for the current OS.
|
||||||
fn gui_select_world(generate_new: bool) -> Result<String, i32> {
|
/// Checks standard install paths including Flatpak on Linux.
|
||||||
// Determine the default Minecraft 'saves' directory based on the OS
|
/// Falls back to Desktop, then current directory.
|
||||||
let default_dir: Option<PathBuf> = if cfg!(target_os = "windows") {
|
fn detect_minecraft_saves_directory() -> PathBuf {
|
||||||
|
// Try standard Minecraft saves directories per OS
|
||||||
|
let mc_saves: Option<PathBuf> = if cfg!(target_os = "windows") {
|
||||||
env::var("APPDATA")
|
env::var("APPDATA")
|
||||||
.ok()
|
.ok()
|
||||||
.map(|appdata: String| PathBuf::from(appdata).join(".minecraft").join("saves"))
|
.map(|appdata| PathBuf::from(appdata).join(".minecraft").join("saves"))
|
||||||
} else if cfg!(target_os = "macos") {
|
} else if cfg!(target_os = "macos") {
|
||||||
dirs::home_dir().map(|home: PathBuf| {
|
dirs::home_dir().map(|home| {
|
||||||
home.join("Library/Application Support/minecraft")
|
home.join("Library/Application Support/minecraft")
|
||||||
.join("saves")
|
.join("saves")
|
||||||
})
|
})
|
||||||
@@ -166,174 +153,75 @@ fn gui_select_world(generate_new: bool) -> Result<String, i32> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if generate_new {
|
if let Some(saves_dir) = mc_saves {
|
||||||
// Handle new world generation
|
if saves_dir.exists() {
|
||||||
// Try Minecraft saves directory first, fall back to current directory
|
return saves_dir;
|
||||||
let target_path = if let Some(default_path) = &default_dir {
|
|
||||||
if default_path.exists() {
|
|
||||||
default_path.clone()
|
|
||||||
} else {
|
|
||||||
// Minecraft directory doesn't exist, use current directory
|
|
||||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No default directory configured, use current directory
|
|
||||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
|
||||||
};
|
|
||||||
|
|
||||||
create_new_world(&target_path).map_err(|_| 3) // Error code 3: Failed to create new world
|
|
||||||
} else {
|
|
||||||
// Handle existing world selection
|
|
||||||
// Open the directory picker dialog
|
|
||||||
let dialog: FileDialog = FileDialog::new();
|
|
||||||
let dialog: FileDialog = if let Some(start_dir) = default_dir.filter(|dir| dir.exists()) {
|
|
||||||
dialog.set_directory(start_dir)
|
|
||||||
} else {
|
|
||||||
dialog
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(path) = dialog.pick_folder() {
|
|
||||||
// Check if the "region" folder exists within the selected directory
|
|
||||||
if path.join("region").exists() {
|
|
||||||
// Check the 'session.lock' file
|
|
||||||
let session_lock_path = path.join("session.lock");
|
|
||||||
if session_lock_path.exists() {
|
|
||||||
// Try to acquire a lock on the session.lock file
|
|
||||||
if let Ok(file) = fs::File::open(&session_lock_path) {
|
|
||||||
if fs2::FileExt::try_lock_shared(&file).is_err() {
|
|
||||||
return Err(2); // Error code 2: The selected world is currently in use
|
|
||||||
} else {
|
|
||||||
// Release the lock immediately
|
|
||||||
let _ = fs2::FileExt::unlock(&file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(path.display().to_string());
|
|
||||||
} else {
|
|
||||||
// No Minecraft directory found, generating new world in custom user selected directory
|
|
||||||
return create_new_world(&path).map_err(|_| 3); // Error code 3: Failed to create new world
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If no folder was selected, return an error message
|
// Fallback to Desktop
|
||||||
Err(4) // Error code 4: No world selected
|
if let Some(desktop) = dirs::desktop_dir() {
|
||||||
|
if desktop.exists() {
|
||||||
|
return desktop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: current directory
|
||||||
|
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default save path (auto-detected on first run).
|
||||||
|
/// The frontend stores/retrieves this via localStorage and passes it here for validation.
|
||||||
|
#[tauri::command]
|
||||||
|
fn gui_get_default_save_path() -> String {
|
||||||
|
detect_minecraft_saves_directory().display().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates and returns a user-provided save path.
|
||||||
|
/// Returns the path string if valid, or an error message.
|
||||||
|
#[tauri::command]
|
||||||
|
fn gui_set_save_path(path: String) -> Result<String, String> {
|
||||||
|
let p = PathBuf::from(&path);
|
||||||
|
if !p.exists() {
|
||||||
|
return Err("Path does not exist.".to_string());
|
||||||
|
}
|
||||||
|
if !p.is_dir() {
|
||||||
|
return Err("Path is not a directory.".to_string());
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a native folder-picker dialog and returns the chosen path.
|
||||||
|
#[tauri::command]
|
||||||
|
fn gui_pick_save_directory(start_path: String) -> Result<String, String> {
|
||||||
|
let start = PathBuf::from(&start_path);
|
||||||
|
let mut dialog = FileDialog::new();
|
||||||
|
if start.is_dir() {
|
||||||
|
dialog = dialog.set_directory(&start);
|
||||||
|
}
|
||||||
|
match dialog.pick_folder() {
|
||||||
|
Some(folder) => Ok(folder.display().to_string()),
|
||||||
|
None => Ok(start_path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_new_world(base_path: &Path) -> Result<String, String> {
|
/// Creates a new Java Edition world in the given base save directory.
|
||||||
// Generate a unique world name with proper counter
|
/// Called when the user clicks "Create World".
|
||||||
// Check for both "Arnis World X" and "Arnis World X: Location" patterns
|
#[tauri::command]
|
||||||
let mut counter: i32 = 1;
|
fn gui_create_world(save_path: String) -> Result<String, i32> {
|
||||||
let unique_name: String = loop {
|
let trimmed = save_path.trim();
|
||||||
let candidate_name: String = format!("Arnis World {counter}");
|
if trimmed.is_empty() {
|
||||||
let candidate_path: PathBuf = base_path.join(&candidate_name);
|
return Err(3);
|
||||||
|
|
||||||
// Check for exact match (no location suffix)
|
|
||||||
let exact_match_exists = candidate_path.exists();
|
|
||||||
|
|
||||||
// Check for worlds with location suffix (Arnis World X: Location)
|
|
||||||
let location_pattern = format!("Arnis World {counter}: ");
|
|
||||||
let location_match_exists = fs::read_dir(base_path)
|
|
||||||
.map(|entries| {
|
|
||||||
entries
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
|
||||||
.any(|name| name.starts_with(&location_pattern))
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if !exact_match_exists && !location_match_exists {
|
|
||||||
break candidate_name;
|
|
||||||
}
|
|
||||||
counter += 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_world_path: PathBuf = base_path.join(&unique_name);
|
|
||||||
|
|
||||||
// Create the new world directory structure
|
|
||||||
fs::create_dir_all(new_world_path.join("region"))
|
|
||||||
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
|
||||||
|
|
||||||
// Copy the region template file
|
|
||||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/region.template");
|
|
||||||
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
|
||||||
fs::write(®ion_path, REGION_TEMPLATE)
|
|
||||||
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
|
||||||
|
|
||||||
// Add the level.dat file
|
|
||||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/level.dat");
|
|
||||||
|
|
||||||
// Decompress the gzipped level.template
|
|
||||||
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
|
||||||
let mut decompressed_data = Vec::new();
|
|
||||||
decoder
|
|
||||||
.read_to_end(&mut decompressed_data)
|
|
||||||
.map_err(|e| format!("Failed to decompress level.template: {e}"))?;
|
|
||||||
|
|
||||||
// Parse the decompressed NBT data
|
|
||||||
let mut level_data: Value = fastnbt::from_bytes(&decompressed_data)
|
|
||||||
.map_err(|e| format!("Failed to parse level.dat template: {e}"))?;
|
|
||||||
|
|
||||||
// Modify the LevelName, LastPlayed and player position fields
|
|
||||||
if let Value::Compound(ref mut root) = level_data {
|
|
||||||
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
|
||||||
// Update LevelName
|
|
||||||
data.insert("LevelName".to_string(), Value::String(unique_name.clone()));
|
|
||||||
|
|
||||||
// Update LastPlayed to the current Unix time in milliseconds
|
|
||||||
let current_time = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.map_err(|e| format!("Failed to get current time: {e}"))?;
|
|
||||||
let current_time_millis = current_time.as_millis() as i64;
|
|
||||||
data.insert("LastPlayed".to_string(), Value::Long(current_time_millis));
|
|
||||||
|
|
||||||
// Update player position and rotation
|
|
||||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
|
||||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
|
||||||
if let Value::Double(ref mut x) = pos.get_mut(0).unwrap() {
|
|
||||||
*x = -5.0;
|
|
||||||
}
|
|
||||||
if let Value::Double(ref mut y) = pos.get_mut(1).unwrap() {
|
|
||||||
*y = -61.0;
|
|
||||||
}
|
|
||||||
if let Value::Double(ref mut z) = pos.get_mut(2).unwrap() {
|
|
||||||
*z = -5.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Value::List(ref mut rot)) = player.get_mut("Rotation") {
|
|
||||||
if let Value::Float(ref mut x) = rot.get_mut(0).unwrap() {
|
|
||||||
*x = -45.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let base = PathBuf::from(trimmed);
|
||||||
|
if !base.is_dir() {
|
||||||
|
return Err(3); // Error code 3: Failed to create new world
|
||||||
|
}
|
||||||
|
create_new_world(&base).map_err(|_| 3)
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize the updated NBT data back to bytes
|
fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||||
let serialized_level_data: Vec<u8> = fastnbt::to_bytes(&level_data)
|
crate::world_utils::create_new_world(base_path)
|
||||||
.map_err(|e| format!("Failed to serialize updated level.dat: {e}"))?;
|
|
||||||
|
|
||||||
// Compress the serialized data back to gzip
|
|
||||||
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
|
||||||
encoder
|
|
||||||
.write_all(&serialized_level_data)
|
|
||||||
.map_err(|e| format!("Failed to compress updated level.dat: {e}"))?;
|
|
||||||
let compressed_level_data = encoder
|
|
||||||
.finish()
|
|
||||||
.map_err(|e| format!("Failed to finalize compression for level.dat: {e}"))?;
|
|
||||||
|
|
||||||
// Write the level.dat file
|
|
||||||
fs::write(new_world_path.join("level.dat"), compressed_level_data)
|
|
||||||
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
|
||||||
|
|
||||||
// Add the icon.png file
|
|
||||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/icon.png");
|
|
||||||
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
|
||||||
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
|
||||||
|
|
||||||
Ok(new_world_path.display().to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds localized area name to the world name in level.dat
|
/// Adds localized area name to the world name in level.dat
|
||||||
@@ -717,12 +605,22 @@ fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, St
|
|||||||
.as_f64()
|
.as_f64()
|
||||||
.ok_or("Missing maxGeoLon in metadata")?;
|
.ok_or("Missing maxGeoLon in metadata")?;
|
||||||
|
|
||||||
|
// Extract Minecraft coordinate bounds
|
||||||
|
let min_mc_x = metadata["minMcX"].as_i64().unwrap_or(0) as i32;
|
||||||
|
let max_mc_x = metadata["maxMcX"].as_i64().unwrap_or(0) as i32;
|
||||||
|
let min_mc_z = metadata["minMcZ"].as_i64().unwrap_or(0) as i32;
|
||||||
|
let max_mc_z = metadata["maxMcZ"].as_i64().unwrap_or(0) as i32;
|
||||||
|
|
||||||
Ok(Some(WorldMapData {
|
Ok(Some(WorldMapData {
|
||||||
image_base64: format!("data:image/png;base64,{}", base64_image),
|
image_base64: format!("data:image/png;base64,{}", base64_image),
|
||||||
min_lat,
|
min_lat,
|
||||||
max_lat,
|
max_lat,
|
||||||
min_lon,
|
min_lon,
|
||||||
max_lon,
|
max_lon,
|
||||||
|
min_mc_x,
|
||||||
|
max_mc_x,
|
||||||
|
min_mc_z,
|
||||||
|
max_mc_z,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,6 +632,11 @@ struct WorldMapData {
|
|||||||
max_lat: f64,
|
max_lat: f64,
|
||||||
min_lon: f64,
|
min_lon: f64,
|
||||||
max_lon: f64,
|
max_lon: f64,
|
||||||
|
// Minecraft coordinate bounds for coordinate copying
|
||||||
|
min_mc_x: i32,
|
||||||
|
max_mc_x: i32,
|
||||||
|
min_mc_z: i32,
|
||||||
|
max_mc_z: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
||||||
@@ -800,6 +703,7 @@ fn gui_start_generation(
|
|||||||
interior_enabled: bool,
|
interior_enabled: bool,
|
||||||
roof_enabled: bool,
|
roof_enabled: bool,
|
||||||
fillground_enabled: bool,
|
fillground_enabled: bool,
|
||||||
|
city_boundaries_enabled: bool,
|
||||||
is_new_world: bool,
|
is_new_world: bool,
|
||||||
spawn_point: Option<(f64, f64)>,
|
spawn_point: Option<(f64, f64)>,
|
||||||
telemetry_consent: bool,
|
telemetry_consent: bool,
|
||||||
@@ -933,11 +837,9 @@ fn gui_start_generation(
|
|||||||
}
|
}
|
||||||
WorldFormat::BedrockMcWorld => {
|
WorldFormat::BedrockMcWorld => {
|
||||||
// Bedrock: generate .mcworld on Desktop with location-based name
|
// Bedrock: generate .mcworld on Desktop with location-based name
|
||||||
let area_name = get_area_name_for_bedrock(&bbox);
|
let output_dir = crate::world_utils::get_bedrock_output_directory();
|
||||||
let filename = format!("Arnis {}.mcworld", area_name);
|
let (output_path, lvl_name) =
|
||||||
let lvl_name = format!("Arnis World: {}", area_name);
|
crate::world_utils::build_bedrock_output(&bbox, output_dir);
|
||||||
|
|
||||||
let output_path = get_bedrock_output_directory().join(&filename);
|
|
||||||
(output_path, Some(lvl_name))
|
(output_path, Some(lvl_name))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -980,11 +882,12 @@ fn gui_start_generation(
|
|||||||
bbox,
|
bbox,
|
||||||
file: None,
|
file: None,
|
||||||
save_json_file: None,
|
save_json_file: None,
|
||||||
path: if world_format == WorldFormat::JavaAnvil {
|
path: Some(if world_format == WorldFormat::JavaAnvil {
|
||||||
generation_path
|
generation_path
|
||||||
} else {
|
} else {
|
||||||
world_path
|
world_path
|
||||||
},
|
}),
|
||||||
|
bedrock: world_format == WorldFormat::BedrockMcWorld,
|
||||||
downloader: "requests".to_string(),
|
downloader: "requests".to_string(),
|
||||||
scale: world_scale,
|
scale: world_scale,
|
||||||
ground_level,
|
ground_level,
|
||||||
@@ -992,6 +895,7 @@ fn gui_start_generation(
|
|||||||
interior: interior_enabled,
|
interior: interior_enabled,
|
||||||
roof: roof_enabled,
|
roof: roof_enabled,
|
||||||
fillground: fillground_enabled,
|
fillground: fillground_enabled,
|
||||||
|
city_boundaries: city_boundaries_enabled,
|
||||||
debug: false,
|
debug: false,
|
||||||
timeout: Some(std::time::Duration::from_secs(40)),
|
timeout: Some(std::time::Duration::from_secs(40)),
|
||||||
};
|
};
|
||||||
|
|||||||
38
src/gui/css/bbox.css
vendored
38
src/gui/css/bbox.css
vendored
@@ -376,3 +376,41 @@ body,
|
|||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Context menu for coordinate copying */
|
||||||
|
.coordinate-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 10000;
|
||||||
|
min-width: 160px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinate-context-menu-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinate-context-menu-item:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinate-context-menu-item svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinate-context-menu-separator {
|
||||||
|
height: 1px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|||||||
42
src/gui/css/styles.css
vendored
42
src/gui/css/styles.css
vendored
@@ -417,6 +417,10 @@ button:hover {
|
|||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#city-boundaries-toggle {
|
||||||
|
accent-color: #fecc44;
|
||||||
|
}
|
||||||
|
|
||||||
#telemetry-toggle {
|
#telemetry-toggle {
|
||||||
accent-color: #fecc44;
|
accent-color: #fecc44;
|
||||||
}
|
}
|
||||||
@@ -574,6 +578,44 @@ button:hover {
|
|||||||
border: 1px solid #fecc44;
|
border: 1px solid #fecc44;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Save Path Setting */
|
||||||
|
.save-path-control {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-path-input {
|
||||||
|
max-width: 200px !important;
|
||||||
|
font-size: 0.85em;
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
border-bottom-right-radius: 0 !important;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-path-browse {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #fecc44;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin-top: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-path-browse:hover {
|
||||||
|
background: rgba(254, 204, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-path-browse svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
fill: #fecc44;
|
||||||
|
}
|
||||||
|
|
||||||
.license-button-row {
|
.license-button-row {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|||||||
42
src/gui/index.html
vendored
42
src/gui/index.html
vendored
@@ -32,11 +32,11 @@
|
|||||||
<!-- World Selection Container -->
|
<!-- World Selection Container -->
|
||||||
<div class="world-selection-container">
|
<div class="world-selection-container">
|
||||||
<div class="tooltip" style="width: 100%;">
|
<div class="tooltip" style="width: 100%;">
|
||||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
<button type="button" id="choose-world-btn" onclick="createWorld()" class="choose-world-btn">
|
||||||
<span id="choose_world">Choose World</span>
|
<span id="choose_world">Create World</span>
|
||||||
<br>
|
<br>
|
||||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||||
No world selected
|
No world created
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,17 +73,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- World Picker Modal -->
|
|
||||||
<div id="world-modal" class="modal" style="display: none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-button" onclick="closeWorldPicker()">×</span>
|
|
||||||
<h2 data-localize="choose_world_modal_title">Choose World</h2>
|
|
||||||
|
|
||||||
<button type="button" id="select-world-button" class="select-world-button" onclick="selectWorld(false)" data-localize="select_existing_world">Select existing world</button>
|
|
||||||
<button type="button" id="generate-world-button" class="generate-world-button" onclick="selectWorld(true)" data-localize="generate_new_world">Generate new world</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<div id="settings-modal" class="modal" style="display: none;">
|
<div id="settings-modal" class="modal" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -138,6 +127,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- City Ground Toggle Button -->
|
||||||
|
<div class="settings-row">
|
||||||
|
<label for="city-boundaries-toggle">
|
||||||
|
<span data-localize="city_boundaries">City Ground</span>
|
||||||
|
<span class="tooltip-icon" data-tooltip="Detect urban areas and place smooth stone ground where cities are located.">?</span>
|
||||||
|
</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<input type="checkbox" id="city-boundaries-toggle" name="city-boundaries-toggle" checked>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- World Scale Slider -->
|
<!-- World Scale Slider -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="scale-value-slider">
|
<label for="scale-value-slider">
|
||||||
@@ -178,6 +178,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Save Path -->
|
||||||
|
<div class="settings-row">
|
||||||
|
<label for="save-path-input">
|
||||||
|
<span data-localize="save_path">Save Path</span>
|
||||||
|
<span class="tooltip-icon" data-tooltip="Directory where new Minecraft worlds are created">?</span>
|
||||||
|
</label>
|
||||||
|
<div class="settings-control save-path-control">
|
||||||
|
<input type="text" id="save-path-input" name="save-path-input" class="save-path-input" placeholder="Minecraft saves directory">
|
||||||
|
<button type="button" id="save-path-browse" class="save-path-browse" title="Browse...">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Language Selector -->
|
<!-- Language Selector -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="language-select">
|
<label for="language-select">
|
||||||
|
|||||||
182
src/gui/js/bbox.js
vendored
182
src/gui/js/bbox.js
vendored
@@ -749,6 +749,188 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Context Menu for Coordinate Copying ==========
|
||||||
|
var contextMenuElement = null;
|
||||||
|
|
||||||
|
// Create the context menu element
|
||||||
|
function createContextMenu() {
|
||||||
|
if (contextMenuElement) return contextMenuElement;
|
||||||
|
|
||||||
|
contextMenuElement = document.createElement('div');
|
||||||
|
contextMenuElement.className = 'coordinate-context-menu';
|
||||||
|
contextMenuElement.style.display = 'none';
|
||||||
|
contextMenuElement.innerHTML = `
|
||||||
|
<div class="coordinate-context-menu-item" id="copy-coords-item">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
<span id="copy-coords-text">Copy coordinates</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(contextMenuElement);
|
||||||
|
|
||||||
|
// Handle click on the copy coordinates item
|
||||||
|
var copyItem = contextMenuElement.querySelector('#copy-coords-item');
|
||||||
|
copyItem.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
copyMinecraftCoordinates();
|
||||||
|
hideContextMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
return contextMenuElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show context menu at position
|
||||||
|
function showContextMenu(x, y, latLng) {
|
||||||
|
if (!worldPreviewAvailable || !worldOverlayData) return;
|
||||||
|
|
||||||
|
var menu = createContextMenu();
|
||||||
|
|
||||||
|
// Position the menu, ensuring it stays within viewport
|
||||||
|
var menuWidth = 180;
|
||||||
|
var menuHeight = 40;
|
||||||
|
var viewportWidth = window.innerWidth;
|
||||||
|
var viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
var posX = x;
|
||||||
|
var posY = y;
|
||||||
|
|
||||||
|
// Adjust if menu would go off-screen
|
||||||
|
if (x + menuWidth > viewportWidth) {
|
||||||
|
posX = viewportWidth - menuWidth - 10;
|
||||||
|
}
|
||||||
|
if (y + menuHeight > viewportHeight) {
|
||||||
|
posY = viewportHeight - menuHeight - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.style.left = posX + 'px';
|
||||||
|
menu.style.top = posY + 'px';
|
||||||
|
menu.style.display = 'block';
|
||||||
|
|
||||||
|
// Store the latLng for copying
|
||||||
|
menu.dataset.lat = latLng.lat;
|
||||||
|
menu.dataset.lng = latLng.lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide context menu
|
||||||
|
function hideContextMenu() {
|
||||||
|
if (contextMenuElement) {
|
||||||
|
contextMenuElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Minecraft coordinates from lat/lng
|
||||||
|
function calculateMinecraftCoords(lat, lng) {
|
||||||
|
if (!worldOverlayData) return null;
|
||||||
|
|
||||||
|
var data = worldOverlayData;
|
||||||
|
|
||||||
|
// Check if Minecraft coordinate bounds are available (not all zeros)
|
||||||
|
if (data.min_mc_x === 0 && data.max_mc_x === 0 &&
|
||||||
|
data.min_mc_z === 0 && data.max_mc_z === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the relative position within the geo bounds (0 to 1)
|
||||||
|
// Note: Latitude increases northward, but Minecraft Z increases southward
|
||||||
|
var relX = (lng - data.min_lon) / (data.max_lon - data.min_lon);
|
||||||
|
var relZ = (data.max_lat - lat) / (data.max_lat - data.min_lat);
|
||||||
|
|
||||||
|
// Clamp to 0-1 range
|
||||||
|
relX = Math.max(0, Math.min(1, relX));
|
||||||
|
relZ = Math.max(0, Math.min(1, relZ));
|
||||||
|
|
||||||
|
// Calculate Minecraft X and Z coordinates
|
||||||
|
var mcX = Math.round(data.min_mc_x + relX * (data.max_mc_x - data.min_mc_x));
|
||||||
|
var mcZ = Math.round(data.min_mc_z + relZ * (data.max_mc_z - data.min_mc_z));
|
||||||
|
|
||||||
|
// Default Y coordinate (ground level, typically around 64-70)
|
||||||
|
var mcY = 100;
|
||||||
|
|
||||||
|
return { x: mcX, y: mcY, z: mcZ };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy Minecraft coordinates to clipboard
|
||||||
|
function copyMinecraftCoordinates() {
|
||||||
|
if (!contextMenuElement) return;
|
||||||
|
|
||||||
|
var lat = parseFloat(contextMenuElement.dataset.lat);
|
||||||
|
var lng = parseFloat(contextMenuElement.dataset.lng);
|
||||||
|
|
||||||
|
var coords = calculateMinecraftCoords(lat, lng);
|
||||||
|
if (!coords) return;
|
||||||
|
|
||||||
|
var tpCommand = '/tp ' + coords.x + ' ' + coords.y + ' ' + coords.z;
|
||||||
|
|
||||||
|
// Copy to clipboard using modern API with fallback
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(tpCommand).catch(function(err) {
|
||||||
|
// Fallback for clipboard API failure
|
||||||
|
fallbackCopyToClipboard(tpCommand);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
fallbackCopyToClipboard(tpCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback clipboard copy method for older browsers
|
||||||
|
function fallbackCopyToClipboard(text) {
|
||||||
|
var textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-9999px';
|
||||||
|
textArea.style.top = '-9999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy coordinates:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Minecraft coordinate bounds are available
|
||||||
|
function hasMinecraftCoords() {
|
||||||
|
if (!worldOverlayData) return false;
|
||||||
|
var data = worldOverlayData;
|
||||||
|
return !(data.min_mc_x === 0 && data.max_mc_x === 0 &&
|
||||||
|
data.min_mc_z === 0 && data.max_mc_z === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle right-click on the map
|
||||||
|
map.on('contextmenu', function(e) {
|
||||||
|
// Only show context menu if world preview is available and has Minecraft coords
|
||||||
|
if (worldPreviewAvailable && worldOverlayData && hasMinecraftCoords()) {
|
||||||
|
// Check if the click is within the world bounds
|
||||||
|
var data = worldOverlayData;
|
||||||
|
var lat = e.latlng.lat;
|
||||||
|
var lng = e.latlng.lng;
|
||||||
|
|
||||||
|
if (lat >= data.min_lat && lat <= data.max_lat &&
|
||||||
|
lng >= data.min_lon && lng <= data.max_lon) {
|
||||||
|
showContextMenu(e.originalEvent.clientX, e.originalEvent.clientY, e.latlng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide context menu on any click or map interaction
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (contextMenuElement && !contextMenuElement.contains(e.target)) {
|
||||||
|
hideContextMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on('movestart', hideContextMenu);
|
||||||
|
map.on('zoomstart', hideContextMenu);
|
||||||
|
// ========== End Context Menu ==========
|
||||||
|
|
||||||
// Listen for messages from parent window
|
// Listen for messages from parent window
|
||||||
window.addEventListener('message', function(event) {
|
window.addEventListener('message', function(event) {
|
||||||
if (event.data && event.data.type === 'changeTileTheme') {
|
if (event.data && event.data.type === 'changeTileTheme') {
|
||||||
|
|||||||
197
src/gui/js/main.js
vendored
197
src/gui/js/main.js
vendored
@@ -34,11 +34,11 @@ async function setBboxSelectionInfo(bboxSelectionElement, localizationKey, color
|
|||||||
// Initialize elements and start the demo progress
|
// Initialize elements and start the demo progress
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
registerMessageEvent();
|
registerMessageEvent();
|
||||||
window.selectWorld = selectWorld;
|
window.createWorld = createWorld;
|
||||||
window.startGeneration = startGeneration;
|
window.startGeneration = startGeneration;
|
||||||
setupProgressListener();
|
setupProgressListener();
|
||||||
|
await initSavePath();
|
||||||
initSettings();
|
initSettings();
|
||||||
initWorldPicker();
|
|
||||||
initTelemetryConsent();
|
initTelemetryConsent();
|
||||||
handleBboxInput();
|
handleBboxInput();
|
||||||
const localization = await getLocalization();
|
const localization = await getLocalization();
|
||||||
@@ -97,27 +97,26 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
|||||||
|
|
||||||
async function applyLocalization(localization) {
|
async function applyLocalization(localization) {
|
||||||
const localizationElements = {
|
const localizationElements = {
|
||||||
"span[id='choose_world']": "choose_world",
|
"span[id='choose_world']": "create_world",
|
||||||
"#selected-world": "no_world_selected",
|
"#selected-world": "no_world_selected",
|
||||||
"#start-button": "start_generation",
|
"#start-button": "start_generation",
|
||||||
"h2[data-localize='choose_world_modal_title']": "choose_world_modal_title",
|
|
||||||
"button[data-localize='select_existing_world']": "select_existing_world",
|
|
||||||
"button[data-localize='generate_new_world']": "generate_new_world",
|
|
||||||
"h2[data-localize='customization_settings']": "customization_settings",
|
"h2[data-localize='customization_settings']": "customization_settings",
|
||||||
"label[data-localize='world_scale']": "world_scale",
|
"span[data-localize='world_scale']": "world_scale",
|
||||||
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
|
"span[data-localize='custom_bounding_box']": "custom_bounding_box",
|
||||||
// DEPRECATED: Ground level localization removed
|
// DEPRECATED: Ground level localization removed
|
||||||
// "label[data-localize='ground_level']": "ground_level",
|
// "label[data-localize='ground_level']": "ground_level",
|
||||||
"label[data-localize='language']": "language",
|
"span[data-localize='language']": "language",
|
||||||
"label[data-localize='generation_mode']": "generation_mode",
|
"span[data-localize='generation_mode']": "generation_mode",
|
||||||
"option[data-localize='mode_geo_terrain']": "mode_geo_terrain",
|
"option[data-localize='mode_geo_terrain']": "mode_geo_terrain",
|
||||||
"option[data-localize='mode_geo_only']": "mode_geo_only",
|
"option[data-localize='mode_geo_only']": "mode_geo_only",
|
||||||
"option[data-localize='mode_terrain_only']": "mode_terrain_only",
|
"option[data-localize='mode_terrain_only']": "mode_terrain_only",
|
||||||
"label[data-localize='terrain']": "terrain",
|
"span[data-localize='terrain']": "terrain",
|
||||||
"label[data-localize='interior']": "interior",
|
"span[data-localize='interior']": "interior",
|
||||||
"label[data-localize='roof']": "roof",
|
"span[data-localize='roof']": "roof",
|
||||||
"label[data-localize='fillground']": "fillground",
|
"span[data-localize='fillground']": "fillground",
|
||||||
"label[data-localize='map_theme']": "map_theme",
|
"span[data-localize='city_boundaries']": "city_boundaries",
|
||||||
|
"span[data-localize='map_theme']": "map_theme",
|
||||||
|
"span[data-localize='save_path']": "save_path",
|
||||||
".footer-link": "footer_text",
|
".footer-link": "footer_text",
|
||||||
"button[data-localize='license_and_credits']": "license_and_credits",
|
"button[data-localize='license_and_credits']": "license_and_credits",
|
||||||
"h2[data-localize='license_and_credits']": "license_and_credits",
|
"h2[data-localize='license_and_credits']": "license_and_credits",
|
||||||
@@ -297,6 +296,9 @@ function initSettings() {
|
|||||||
// World format toggle (Java/Bedrock)
|
// World format toggle (Java/Bedrock)
|
||||||
initWorldFormatToggle();
|
initWorldFormatToggle();
|
||||||
|
|
||||||
|
// Save path setting
|
||||||
|
initSavePathSetting();
|
||||||
|
|
||||||
// Language selector
|
// Language selector
|
||||||
const languageSelect = document.getElementById("language-select");
|
const languageSelect = document.getElementById("language-select");
|
||||||
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
||||||
@@ -335,6 +337,14 @@ function initSettings() {
|
|||||||
// Reload localization with the new language
|
// Reload localization with the new language
|
||||||
const localization = await fetchLanguage(selectedLanguage);
|
const localization = await fetchLanguage(selectedLanguage);
|
||||||
await applyLocalization(localization);
|
await applyLocalization(localization);
|
||||||
|
|
||||||
|
// Restore correct #selected-world text after localization overwrites it
|
||||||
|
updateFormatToggleUI(selectedWorldFormat);
|
||||||
|
// If a world was already created, show its name
|
||||||
|
if (worldPath) {
|
||||||
|
const lastSegment = worldPath.split(/[\\/]/).pop();
|
||||||
|
document.getElementById('selected-world').textContent = lastSegment;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tile theme selector
|
// Tile theme selector
|
||||||
@@ -430,22 +440,22 @@ function updateFormatToggleUI(format) {
|
|||||||
if (format === 'java') {
|
if (format === 'java') {
|
||||||
javaBtn.classList.add('format-active');
|
javaBtn.classList.add('format-active');
|
||||||
bedrockBtn.classList.remove('format-active');
|
bedrockBtn.classList.remove('format-active');
|
||||||
// Enable Choose World button for Java
|
// Enable Create World button for Java
|
||||||
if (chooseWorldBtn) {
|
if (chooseWorldBtn) {
|
||||||
chooseWorldBtn.disabled = false;
|
chooseWorldBtn.disabled = false;
|
||||||
chooseWorldBtn.style.opacity = '1';
|
chooseWorldBtn.style.opacity = '1';
|
||||||
chooseWorldBtn.style.cursor = 'pointer';
|
chooseWorldBtn.style.cursor = 'pointer';
|
||||||
}
|
}
|
||||||
// Show default text (world was cleared when switching to Bedrock)
|
// Show appropriate text based on whether a world was already created
|
||||||
if (selectedWorldText) {
|
if (selectedWorldText && !worldPath) {
|
||||||
const noWorldText = window.localization?.no_world_selected || 'No world selected';
|
const noWorldText = window.localization?.no_world_selected || 'No world created';
|
||||||
selectedWorldText.textContent = noWorldText;
|
selectedWorldText.textContent = noWorldText;
|
||||||
selectedWorldText.style.color = '#fecc44';
|
selectedWorldText.style.color = '#fecc44';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
javaBtn.classList.remove('format-active');
|
javaBtn.classList.remove('format-active');
|
||||||
bedrockBtn.classList.add('format-active');
|
bedrockBtn.classList.add('format-active');
|
||||||
// Disable Choose World button for Bedrock and clear any selected world
|
// Disable Create World button for Bedrock
|
||||||
if (chooseWorldBtn) {
|
if (chooseWorldBtn) {
|
||||||
chooseWorldBtn.disabled = true;
|
chooseWorldBtn.disabled = true;
|
||||||
chooseWorldBtn.style.opacity = '0.5';
|
chooseWorldBtn.style.opacity = '0.5';
|
||||||
@@ -453,9 +463,8 @@ function updateFormatToggleUI(format) {
|
|||||||
}
|
}
|
||||||
// Clear world selection and show Bedrock info message
|
// Clear world selection and show Bedrock info message
|
||||||
worldPath = "";
|
worldPath = "";
|
||||||
isNewWorld = false;
|
|
||||||
if (selectedWorldText) {
|
if (selectedWorldText) {
|
||||||
const bedrockText = window.localization?.bedrock_use_java || 'Use Java to select worlds';
|
const bedrockText = window.localization?.bedrock_auto_generated || 'Bedrock world is auto-generated';
|
||||||
selectedWorldText.textContent = bedrockText;
|
selectedWorldText.textContent = bedrockText;
|
||||||
selectedWorldText.style.color = '#fecc44';
|
selectedWorldText.style.color = '#fecc44';
|
||||||
}
|
}
|
||||||
@@ -508,24 +517,86 @@ function initTelemetryConsent() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initWorldPicker() {
|
/// Save path management
|
||||||
// World Picker
|
let savePath = "";
|
||||||
const worldPickerModal = document.getElementById("world-modal");
|
|
||||||
|
|
||||||
// Open world picker modal
|
async function initSavePath() {
|
||||||
function openWorldPicker() {
|
// Check if user has a saved path in localStorage
|
||||||
worldPickerModal.style.display = "flex";
|
const saved = localStorage.getItem('arnis-save-path');
|
||||||
worldPickerModal.style.justifyContent = "center";
|
if (saved) {
|
||||||
worldPickerModal.style.alignItems = "center";
|
// Validate the saved path still exists (handles upgrades / moved directories)
|
||||||
|
try {
|
||||||
|
const normalized = await invoke('gui_set_save_path', { path: saved });
|
||||||
|
savePath = normalized;
|
||||||
|
localStorage.setItem('arnis-save-path', savePath);
|
||||||
|
} catch (_) {
|
||||||
|
// Saved path is no longer valid – re-detect
|
||||||
|
console.warn("Stored save path no longer valid, re-detecting...");
|
||||||
|
localStorage.removeItem('arnis-save-path');
|
||||||
|
try {
|
||||||
|
savePath = await invoke('gui_get_default_save_path');
|
||||||
|
localStorage.setItem('arnis-save-path', savePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to detect save path:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Auto-detect on first run
|
||||||
|
try {
|
||||||
|
savePath = await invoke('gui_get_default_save_path');
|
||||||
|
localStorage.setItem('arnis-save-path', savePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to detect save path:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close world picker modal
|
// Populate the save path input in settings
|
||||||
function closeWorldPicker() {
|
const savePathInput = document.getElementById('save-path-input');
|
||||||
worldPickerModal.style.display = "none";
|
if (savePathInput) {
|
||||||
|
savePathInput.value = savePath;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.openWorldPicker = openWorldPicker;
|
function initSavePathSetting() {
|
||||||
window.closeWorldPicker = closeWorldPicker;
|
const savePathInput = document.getElementById('save-path-input');
|
||||||
|
if (!savePathInput) return;
|
||||||
|
|
||||||
|
savePathInput.value = savePath;
|
||||||
|
|
||||||
|
// Manual text input – validate on change, revert if invalid
|
||||||
|
savePathInput.addEventListener('change', async () => {
|
||||||
|
const newPath = savePathInput.value.trim();
|
||||||
|
if (!newPath) {
|
||||||
|
savePathInput.value = savePath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validated = await invoke('gui_set_save_path', { path: newPath });
|
||||||
|
savePath = validated;
|
||||||
|
localStorage.setItem('arnis-save-path', savePath);
|
||||||
|
} catch (_) {
|
||||||
|
// Invalid path – silently revert to previous value
|
||||||
|
savePathInput.value = savePath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folder picker button
|
||||||
|
const browseBtn = document.getElementById('save-path-browse');
|
||||||
|
if (browseBtn) {
|
||||||
|
browseBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const picked = await invoke('gui_pick_save_directory', { startPath: savePath });
|
||||||
|
if (picked) {
|
||||||
|
savePath = picked;
|
||||||
|
savePathInput.value = savePath;
|
||||||
|
localStorage.setItem('arnis-save-path', savePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Folder picker failed:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -710,57 +781,31 @@ function displayBboxInfoText(bboxText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let worldPath = "";
|
let worldPath = "";
|
||||||
let isNewWorld = false;
|
|
||||||
|
|
||||||
async function selectWorld(generate_new_world) {
|
async function createWorld() {
|
||||||
|
// Don't create if format is Bedrock (button should be disabled)
|
||||||
|
if (selectedWorldFormat === 'bedrock') return;
|
||||||
|
|
||||||
|
// Don't create if save path hasn't been initialized
|
||||||
|
if (!savePath) {
|
||||||
|
console.warn("Cannot create world: save path not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const worldName = await invoke('gui_select_world', { generateNew: generate_new_world });
|
const worldName = await invoke('gui_create_world', { savePath: savePath });
|
||||||
if (worldName) {
|
if (worldName) {
|
||||||
worldPath = worldName;
|
worldPath = worldName;
|
||||||
isNewWorld = generate_new_world;
|
|
||||||
const lastSegment = worldName.split(/[\\/]/).pop();
|
const lastSegment = worldName.split(/[\\/]/).pop();
|
||||||
document.getElementById('selected-world').textContent = lastSegment;
|
document.getElementById('selected-world').textContent = lastSegment;
|
||||||
document.getElementById('selected-world').style.color = "#fecc44";
|
document.getElementById('selected-world').style.color = "#fecc44";
|
||||||
|
|
||||||
// Notify that world changed (reset preview)
|
// Notify that world changed (reset preview)
|
||||||
notifyWorldChanged();
|
notifyWorldChanged();
|
||||||
|
|
||||||
// If selecting an existing world, check for existing map data
|
|
||||||
if (!generate_new_world) {
|
|
||||||
await loadExistingWorldMapData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleWorldSelectionError(error);
|
handleWorldSelectionError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeWorldPicker();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads existing world map data if available (for existing worlds)
|
|
||||||
* This will zoom to the location and auto-enable the preview
|
|
||||||
*/
|
|
||||||
async function loadExistingWorldMapData() {
|
|
||||||
if (!worldPath) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
|
||||||
if (mapData) {
|
|
||||||
currentWorldMapData = mapData;
|
|
||||||
|
|
||||||
// Send data to the map iframe with instruction to zoom and auto-enable
|
|
||||||
const mapFrame = document.querySelector('.map-container');
|
|
||||||
if (mapFrame && mapFrame.contentWindow) {
|
|
||||||
mapFrame.contentWindow.postMessage({
|
|
||||||
type: 'loadExistingWorldMap',
|
|
||||||
data: mapData
|
|
||||||
}, '*');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("No existing world map data found:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -801,10 +846,10 @@ async function startGeneration() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only require world selection for Java format (Bedrock generates a new .mcworld file)
|
// Only require world creation for Java format (Bedrock generates a new .mcworld file)
|
||||||
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
|
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
|
||||||
const selectedWorld = document.getElementById('selected-world');
|
const selectedWorld = document.getElementById('selected-world');
|
||||||
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
|
localizeElement(window.localization, { element: selectedWorld }, "create_world_first");
|
||||||
selectedWorld.style.color = "#fa7878";
|
selectedWorld.style.color = "#fa7878";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -832,6 +877,7 @@ async function startGeneration() {
|
|||||||
var interior = document.getElementById("interior-toggle").checked;
|
var interior = document.getElementById("interior-toggle").checked;
|
||||||
var roof = document.getElementById("roof-toggle").checked;
|
var roof = document.getElementById("roof-toggle").checked;
|
||||||
var fill_ground = document.getElementById("fillground-toggle").checked;
|
var fill_ground = document.getElementById("fillground-toggle").checked;
|
||||||
|
var city_boundaries = document.getElementById("city-boundaries-toggle").checked;
|
||||||
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
||||||
// var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
// var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
||||||
// DEPRECATED: Ground level input removed from UI
|
// DEPRECATED: Ground level input removed from UI
|
||||||
@@ -854,7 +900,8 @@ async function startGeneration() {
|
|||||||
interiorEnabled: interior,
|
interiorEnabled: interior,
|
||||||
roofEnabled: roof,
|
roofEnabled: roof,
|
||||||
fillgroundEnabled: fill_ground,
|
fillgroundEnabled: fill_ground,
|
||||||
isNewWorld: isNewWorld,
|
cityBoundariesEnabled: city_boundaries,
|
||||||
|
isNewWorld: true,
|
||||||
spawnPoint: spawnPoint,
|
spawnPoint: spawnPoint,
|
||||||
telemetryConsent: telemetryConsent || false,
|
telemetryConsent: telemetryConsent || false,
|
||||||
worldFormat: selectedWorldFormat
|
worldFormat: selectedWorldFormat
|
||||||
|
|||||||
4
src/gui/js/maps/wkt.parser.js
vendored
4
src/gui/js/maps/wkt.parser.js
vendored
@@ -195,11 +195,7 @@ Wkt.Wkt.prototype.toObject = function (config) {
|
|||||||
* Absorbs the geometry of another Wkt.Wkt instance, merging it with its own,
|
* Absorbs the geometry of another Wkt.Wkt instance, merging it with its own,
|
||||||
* creating a collection (MULTI-geometry) based on their types, which must agree.
|
* creating a collection (MULTI-geometry) based on their types, which must agree.
|
||||||
* For example, creates a MULTIPOLYGON from a POLYGON type merged with another
|
* For example, creates a MULTIPOLYGON from a POLYGON type merged with another
|
||||||
<<<<<<< HEAD
|
|
||||||
* POLYGON type.
|
|
||||||
=======
|
|
||||||
* POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
|
* POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
|
||||||
>>>>>>> dev
|
|
||||||
* @memberof Wkt.Wkt
|
* @memberof Wkt.Wkt
|
||||||
* @method
|
* @method
|
||||||
*/
|
*/
|
||||||
|
|||||||
13
src/gui/locales/ar.json
vendored
13
src/gui/locales/ar.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "اختيار عالم",
|
"create_world": "إنشاء عالم",
|
||||||
"no_world_selected": "لم يتم تحديد عالم",
|
"no_world_selected": "لم يتم إنشاء عالم",
|
||||||
"start_generation": "بدء البناء",
|
"start_generation": "بدء البناء",
|
||||||
"custom_selection_confirmed": "تم تأكيد التحديد المخصص!",
|
"custom_selection_confirmed": "تم تأكيد التحديد المخصص!",
|
||||||
"error_coordinates_out_of_range": "خطأ: الإحداثيات خارج النطاق أو مرتبة بشكل غير صحيح (مطلوب خط العرض قبل خط الطول).",
|
"error_coordinates_out_of_range": "خطأ: الإحداثيات خارج النطاق أو مرتبة بشكل غير صحيح (مطلوب خط العرض قبل خط الطول).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "مربع الحدود المخصص",
|
"custom_bounding_box": "مربع الحدود المخصص",
|
||||||
"floodfill_timeout": "مهلة ملء الفيضان (ثواني)",
|
"floodfill_timeout": "مهلة ملء الفيضان (ثواني)",
|
||||||
"ground_level": "مستوى الأرض",
|
"ground_level": "مستوى الأرض",
|
||||||
"choose_world_modal_title": "اختيار عالم",
|
|
||||||
"select_existing_world": "اختيار عالم موجود مسبقًا",
|
|
||||||
"generate_new_world": "إنشاء عالم جديد",
|
|
||||||
"customization_settings": "إعدادات التخصيص",
|
"customization_settings": "إعدادات التخصيص",
|
||||||
"footer_text": "© {year} Arnis v{version} من louis-e",
|
"footer_text": "© {year} Arnis v{version} من louis-e",
|
||||||
"new_version_available": "هناك نسخة جديدة متاحة! انقر هنا لتنزيلها.",
|
"new_version_available": "هناك نسخة جديدة متاحة! انقر هنا لتنزيلها.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "العالم المحدد قيد الاستخدام حاليًا",
|
"world_in_use": "العالم المحدد قيد الاستخدام حاليًا",
|
||||||
"failed_to_create_world": "حدث خطأ عند محاولة إنشاء عالم جديد",
|
"failed_to_create_world": "حدث خطأ عند محاولة إنشاء عالم جديد",
|
||||||
"no_world_selected_error": "لم يتم تحديد عالم",
|
"no_world_selected_error": "لم يتم تحديد عالم",
|
||||||
"select_minecraft_world_first": "يرجى تحديد عالم ماين كرافت أولاً!",
|
"create_world_first": "أنشئ عالمًا أولاً!",
|
||||||
"select_location_first": "يرجى اختيار موقع أولاً!",
|
"select_location_first": "يرجى اختيار موقع أولاً!",
|
||||||
"area_too_large": "تُعتبر هذه المنطقة كبيرة جدًا وقد تتجاوز حدود الحوسبة النموذجية.",
|
"area_too_large": "تُعتبر هذه المنطقة كبيرة جدًا وقد تتجاوز حدود الحوسبة النموذجية.",
|
||||||
"area_extensive": "المنطقة واسعة جدًا وقد تتطلب الكثير من الوقت والموارد.",
|
"area_extensive": "المنطقة واسعة جدًا وقد تتطلب الكثير من الوقت والموارد.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "توليد الداخلية",
|
"interior": "توليد الداخلية",
|
||||||
"roof": "توليد السقف",
|
"roof": "توليد السقف",
|
||||||
"fillground": "ملء الأرض",
|
"fillground": "ملء الأرض",
|
||||||
"bedrock_use_java": "استخدم Java لاختيار العوالم"
|
"city_boundaries": "أرضية المدينة",
|
||||||
|
"bedrock_auto_generated": "يتم إنشاء عالم Bedrock تلقائيًا",
|
||||||
|
"save_path": "مسار الحفظ"
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/gui/locales/de.json
vendored
13
src/gui/locales/de.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Welt wählen",
|
"create_world": "Welt erstellen",
|
||||||
"no_world_selected": "Keine Welt ausgewählt",
|
"no_world_selected": "Keine Welt erstellt",
|
||||||
"start_generation": "Generierung starten",
|
"start_generation": "Generierung starten",
|
||||||
"custom_selection_confirmed": "Benutzerdefinierte Auswahl bestätigt!",
|
"custom_selection_confirmed": "Benutzerdefinierte Auswahl bestätigt!",
|
||||||
"error_coordinates_out_of_range": "Fehler: Koordinaten sind außerhalb des Bereichs oder falsch geordnet (Lat vor Lng erforderlich).",
|
"error_coordinates_out_of_range": "Fehler: Koordinaten sind außerhalb des Bereichs oder falsch geordnet (Lat vor Lng erforderlich).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Benutzerdefinierte BBOX",
|
"custom_bounding_box": "Benutzerdefinierte BBOX",
|
||||||
"floodfill_timeout": "Floodfill-Timeout (Sek)",
|
"floodfill_timeout": "Floodfill-Timeout (Sek)",
|
||||||
"ground_level": "Bodenhöhe",
|
"ground_level": "Bodenhöhe",
|
||||||
"choose_world_modal_title": "Welt wählen",
|
|
||||||
"select_existing_world": "Vorhandene Welt auswählen",
|
|
||||||
"generate_new_world": "Neue Welt generieren",
|
|
||||||
"customization_settings": "Einstellungen",
|
"customization_settings": "Einstellungen",
|
||||||
"footer_text": "© {year} Arnis v{version} von louis-e",
|
"footer_text": "© {year} Arnis v{version} von louis-e",
|
||||||
"new_version_available": "Eine neue Version ist verfügbar! Klicke hier, um sie herunterzuladen.",
|
"new_version_available": "Eine neue Version ist verfügbar! Klicke hier, um sie herunterzuladen.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Die ausgewählte Welt ist gerade in Benutzung",
|
"world_in_use": "Die ausgewählte Welt ist gerade in Benutzung",
|
||||||
"failed_to_create_world": "Neue Welt konnte nicht erstellt werden",
|
"failed_to_create_world": "Neue Welt konnte nicht erstellt werden",
|
||||||
"no_world_selected_error": "Keine Welt ausgewählt",
|
"no_world_selected_error": "Keine Welt ausgewählt",
|
||||||
"select_minecraft_world_first": "Wähle zuerst eine Minecraft Welt aus!",
|
"create_world_first": "Erstelle zuerst eine Welt!",
|
||||||
"select_location_first": "Wähle zuerst einen Standort aus!",
|
"select_location_first": "Wähle zuerst einen Standort aus!",
|
||||||
"area_too_large": "Dieses Gebiet ist sehr groß und könnte das Berechnungslimit überschreiten.",
|
"area_too_large": "Dieses Gebiet ist sehr groß und könnte das Berechnungslimit überschreiten.",
|
||||||
"area_extensive": "Diese Gebietsgröße könnte längere Zeit für die Generierung benötigen.",
|
"area_extensive": "Diese Gebietsgröße könnte längere Zeit für die Generierung benötigen.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Innenraum Generierung",
|
"interior": "Innenraum Generierung",
|
||||||
"roof": "Dach Generierung",
|
"roof": "Dach Generierung",
|
||||||
"fillground": "Boden füllen",
|
"fillground": "Boden füllen",
|
||||||
"bedrock_use_java": "Java für Weltauswahl nutzen"
|
"city_boundaries": "Stadtboden",
|
||||||
|
"bedrock_auto_generated": "Bedrock-Welt wird automatisch generiert",
|
||||||
|
"save_path": "Speicherpfad"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/en-US.json
vendored
13
src/gui/locales/en-US.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Choose World",
|
"create_world": "Create World",
|
||||||
"no_world_selected": "No world selected",
|
"no_world_selected": "No world created",
|
||||||
"start_generation": "Start Generation",
|
"start_generation": "Start Generation",
|
||||||
"custom_selection_confirmed": "Custom selection confirmed!",
|
"custom_selection_confirmed": "Custom selection confirmed!",
|
||||||
"error_coordinates_out_of_range": "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).",
|
"error_coordinates_out_of_range": "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Custom Bounding Box",
|
"custom_bounding_box": "Custom Bounding Box",
|
||||||
"floodfill_timeout": "Floodfill Timeout (sec)",
|
"floodfill_timeout": "Floodfill Timeout (sec)",
|
||||||
"ground_level": "Ground Level",
|
"ground_level": "Ground Level",
|
||||||
"choose_world_modal_title": "Choose World",
|
|
||||||
"select_existing_world": "Select existing world",
|
|
||||||
"generate_new_world": "Generate new world",
|
|
||||||
"customization_settings": "Customization Settings",
|
"customization_settings": "Customization Settings",
|
||||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||||
"new_version_available": "There's a new version available! Click here to download it.",
|
"new_version_available": "There's a new version available! Click here to download it.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "The selected world is currently in use",
|
"world_in_use": "The selected world is currently in use",
|
||||||
"failed_to_create_world": "Failed to create new world",
|
"failed_to_create_world": "Failed to create new world",
|
||||||
"no_world_selected_error": "No world selected",
|
"no_world_selected_error": "No world selected",
|
||||||
"select_minecraft_world_first": "Select a Minecraft world first!",
|
"create_world_first": "Create a world first!",
|
||||||
"select_location_first": "Select a location first!",
|
"select_location_first": "Select a location first!",
|
||||||
"area_too_large": "This area is very large and could exceed typical computing limits.",
|
"area_too_large": "This area is very large and could exceed typical computing limits.",
|
||||||
"area_extensive": "The area is quite extensive and may take significant time and resources.",
|
"area_extensive": "The area is quite extensive and may take significant time and resources.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Interior Generation",
|
"interior": "Interior Generation",
|
||||||
"roof": "Roof Generation",
|
"roof": "Roof Generation",
|
||||||
"fillground": "Fill Ground",
|
"fillground": "Fill Ground",
|
||||||
"bedrock_use_java": "Use Java to select worlds"
|
"city_boundaries": "City Ground",
|
||||||
|
"bedrock_auto_generated": "Bedrock world is auto-generated",
|
||||||
|
"save_path": "Save Path"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/es.json
vendored
13
src/gui/locales/es.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Elegir mundo",
|
"create_world": "Crear mundo",
|
||||||
"no_world_selected": "Ningún mundo seleccionado",
|
"no_world_selected": "Ningún mundo creado",
|
||||||
"start_generation": "Iniciar generación",
|
"start_generation": "Iniciar generación",
|
||||||
"custom_selection_confirmed": "¡Selección personalizada confirmada!",
|
"custom_selection_confirmed": "¡Selección personalizada confirmada!",
|
||||||
"error_coordinates_out_of_range": "Error: Las coordenadas están fuera de rango o están ordenadas incorrectamente (Lat antes de Lng requerido).",
|
"error_coordinates_out_of_range": "Error: Las coordenadas están fuera de rango o están ordenadas incorrectamente (Lat antes de Lng requerido).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Caja delimitadora personalizada",
|
"custom_bounding_box": "Caja delimitadora personalizada",
|
||||||
"floodfill_timeout": "Tiempo de espera de relleno (seg)",
|
"floodfill_timeout": "Tiempo de espera de relleno (seg)",
|
||||||
"ground_level": "Nivel del suelo",
|
"ground_level": "Nivel del suelo",
|
||||||
"choose_world_modal_title": "Elegir mundo",
|
|
||||||
"select_existing_world": "Seleccionar mundo existente",
|
|
||||||
"generate_new_world": "Generar nuevo mundo",
|
|
||||||
"customization_settings": "Configuración de personalización",
|
"customization_settings": "Configuración de personalización",
|
||||||
"footer_text": "© {year} Arnis v{version} por louis-e",
|
"footer_text": "© {year} Arnis v{version} por louis-e",
|
||||||
"new_version_available": "¡Hay una nueva versión disponible! Haga clic aquí para descargarla.",
|
"new_version_available": "¡Hay una nueva versión disponible! Haga clic aquí para descargarla.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "El mundo seleccionado está en uso",
|
"world_in_use": "El mundo seleccionado está en uso",
|
||||||
"failed_to_create_world": "No se pudo crear un nuevo mundo",
|
"failed_to_create_world": "No se pudo crear un nuevo mundo",
|
||||||
"no_world_selected_error": "Ningún mundo seleccionado",
|
"no_world_selected_error": "Ningún mundo seleccionado",
|
||||||
"select_minecraft_world_first": "¡Seleccione un mundo de Minecraft primero!",
|
"create_world_first": "¡Crea un mundo primero!",
|
||||||
"select_location_first": "¡Seleccione una ubicación primero!",
|
"select_location_first": "¡Seleccione una ubicación primero!",
|
||||||
"area_too_large": "Esta área es muy grande y podría exceder los límites típicos de computación.",
|
"area_too_large": "Esta área es muy grande y podría exceder los límites típicos de computación.",
|
||||||
"area_extensive": "El área es bastante extensa y puede requerir mucho tiempo y recursos.",
|
"area_extensive": "El área es bastante extensa y puede requerir mucho tiempo y recursos.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Generación Interior",
|
"interior": "Generación Interior",
|
||||||
"roof": "Generación de Tejado",
|
"roof": "Generación de Tejado",
|
||||||
"fillground": "Rellenar Suelo",
|
"fillground": "Rellenar Suelo",
|
||||||
"bedrock_use_java": "Usa Java para elegir mundos"
|
"city_boundaries": "Suelo Urbano",
|
||||||
|
"bedrock_auto_generated": "El mundo Bedrock se genera automáticamente",
|
||||||
|
"save_path": "Ruta de guardado"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/fi.json
vendored
13
src/gui/locales/fi.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Valitse maailma",
|
"create_world": "Luo maailma",
|
||||||
"no_world_selected": "Maailmaa ei valittu",
|
"no_world_selected": "Maailmaa ei luotu",
|
||||||
"start_generation": "Aloita generointi",
|
"start_generation": "Aloita generointi",
|
||||||
"custom_selection_confirmed": "Mukautettu valinta vahvistettu!",
|
"custom_selection_confirmed": "Mukautettu valinta vahvistettu!",
|
||||||
"error_coordinates_out_of_range": "Virhe: Koordinaatit ovat kantaman ulkopuolella tai vääriin aseteltu (Lat ennen Lng vaadittu).",
|
"error_coordinates_out_of_range": "Virhe: Koordinaatit ovat kantaman ulkopuolella tai vääriin aseteltu (Lat ennen Lng vaadittu).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Mukautettu rajoituslaatikko",
|
"custom_bounding_box": "Mukautettu rajoituslaatikko",
|
||||||
"floodfill_timeout": "Täytön aikakatkaisu (sec)",
|
"floodfill_timeout": "Täytön aikakatkaisu (sec)",
|
||||||
"ground_level": "Maataso",
|
"ground_level": "Maataso",
|
||||||
"choose_world_modal_title": "Valitse maailma",
|
|
||||||
"select_existing_world": "Valitse olemassa oleva maailma",
|
|
||||||
"generate_new_world": "Luo uusi maailma",
|
|
||||||
"customization_settings": "Kustomisaatio-asetukset",
|
"customization_settings": "Kustomisaatio-asetukset",
|
||||||
"footer_text": "© {year} Arnis v{version} tekijänä louis-e",
|
"footer_text": "© {year} Arnis v{version} tekijänä louis-e",
|
||||||
"new_version_available": "Uusi versio on saatavilla! Paina tästä ladataksesi sen.",
|
"new_version_available": "Uusi versio on saatavilla! Paina tästä ladataksesi sen.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Valittu maailma käytössä.",
|
"world_in_use": "Valittu maailma käytössä.",
|
||||||
"failed_to_create_world": "Uuden maailman luonti epäonnistui",
|
"failed_to_create_world": "Uuden maailman luonti epäonnistui",
|
||||||
"no_world_selected_error": "Maailmaa ei valittu",
|
"no_world_selected_error": "Maailmaa ei valittu",
|
||||||
"select_minecraft_world_first": "Valitse Minecraft-maailma ensin!",
|
"create_world_first": "Luo ensin maailma!",
|
||||||
"select_location_first": "Valitse paikka ensin!",
|
"select_location_first": "Valitse paikka ensin!",
|
||||||
"area_too_large": "Tämä alue on todella iso ja voi ylittää tyypilliset laskentarajat.",
|
"area_too_large": "Tämä alue on todella iso ja voi ylittää tyypilliset laskentarajat.",
|
||||||
"area_extensive": "Alue on aika laaja ja voi viedä pitkän ajan ja resursseja.",
|
"area_extensive": "Alue on aika laaja ja voi viedä pitkän ajan ja resursseja.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Sisätilan luonti",
|
"interior": "Sisätilan luonti",
|
||||||
"roof": "Katon luonti",
|
"roof": "Katon luonti",
|
||||||
"fillground": "Täytä maa",
|
"fillground": "Täytä maa",
|
||||||
"bedrock_use_java": "Käytä Javaa maailmojen valintaan"
|
"city_boundaries": "Kaupungin maa",
|
||||||
|
"bedrock_auto_generated": "Bedrock-maailma luodaan automaattisesti",
|
||||||
|
"save_path": "Tallennuspolku"
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/gui/locales/fr-FR.json
vendored
13
src/gui/locales/fr-FR.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Choisir un monde",
|
"create_world": "Créer un monde",
|
||||||
"no_world_selected": "Aucun monde sélectionné",
|
"no_world_selected": "Aucun monde créé",
|
||||||
"start_generation": "Commencer la génération",
|
"start_generation": "Commencer la génération",
|
||||||
"custom_selection_confirmed": "Sélection personnalisée confirmée !",
|
"custom_selection_confirmed": "Sélection personnalisée confirmée !",
|
||||||
"error_coordinates_out_of_range": "Erreur: Coordonnées hors de portée ou dans un ordre incorrect (besoin de la latitude avant la longitude).",
|
"error_coordinates_out_of_range": "Erreur: Coordonnées hors de portée ou dans un ordre incorrect (besoin de la latitude avant la longitude).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Cadre de délimitation personnalisé",
|
"custom_bounding_box": "Cadre de délimitation personnalisé",
|
||||||
"floodfill_timeout": "Expiration du délai de remplissage (en secondes)",
|
"floodfill_timeout": "Expiration du délai de remplissage (en secondes)",
|
||||||
"ground_level": "Niveau du sol",
|
"ground_level": "Niveau du sol",
|
||||||
"choose_world_modal_title": "Choisir un monde",
|
|
||||||
"select_existing_world": "Sélectionner un monde existant",
|
|
||||||
"generate_new_world": "Générer un nouveau monde",
|
|
||||||
"customization_settings": "Paramètres de personnalisation",
|
"customization_settings": "Paramètres de personnalisation",
|
||||||
"footer_text": "© {year} Arnis v{version} par louis-e",
|
"footer_text": "© {year} Arnis v{version} par louis-e",
|
||||||
"new_version_available": "Une nouvelle version est disponible ! Cliquez ici pour la télécharger.",
|
"new_version_available": "Une nouvelle version est disponible ! Cliquez ici pour la télécharger.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Le monde sélectionné est en cours d'utilisation",
|
"world_in_use": "Le monde sélectionné est en cours d'utilisation",
|
||||||
"failed_to_create_world": "Échec de la création du nouveau monde",
|
"failed_to_create_world": "Échec de la création du nouveau monde",
|
||||||
"no_world_selected_error": "Aucun monde sélectionné",
|
"no_world_selected_error": "Aucun monde sélectionné",
|
||||||
"select_minecraft_world_first": "Sélectionnez d'abord un monde Minecraft !",
|
"create_world_first": "Créez d'abord un monde !",
|
||||||
"select_location_first": "Sélectionnez d'abord une localisation !",
|
"select_location_first": "Sélectionnez d'abord une localisation !",
|
||||||
"area_too_large": "Cette zone est très grande et pourrait dépasser les limites de calcul courantes.",
|
"area_too_large": "Cette zone est très grande et pourrait dépasser les limites de calcul courantes.",
|
||||||
"area_extensive": "Cette zone est très étendue et pourrait nécessiter beaucoup de ressources et de temps.",
|
"area_extensive": "Cette zone est très étendue et pourrait nécessiter beaucoup de ressources et de temps.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Génération d'intérieur",
|
"interior": "Génération d'intérieur",
|
||||||
"roof": "Génération de toit",
|
"roof": "Génération de toit",
|
||||||
"fillground": "Remplir le sol",
|
"fillground": "Remplir le sol",
|
||||||
"bedrock_use_java": "Utilisez Java pour les mondes"
|
"city_boundaries": "Sol urbain",
|
||||||
|
"bedrock_auto_generated": "Le monde Bedrock est généré automatiquement",
|
||||||
|
"save_path": "Chemin de sauvegarde"
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/gui/locales/hu.json
vendored
13
src/gui/locales/hu.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Világ kiválasztása",
|
"create_world": "Világ létrehozása",
|
||||||
"no_world_selected": "Nincs világ kiválasztva",
|
"no_world_selected": "Nincs világ létrehozva",
|
||||||
"start_generation": "Generálás indítása",
|
"start_generation": "Generálás indítása",
|
||||||
"custom_selection_confirmed": "Egyéni kiválasztás megerősítve",
|
"custom_selection_confirmed": "Egyéni kiválasztás megerősítve",
|
||||||
"error_coordinates_out_of_range": "Hiba: A koordináták tartományon kívül vannak vagy hibásan rendezettek (a szélességi foknak a hosszúsági fok előtt kell lennie)",
|
"error_coordinates_out_of_range": "Hiba: A koordináták tartományon kívül vannak vagy hibásan rendezettek (a szélességi foknak a hosszúsági fok előtt kell lennie)",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Egyéni határoló keret",
|
"custom_bounding_box": "Egyéni határoló keret",
|
||||||
"floodfill_timeout": "Floodfill Timeout (sec)",
|
"floodfill_timeout": "Floodfill Timeout (sec)",
|
||||||
"ground_level": "Földszint",
|
"ground_level": "Földszint",
|
||||||
"choose_world_modal_title": "Világ kiválasztása",
|
|
||||||
"select_existing_world": "Már létező világ kiválasztása",
|
|
||||||
"generate_new_world": "Új világ generálása",
|
|
||||||
"customization_settings": "Testreszabási lehetőségek",
|
"customization_settings": "Testreszabási lehetőségek",
|
||||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||||
"new_version_available": "Egy új verzió elérhető kattints ide hogy letöltsd",
|
"new_version_available": "Egy új verzió elérhető kattints ide hogy letöltsd",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "A kiválasztott világ már használatban van",
|
"world_in_use": "A kiválasztott világ már használatban van",
|
||||||
"failed_to_create_world": "Nem sikerült új világot létrehozni",
|
"failed_to_create_world": "Nem sikerült új világot létrehozni",
|
||||||
"no_world_selected_error": "Nincs kiválasztott világ",
|
"no_world_selected_error": "Nincs kiválasztott világ",
|
||||||
"select_minecraft_world_first": "Válassz ki egy Minecraft világot először!",
|
"create_world_first": "Először hozz létre egy világot!",
|
||||||
"select_location_first": "Válassz egy helyet először!",
|
"select_location_first": "Válassz egy helyet először!",
|
||||||
"area_too_large": "Ez a terület nagyon nagy, és meghaladhatja a szokásos számítási korlátokat.",
|
"area_too_large": "Ez a terület nagyon nagy, és meghaladhatja a szokásos számítási korlátokat.",
|
||||||
"area_extensive": "A terület meglehetősen kiterjedt, és jelentős időt és erőforrásokat igényelhet.",
|
"area_extensive": "A terület meglehetősen kiterjedt, és jelentős időt és erőforrásokat igényelhet.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Belső generálás",
|
"interior": "Belső generálás",
|
||||||
"roof": "Tető generálás",
|
"roof": "Tető generálás",
|
||||||
"fillground": "Talaj feltöltése",
|
"fillground": "Talaj feltöltése",
|
||||||
"bedrock_use_java": "Java világválasztáshoz"
|
"city_boundaries": "Városi talaj",
|
||||||
|
"bedrock_auto_generated": "A Bedrock világ automatikusan generálódik",
|
||||||
|
"save_path": "Mentési útvonal"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/ko.json
vendored
13
src/gui/locales/ko.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "세계 선택",
|
"create_world": "월드 만들기",
|
||||||
"no_world_selected": "선택된 세계 없음",
|
"no_world_selected": "생성된 월드 없음",
|
||||||
"start_generation": "생성 시작",
|
"start_generation": "생성 시작",
|
||||||
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
||||||
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "사용자 지정 경계 상자",
|
"custom_bounding_box": "사용자 지정 경계 상자",
|
||||||
"floodfill_timeout": "채우기 시간 초과 (초)",
|
"floodfill_timeout": "채우기 시간 초과 (초)",
|
||||||
"ground_level": "지면 레벨",
|
"ground_level": "지면 레벨",
|
||||||
"choose_world_modal_title": "세계 선택",
|
|
||||||
"select_existing_world": "이미 존재하는 세계 선택",
|
|
||||||
"generate_new_world": "새 세계 생성",
|
|
||||||
"customization_settings": "사용자 지정 설정",
|
"customization_settings": "사용자 지정 설정",
|
||||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||||
"new_version_available": "새로운 버전이 있습니다! 여기를 클릭하여 다운로드하세요.",
|
"new_version_available": "새로운 버전이 있습니다! 여기를 클릭하여 다운로드하세요.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "선택한 세계가 현재 사용 중입니다",
|
"world_in_use": "선택한 세계가 현재 사용 중입니다",
|
||||||
"failed_to_create_world": "새 세계 생성에 실패했습니다",
|
"failed_to_create_world": "새 세계 생성에 실패했습니다",
|
||||||
"no_world_selected_error": "선택된 세계 없음 오류",
|
"no_world_selected_error": "선택된 세계 없음 오류",
|
||||||
"select_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
"create_world_first": "먼저 월드를 만드세요!",
|
||||||
"select_location_first": "먼저 위치를 선택하세요!",
|
"select_location_first": "먼저 위치를 선택하세요!",
|
||||||
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
||||||
"area_extensive": "이 지역은 꽤 광범위하여 상당한 시간과 자원이 필요할 수 있습니다.",
|
"area_extensive": "이 지역은 꽤 광범위하여 상당한 시간과 자원이 필요할 수 있습니다.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "내부 생성",
|
"interior": "내부 생성",
|
||||||
"roof": "지붕 생성",
|
"roof": "지붕 생성",
|
||||||
"fillground": "지면 채우기",
|
"fillground": "지면 채우기",
|
||||||
"bedrock_use_java": "Java로 세계 선택"
|
"city_boundaries": "도시 지면",
|
||||||
|
"bedrock_auto_generated": "Bedrock 월드는 자동 생성됩니다",
|
||||||
|
"save_path": "저장 경로"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/lt.json
vendored
13
src/gui/locales/lt.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Pasirinkti pasaulį",
|
"create_world": "Sukurti pasaulį",
|
||||||
"no_world_selected": "Pasaulis nepasirinktas",
|
"no_world_selected": "Pasaulis nesukurtas",
|
||||||
"start_generation": "Pradėti generaciją",
|
"start_generation": "Pradėti generaciją",
|
||||||
"custom_selection_confirmed": "Rėmo pasirinkimas patvirtintas!",
|
"custom_selection_confirmed": "Rėmo pasirinkimas patvirtintas!",
|
||||||
"error_coordinates_out_of_range": "Klaida: Koordinatės yra už ribų arba neteisingai išdėstytos (plat turi būti prieš ilg).",
|
"error_coordinates_out_of_range": "Klaida: Koordinatės yra už ribų arba neteisingai išdėstytos (plat turi būti prieš ilg).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Pasirinktinis ribos rėmas",
|
"custom_bounding_box": "Pasirinktinis ribos rėmas",
|
||||||
"floodfill_timeout": "Užpildymo laiko limitas (sek.)",
|
"floodfill_timeout": "Užpildymo laiko limitas (sek.)",
|
||||||
"ground_level": "Žemės lygis",
|
"ground_level": "Žemės lygis",
|
||||||
"choose_world_modal_title": "Pasaulio pasirinkimas",
|
|
||||||
"select_existing_world": "Pasirinkti esamą pasaulį",
|
|
||||||
"generate_new_world": "Sugeneruoti naują pasaulį",
|
|
||||||
"customization_settings": "Generacijos nustatymai",
|
"customization_settings": "Generacijos nustatymai",
|
||||||
"footer_text": "© {year} „Arnis“ v{version} sukurta louis-e",
|
"footer_text": "© {year} „Arnis“ v{version} sukurta louis-e",
|
||||||
"new_version_available": "Surasta nauja versija! Spauskite čia kad ją atsisiųstumėte.",
|
"new_version_available": "Surasta nauja versija! Spauskite čia kad ją atsisiųstumėte.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Pasirinktas pasaulis dabar užimtas",
|
"world_in_use": "Pasirinktas pasaulis dabar užimtas",
|
||||||
"failed_to_create_world": "Klaida sukuriant naują pasaulį",
|
"failed_to_create_world": "Klaida sukuriant naują pasaulį",
|
||||||
"no_world_selected_error": "Nėra pasirinktas pasaulis",
|
"no_world_selected_error": "Nėra pasirinktas pasaulis",
|
||||||
"select_minecraft_world_first": "Pirma pasirinkite „Minecraft“ pasaulį!",
|
"create_world_first": "Pirmiausia sukurkite pasaulį!",
|
||||||
"select_location_first": "Pirma pasirinkite vietą!",
|
"select_location_first": "Pirma pasirinkite vietą!",
|
||||||
"area_too_large": "Šis plotas yra labai didelis ir gali viršyti tipinius resursų limitus.",
|
"area_too_large": "Šis plotas yra labai didelis ir gali viršyti tipinius resursų limitus.",
|
||||||
"area_extensive": "Šis plotas yra pakankamai didelis kuriam reikėtų daug laiko ir resursų.",
|
"area_extensive": "Šis plotas yra pakankamai didelis kuriam reikėtų daug laiko ir resursų.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Interjero generavimas",
|
"interior": "Interjero generavimas",
|
||||||
"roof": "Stogo generavimas",
|
"roof": "Stogo generavimas",
|
||||||
"fillground": "Užpildyti pagrindą",
|
"fillground": "Užpildyti pagrindą",
|
||||||
"bedrock_use_java": "Naudok Java pasauliams"
|
"city_boundaries": "Miesto žemė",
|
||||||
|
"bedrock_auto_generated": "Bedrock pasaulis generuojamas automatiškai",
|
||||||
|
"save_path": "Išsaugojimo kelias"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/lv.json
vendored
13
src/gui/locales/lv.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Izvēlēties pasauli",
|
"create_world": "Izveidot pasauli",
|
||||||
"no_world_selected": "Pasaulē nav izvēlēta",
|
"no_world_selected": "Pasaule nav izveidota",
|
||||||
"start_generation": "Sākt ģenerēšanu",
|
"start_generation": "Sākt ģenerēšanu",
|
||||||
"custom_selection_confirmed": "Pielāgota izvēle apstiprināta!",
|
"custom_selection_confirmed": "Pielāgota izvēle apstiprināta!",
|
||||||
"error_coordinates_out_of_range": "Kļūda: koordinātas ir ārpus darbības zonas vai norādītas nepareizā secībā (vispirms platums, tad garums)",
|
"error_coordinates_out_of_range": "Kļūda: koordinātas ir ārpus darbības zonas vai norādītas nepareizā secībā (vispirms platums, tad garums)",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Pielāgota ierobežojošā rāmja zona",
|
"custom_bounding_box": "Pielāgota ierobežojošā rāmja zona",
|
||||||
"floodfill_timeout": "Aizpildes noildze (sek.)",
|
"floodfill_timeout": "Aizpildes noildze (sek.)",
|
||||||
"ground_level": "Zemes līmenis",
|
"ground_level": "Zemes līmenis",
|
||||||
"choose_world_modal_title": "Izvēlēties pasauli",
|
|
||||||
"select_existing_world": "Izvēlēties esošu pasauli",
|
|
||||||
"generate_new_world": "Izveidot jaunu pasauli",
|
|
||||||
"customization_settings": "Personalizācijas iestatījumi",
|
"customization_settings": "Personalizācijas iestatījumi",
|
||||||
"footer_text": "© {year} Arnis v{version} no louis-e",
|
"footer_text": "© {year} Arnis v{version} no louis-e",
|
||||||
"new_version_available": "Pieejama jauna versija! Noklikšķiniet šeit, lai lejupielādētu",
|
"new_version_available": "Pieejama jauna versija! Noklikšķiniet šeit, lai lejupielādētu",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Izvēlētā pasaule jau tiek izmantota",
|
"world_in_use": "Izvēlētā pasaule jau tiek izmantota",
|
||||||
"failed_to_create_world": "Neizdevās izveidot jaunu pasauli",
|
"failed_to_create_world": "Neizdevās izveidot jaunu pasauli",
|
||||||
"no_world_selected_error": "Pasaulē nav izvēlēta",
|
"no_world_selected_error": "Pasaulē nav izvēlēta",
|
||||||
"select_minecraft_world_first": "Vispirms izvēlieties Minecraft pasauli!",
|
"create_world_first": "Vispirms izveidojiet pasauli!",
|
||||||
"select_location_first": "Vispirms izvēlieties atrašanās vietu!",
|
"select_location_first": "Vispirms izvēlieties atrašanās vietu!",
|
||||||
"area_too_large": "Šis apgabals ir pārāk liels un var pārsniegt tipiskos aprēķina ierobežojumus",
|
"area_too_large": "Šis apgabals ir pārāk liels un var pārsniegt tipiskos aprēķina ierobežojumus",
|
||||||
"area_extensive": "Apgabals ir diezgan plašs un var prasīt ievērojamu laiku un resursus",
|
"area_extensive": "Apgabals ir diezgan plašs un var prasīt ievērojamu laiku un resursus",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Interjera ģenerēšana",
|
"interior": "Interjera ģenerēšana",
|
||||||
"roof": "Jumta ģenerēšana",
|
"roof": "Jumta ģenerēšana",
|
||||||
"fillground": "Aizpildīt zemi",
|
"fillground": "Aizpildīt zemi",
|
||||||
"bedrock_use_java": "Izmanto Java pasaulēm"
|
"city_boundaries": "Pilsētas zeme",
|
||||||
|
"bedrock_auto_generated": "Bedrock pasaule tiek ģenerēta automātiski",
|
||||||
|
"save_path": "Saglabāšanas ceļš"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/pl.json
vendored
13
src/gui/locales/pl.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Wybierz świat",
|
"create_world": "Utwórz świat",
|
||||||
"no_world_selected": "Nie wybrano świata",
|
"no_world_selected": "Nie utworzono świata",
|
||||||
"start_generation": "Rozpocznij generowanie",
|
"start_generation": "Rozpocznij generowanie",
|
||||||
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
||||||
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub w złej kolejności (wymagana szerokość przed długością).",
|
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub w złej kolejności (wymagana szerokość przed długością).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Niestandardowy obszar",
|
"custom_bounding_box": "Niestandardowy obszar",
|
||||||
"floodfill_timeout": "Limit czasu wypełniania (sek)",
|
"floodfill_timeout": "Limit czasu wypełniania (sek)",
|
||||||
"ground_level": "Wysokość obszaru",
|
"ground_level": "Wysokość obszaru",
|
||||||
"choose_world_modal_title": "Wybierz świat",
|
|
||||||
"select_existing_world": "Wybierz istniejący świat",
|
|
||||||
"generate_new_world": "Generuj nowy świat",
|
|
||||||
"customization_settings": "Ustawienia personalizacji",
|
"customization_settings": "Ustawienia personalizacji",
|
||||||
"footer_text": "© {year} Arnis v{version} autorstwa louis-e",
|
"footer_text": "© {year} Arnis v{version} autorstwa louis-e",
|
||||||
"new_version_available": "Dostępna jest nowa wersja! Kliknij tutaj, aby ją pobrać.",
|
"new_version_available": "Dostępna jest nowa wersja! Kliknij tutaj, aby ją pobrać.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Wybrany świat jest obecnie używany",
|
"world_in_use": "Wybrany świat jest obecnie używany",
|
||||||
"failed_to_create_world": "Nie udało się utworzyć świata",
|
"failed_to_create_world": "Nie udało się utworzyć świata",
|
||||||
"no_world_selected_error": "Nie wybrano świata",
|
"no_world_selected_error": "Nie wybrano świata",
|
||||||
"select_minecraft_world_first": "Najpierw wybierz świat Minecrafta!",
|
"create_world_first": "Najpierw utwórz świat!",
|
||||||
"select_location_first": "Najpierw wybierz lokalizację!",
|
"select_location_first": "Najpierw wybierz lokalizację!",
|
||||||
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć limity obliczeniowe.",
|
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć limity obliczeniowe.",
|
||||||
"area_extensive": "Ten obszar jest rozległy i może pochłonąć dużo czasu oraz zasobów.",
|
"area_extensive": "Ten obszar jest rozległy i może pochłonąć dużo czasu oraz zasobów.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Generowanie wnętrza",
|
"interior": "Generowanie wnętrza",
|
||||||
"roof": "Generowanie dachu",
|
"roof": "Generowanie dachu",
|
||||||
"fillground": "Wypełnij podłoże",
|
"fillground": "Wypełnij podłoże",
|
||||||
"bedrock_use_java": "Użyj Java do wyboru światów"
|
"city_boundaries": "Podłoże miejskie",
|
||||||
|
"bedrock_auto_generated": "Świat Bedrock jest generowany automatycznie",
|
||||||
|
"save_path": "Ścieżka zapisu"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/ru.json
vendored
13
src/gui/locales/ru.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Выбрать мир",
|
"create_world": "Создать мир",
|
||||||
"no_world_selected": "Мир не выбран",
|
"no_world_selected": "Мир не создан",
|
||||||
"start_generation": "Начать генерацию",
|
"start_generation": "Начать генерацию",
|
||||||
"custom_selection_confirmed": "Пользовательский выбор подтвержден!",
|
"custom_selection_confirmed": "Пользовательский выбор подтвержден!",
|
||||||
"error_coordinates_out_of_range": "Ошибка: Координаты находятся вне зоны действия или указаны в неправильном порядке (сначала широта, затем долгота)",
|
"error_coordinates_out_of_range": "Ошибка: Координаты находятся вне зоны действия или указаны в неправильном порядке (сначала широта, затем долгота)",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Пользовательская ограничивающая рамка",
|
"custom_bounding_box": "Пользовательская ограничивающая рамка",
|
||||||
"floodfill_timeout": "Тайм-аут заливки (сек)",
|
"floodfill_timeout": "Тайм-аут заливки (сек)",
|
||||||
"ground_level": "Уровень земли",
|
"ground_level": "Уровень земли",
|
||||||
"choose_world_modal_title": "Выбрать мир",
|
|
||||||
"select_existing_world": "Выбрать существующий мир",
|
|
||||||
"generate_new_world": "Создать новый мир",
|
|
||||||
"customization_settings": "Настройки персонализации",
|
"customization_settings": "Настройки персонализации",
|
||||||
"footer_text": "© {year} Arnis v{version} от louis-e",
|
"footer_text": "© {year} Arnis v{version} от louis-e",
|
||||||
"new_version_available": "Доступна новая версия! Нажмите здесь, чтобы скачать",
|
"new_version_available": "Доступна новая версия! Нажмите здесь, чтобы скачать",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Выбранный мир уже используется",
|
"world_in_use": "Выбранный мир уже используется",
|
||||||
"failed_to_create_world": "Не удалось создать новый мир",
|
"failed_to_create_world": "Не удалось создать новый мир",
|
||||||
"no_world_selected_error": "Мир не выбран",
|
"no_world_selected_error": "Мир не выбран",
|
||||||
"select_minecraft_world_first": "Сначала выберите мир Minecraft!",
|
"create_world_first": "Сначала создайте мир!",
|
||||||
"select_location_first": "Сначала выберите местоположение!",
|
"select_location_first": "Сначала выберите местоположение!",
|
||||||
"area_too_large": "Эта область слишком велика и может превысить типичные вычислительные ограничения",
|
"area_too_large": "Эта область слишком велика и может превысить типичные вычислительные ограничения",
|
||||||
"area_extensive": "Область довольно обширна и может потребовать значительного времени и ресурсов",
|
"area_extensive": "Область довольно обширна и может потребовать значительного времени и ресурсов",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Генерация Интерьера",
|
"interior": "Генерация Интерьера",
|
||||||
"roof": "Генерация Крыши",
|
"roof": "Генерация Крыши",
|
||||||
"fillground": "Заполнить Землю",
|
"fillground": "Заполнить Землю",
|
||||||
"bedrock_use_java": "Используйте Java для миров"
|
"city_boundaries": "Городской грунт",
|
||||||
|
"bedrock_auto_generated": "Мир Bedrock генерируется автоматически",
|
||||||
|
"save_path": "Путь сохранения"
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/gui/locales/sv.json
vendored
13
src/gui/locales/sv.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Välj värld",
|
"create_world": "Skapa värld",
|
||||||
"no_world_selected": "Ingen värld vald",
|
"no_world_selected": "Ingen värld skapad",
|
||||||
"start_generation": "Starta generering",
|
"start_generation": "Starta generering",
|
||||||
"custom_selection_confirmed": "Anpassad markering bekräftad!",
|
"custom_selection_confirmed": "Anpassad markering bekräftad!",
|
||||||
"error_coordinates_out_of_range": "Fel: Koordinater är utanför området eller felaktigt ordnade (Lat före Lng krävs).",
|
"error_coordinates_out_of_range": "Fel: Koordinater är utanför området eller felaktigt ordnade (Lat före Lng krävs).",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Anpassad begränsningsram",
|
"custom_bounding_box": "Anpassad begränsningsram",
|
||||||
"floodfill_timeout": "Floodfill-tidsgräns (sek)",
|
"floodfill_timeout": "Floodfill-tidsgräns (sek)",
|
||||||
"ground_level": "Marknivå",
|
"ground_level": "Marknivå",
|
||||||
"choose_world_modal_title": "Välj värld",
|
|
||||||
"select_existing_world": "Välj existerande värld",
|
|
||||||
"generate_new_world": "Generera ny värld",
|
|
||||||
"customization_settings": "Anpassningsinställningar",
|
"customization_settings": "Anpassningsinställningar",
|
||||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||||
"new_version_available": "Det finns en ny version tillgänglig! Klicka här för att ladda ner den.",
|
"new_version_available": "Det finns en ny version tillgänglig! Klicka här för att ladda ner den.",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Den valda världen används just nu",
|
"world_in_use": "Den valda världen används just nu",
|
||||||
"failed_to_create_world": "Misslyckades att skapa ny värld",
|
"failed_to_create_world": "Misslyckades att skapa ny värld",
|
||||||
"no_world_selected_error": "Ingen värld vald fel",
|
"no_world_selected_error": "Ingen värld vald fel",
|
||||||
"select_minecraft_world_first": "Välj Minecraft-värld först!",
|
"create_world_first": "Skapa en värld först!",
|
||||||
"select_location_first": "Välj plats först!",
|
"select_location_first": "Välj plats först!",
|
||||||
"area_too_large": "Detta område är mycket stort och kan överskrida vanliga beräkningsgränser.",
|
"area_too_large": "Detta område är mycket stort och kan överskrida vanliga beräkningsgränser.",
|
||||||
"area_extensive": "Området är ganska extensivt och kan ta betydande tid och resurser.",
|
"area_extensive": "Området är ganska extensivt och kan ta betydande tid och resurser.",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Interiörgenerering",
|
"interior": "Interiörgenerering",
|
||||||
"roof": "Takgenerering",
|
"roof": "Takgenerering",
|
||||||
"fillground": "Fyll mark",
|
"fillground": "Fyll mark",
|
||||||
"bedrock_use_java": "Använd Java för världar"
|
"city_boundaries": "Stadsmark",
|
||||||
|
"bedrock_auto_generated": "Bedrock-världen genereras automatiskt",
|
||||||
|
"save_path": "Sökväg"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/ua.json
vendored
13
src/gui/locales/ua.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "Обрати світ",
|
"create_world": "Створити світ",
|
||||||
"no_world_selected": "Світ не обрано",
|
"no_world_selected": "Світ не створено",
|
||||||
"start_generation": "Почати генерацію",
|
"start_generation": "Почати генерацію",
|
||||||
"custom_selection_confirmed": "Користувацький вибір підтверджено!",
|
"custom_selection_confirmed": "Користувацький вибір підтверджено!",
|
||||||
"error_coordinates_out_of_range": "Помилка: Координати поза діапазоном або неправильно впорядковані (потрібно широта перед довгота)",
|
"error_coordinates_out_of_range": "Помилка: Координати поза діапазоном або неправильно впорядковані (потрібно широта перед довгота)",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "Користувацька обмежувальна рамка",
|
"custom_bounding_box": "Користувацька обмежувальна рамка",
|
||||||
"floodfill_timeout": "Тайм-аут заливки (сек)",
|
"floodfill_timeout": "Тайм-аут заливки (сек)",
|
||||||
"ground_level": "Рівень землі",
|
"ground_level": "Рівень землі",
|
||||||
"choose_world_modal_title": "Обрати світ",
|
|
||||||
"select_existing_world": "Обрати наявний світ",
|
|
||||||
"generate_new_world": "Створити новий світ",
|
|
||||||
"customization_settings": "Налаштування параметрів",
|
"customization_settings": "Налаштування параметрів",
|
||||||
"footer_text": "© {year} Arnis v{version} від louis-e",
|
"footer_text": "© {year} Arnis v{version} від louis-e",
|
||||||
"new_version_available": "Доступна нова версія! Натисніть тут, щоб завантажити її",
|
"new_version_available": "Доступна нова версія! Натисніть тут, щоб завантажити її",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "Вибраний світ зараз використовується",
|
"world_in_use": "Вибраний світ зараз використовується",
|
||||||
"failed_to_create_world": "Не вдалося створити новий світ",
|
"failed_to_create_world": "Не вдалося створити новий світ",
|
||||||
"no_world_selected_error": "Світ не обрано",
|
"no_world_selected_error": "Світ не обрано",
|
||||||
"select_minecraft_world_first": "Спочатку виберіть світ Minecraft!",
|
"create_world_first": "Спочатку створіть світ!",
|
||||||
"select_location_first": "Спочатку виберіть місцезнаходження!",
|
"select_location_first": "Спочатку виберіть місцезнаходження!",
|
||||||
"area_too_large": "Ця область дуже велика і може перевищити типові обчислювальні межі",
|
"area_too_large": "Ця область дуже велика і може перевищити типові обчислювальні межі",
|
||||||
"area_extensive": "Область досить велика і може вимагати значного часу та ресурсів",
|
"area_extensive": "Область досить велика і може вимагати значного часу та ресурсів",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "Генерація інтер'єру",
|
"interior": "Генерація інтер'єру",
|
||||||
"roof": "Генерація даху",
|
"roof": "Генерація даху",
|
||||||
"fillground": "Заповнити землю",
|
"fillground": "Заповнити землю",
|
||||||
"bedrock_use_java": "Використовуй Java для світів"
|
"city_boundaries": "Міська земля",
|
||||||
|
"bedrock_auto_generated": "Bedrock світ генерується автоматично",
|
||||||
|
"save_path": "Шлях збереження"
|
||||||
}
|
}
|
||||||
13
src/gui/locales/zh-CN.json
vendored
13
src/gui/locales/zh-CN.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"choose_world": "选择世界",
|
"create_world": "创建世界",
|
||||||
"no_world_selected": "未选择世界",
|
"no_world_selected": "未创建世界",
|
||||||
"start_generation": "开始生成",
|
"start_generation": "开始生成",
|
||||||
"custom_selection_confirmed": "自定义选择已确认!",
|
"custom_selection_confirmed": "自定义选择已确认!",
|
||||||
"error_coordinates_out_of_range": "错误:坐标超出范围或顺序不正确(需要先纬度后经度)。",
|
"error_coordinates_out_of_range": "错误:坐标超出范围或顺序不正确(需要先纬度后经度)。",
|
||||||
@@ -11,9 +11,6 @@
|
|||||||
"custom_bounding_box": "自定义边界框",
|
"custom_bounding_box": "自定义边界框",
|
||||||
"floodfill_timeout": "填充超时(秒)",
|
"floodfill_timeout": "填充超时(秒)",
|
||||||
"ground_level": "地面高度",
|
"ground_level": "地面高度",
|
||||||
"choose_world_modal_title": "选择世界",
|
|
||||||
"select_existing_world": "选择现有世界",
|
|
||||||
"generate_new_world": "生成新世界",
|
|
||||||
"customization_settings": "自定义设置",
|
"customization_settings": "自定义设置",
|
||||||
"footer_text": "© {year} Arnis v{version} 由 louis-e 提供",
|
"footer_text": "© {year} Arnis v{version} 由 louis-e 提供",
|
||||||
"new_version_available": "有新版本可用!点击这里下载。",
|
"new_version_available": "有新版本可用!点击这里下载。",
|
||||||
@@ -21,7 +18,7 @@
|
|||||||
"world_in_use": "所选世界正在使用中",
|
"world_in_use": "所选世界正在使用中",
|
||||||
"failed_to_create_world": "无法创建新世界",
|
"failed_to_create_world": "无法创建新世界",
|
||||||
"no_world_selected_error": "未选择世界",
|
"no_world_selected_error": "未选择世界",
|
||||||
"select_minecraft_world_first": "请先选择一个 Minecraft 世界!",
|
"create_world_first": "请先创建一个世界!",
|
||||||
"select_location_first": "请先选择一个位置!",
|
"select_location_first": "请先选择一个位置!",
|
||||||
"area_too_large": "该区域非常大,可能会超出典型的计算限制。",
|
"area_too_large": "该区域非常大,可能会超出典型的计算限制。",
|
||||||
"area_extensive": "该区域相当广泛,可能需要大量时间和资源。",
|
"area_extensive": "该区域相当广泛,可能需要大量时间和资源。",
|
||||||
@@ -42,5 +39,7 @@
|
|||||||
"interior": "内部生成",
|
"interior": "内部生成",
|
||||||
"roof": "屋顶生成",
|
"roof": "屋顶生成",
|
||||||
"fillground": "填充地面",
|
"fillground": "填充地面",
|
||||||
"bedrock_use_java": "使用Java选择世界"
|
"city_boundaries": "城市地面",
|
||||||
|
"bedrock_auto_generated": "Bedrock 世界自动生成",
|
||||||
|
"save_path": "存档路径"
|
||||||
}
|
}
|
||||||
82
src/main.rs
82
src/main.rs
@@ -25,12 +25,15 @@ mod retrieve_data;
|
|||||||
mod telemetry;
|
mod telemetry;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_utilities;
|
mod test_utilities;
|
||||||
|
mod urban_ground;
|
||||||
mod version_check;
|
mod version_check;
|
||||||
mod world_editor;
|
mod world_editor;
|
||||||
|
mod world_utils;
|
||||||
|
|
||||||
use args::Args;
|
use args::Args;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::{env, fs, io::Write};
|
use std::{env, fs, io::Write};
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
@@ -90,6 +93,54 @@ fn run_cli() {
|
|||||||
// Parse input arguments
|
// Parse input arguments
|
||||||
let args: Args = Args::parse();
|
let args: Args = Args::parse();
|
||||||
|
|
||||||
|
// Validate arguments (path requirements differ between Java and Bedrock)
|
||||||
|
if let Err(e) = args::validate_args(&args) {
|
||||||
|
eprintln!("{}: {}", "Error".red().bold(), e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early guard: --bedrock requires the bedrock cargo feature
|
||||||
|
if args.bedrock && !cfg!(feature = "bedrock") {
|
||||||
|
eprintln!(
|
||||||
|
"{}: The --bedrock flag requires the 'bedrock' feature. Rebuild with: cargo build --features bedrock",
|
||||||
|
"Error".red().bold()
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine world format and output path
|
||||||
|
let world_format = if args.bedrock {
|
||||||
|
world_editor::WorldFormat::BedrockMcWorld
|
||||||
|
} else {
|
||||||
|
world_editor::WorldFormat::JavaAnvil
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the generation output path and level name
|
||||||
|
let (generation_path, level_name) = if args.bedrock {
|
||||||
|
// Bedrock: generate .mcworld file in user-specified path or Desktop
|
||||||
|
let output_dir = args
|
||||||
|
.path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(world_utils::get_bedrock_output_directory);
|
||||||
|
let (output_path, lvl_name) = world_utils::build_bedrock_output(&args.bbox, output_dir);
|
||||||
|
(output_path, Some(lvl_name))
|
||||||
|
} else {
|
||||||
|
// Java: create a new world in the provided output directory
|
||||||
|
let base_dir = args.path.clone().unwrap();
|
||||||
|
let world_path = match world_utils::create_new_world(&base_dir) {
|
||||||
|
Ok(path) => PathBuf::from(path),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{} {}", "Error:".red().bold(), e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"Created new world at: {}",
|
||||||
|
world_path.display().to_string().bright_white().bold()
|
||||||
|
);
|
||||||
|
(world_path, None)
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch data
|
// Fetch data
|
||||||
let raw_data = match &args.file {
|
let raw_data = match &args.file {
|
||||||
Some(file) => retrieve_data::fetch_data_from_file(file),
|
Some(file) => retrieve_data::fetch_data_from_file(file),
|
||||||
@@ -130,8 +181,37 @@ fn run_cli() {
|
|||||||
// Transform map (parsed_elements). Operations are defined in a json file
|
// Transform map (parsed_elements). Operations are defined in a json file
|
||||||
map_transformation::transform_map(&mut parsed_elements, &mut xzbbox, &mut ground);
|
map_transformation::transform_map(&mut parsed_elements, &mut xzbbox, &mut ground);
|
||||||
|
|
||||||
|
// Build generation options
|
||||||
|
let generation_options = data_processing::GenerationOptions {
|
||||||
|
path: generation_path.clone(),
|
||||||
|
format: world_format,
|
||||||
|
level_name,
|
||||||
|
spawn_point: None,
|
||||||
|
};
|
||||||
|
|
||||||
// Generate world
|
// Generate world
|
||||||
let _ = data_processing::generate_world(parsed_elements, xzbbox, args.bbox, ground, &args);
|
match data_processing::generate_world_with_options(
|
||||||
|
parsed_elements,
|
||||||
|
xzbbox,
|
||||||
|
args.bbox,
|
||||||
|
ground,
|
||||||
|
&args,
|
||||||
|
generation_options,
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
if args.bedrock {
|
||||||
|
println!(
|
||||||
|
"{} Bedrock world saved to: {}",
|
||||||
|
"Done!".green().bold(),
|
||||||
|
generation_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{} {}", "Error:".red().bold(), e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -272,13 +272,17 @@ pub fn parse_osm_data(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only process multipolygons for now
|
// Process multipolygons and boundary relations
|
||||||
if tags.get("type").map(|x: &String| x.as_str()) != Some("multipolygon") {
|
let relation_type = tags.get("type").map(|x: &String| x.as_str());
|
||||||
|
if relation_type != Some("multipolygon") && relation_type != Some("boundary") {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Water relations require unclipped ways for ring merging in water_areas.rs
|
// Water relations require unclipped ways for ring merging in water_areas.rs
|
||||||
|
// Boundary relations also require unclipped ways for proper ring assembly
|
||||||
let is_water_relation = is_water_element(tags);
|
let is_water_relation = is_water_element(tags);
|
||||||
|
let is_boundary_relation = tags.contains_key("boundary");
|
||||||
|
let keep_unclipped = is_water_relation || is_boundary_relation;
|
||||||
|
|
||||||
let members: Vec<ProcessedMember> = element
|
let members: Vec<ProcessedMember> = element
|
||||||
.members
|
.members
|
||||||
@@ -304,9 +308,9 @@ pub fn parse_osm_data(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Water relations: keep unclipped for ring merging
|
// Water and boundary relations: keep unclipped for ring merging
|
||||||
// Non-water relations: clip member ways now
|
// Other relations: clip member ways now
|
||||||
let final_way = if is_water_relation {
|
let final_way = if keep_unclipped {
|
||||||
way
|
way
|
||||||
} else {
|
} else {
|
||||||
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
|
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ pub fn fetch_data_from_overpass(
|
|||||||
r#"[out:json][timeout:360][bbox:{},{},{},{}];
|
r#"[out:json][timeout:360][bbox:{},{},{},{}];
|
||||||
(
|
(
|
||||||
nwr["building"];
|
nwr["building"];
|
||||||
|
nwr["building:part"];
|
||||||
nwr["highway"];
|
nwr["highway"];
|
||||||
nwr["landuse"];
|
nwr["landuse"];
|
||||||
nwr["natural"];
|
nwr["natural"];
|
||||||
@@ -134,9 +135,18 @@ pub fn fetch_data_from_overpass(
|
|||||||
nwr["tourism"];
|
nwr["tourism"];
|
||||||
nwr["bridge"];
|
nwr["bridge"];
|
||||||
nwr["railway"];
|
nwr["railway"];
|
||||||
|
nwr["roller_coaster"];
|
||||||
nwr["barrier"];
|
nwr["barrier"];
|
||||||
nwr["entrance"];
|
nwr["entrance"];
|
||||||
nwr["door"];
|
nwr["door"];
|
||||||
|
nwr["boundary"];
|
||||||
|
nwr["power"];
|
||||||
|
nwr["historic"];
|
||||||
|
nwr["emergency"];
|
||||||
|
nwr["advertising"];
|
||||||
|
nwr["man_made"];
|
||||||
|
nwr["aeroway"];
|
||||||
|
way["place"];
|
||||||
way;
|
way;
|
||||||
)->.relsinbbox;
|
)->.relsinbbox;
|
||||||
(
|
(
|
||||||
|
|||||||
848
src/urban_ground.rs
Normal file
848
src/urban_ground.rs
Normal file
@@ -0,0 +1,848 @@
|
|||||||
|
//! Urban ground detection and generation based on building clusters.
|
||||||
|
//!
|
||||||
|
//! This module computes urban areas by analyzing building density and clustering,
|
||||||
|
//! then generates appropriate ground blocks (smooth stone) for those areas.
|
||||||
|
//!
|
||||||
|
//! # Algorithm Overview
|
||||||
|
//!
|
||||||
|
//! 1. **Grid-based density analysis**: Divide the world into cells and count buildings per cell
|
||||||
|
//! 2. **Connected component detection**: Find clusters of dense cells using flood fill
|
||||||
|
//! 3. **Cluster filtering**: Only keep clusters with enough buildings to be considered "urban"
|
||||||
|
//! 4. **Concave hull computation**: Compute a tight-fitting boundary around each cluster
|
||||||
|
//! 5. **Ground filling**: Fill the hull area with stone blocks
|
||||||
|
//!
|
||||||
|
//! This approach handles various scenarios:
|
||||||
|
//! - Full city coverage: Large connected cluster
|
||||||
|
//! - Multiple cities: Separate clusters, each gets its own hull
|
||||||
|
//! - Rural areas: No clusters meet threshold, no stone placed
|
||||||
|
//! - Isolated buildings: Don't meet cluster threshold, remain on grass
|
||||||
|
|
||||||
|
use crate::coordinate_system::cartesian::XZBBox;
|
||||||
|
use crate::floodfill::flood_fill_area;
|
||||||
|
use geo::{ConcaveHull, ConvexHull, MultiPoint, Point, Polygon, Simplify};
|
||||||
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Configuration for urban ground detection.
|
||||||
|
///
|
||||||
|
/// These parameters control how building clusters are identified and
|
||||||
|
/// how the urban ground boundary is computed.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UrbanGroundConfig {
|
||||||
|
/// Grid cell size for density analysis (in blocks).
|
||||||
|
/// Smaller = more precise but slower. Default: 64 blocks (4 chunks).
|
||||||
|
pub cell_size: i32,
|
||||||
|
|
||||||
|
/// Minimum buildings per cell to consider it potentially urban.
|
||||||
|
/// Cells below this threshold are ignored. Default: 1.
|
||||||
|
pub min_buildings_per_cell: usize,
|
||||||
|
|
||||||
|
/// Minimum total buildings in a connected cluster to be considered urban.
|
||||||
|
/// Small clusters (villages, isolated buildings) won't get stone ground. Default: 5.
|
||||||
|
pub min_buildings_for_cluster: usize,
|
||||||
|
|
||||||
|
/// Concavity parameter for hull computation (used in legacy hull-based method).
|
||||||
|
/// Lower = tighter fit to buildings (more concave), Higher = smoother (more convex).
|
||||||
|
/// Range: 1.0 (very tight) to 10.0 (almost convex). Default: 2.0.
|
||||||
|
pub concavity: f64,
|
||||||
|
|
||||||
|
/// Whether to expand the hull slightly beyond building boundaries (used in legacy method).
|
||||||
|
/// This creates a small buffer zone around the urban area. Default: true.
|
||||||
|
pub expand_hull: bool,
|
||||||
|
|
||||||
|
/// Base number of cells to expand the urban region.
|
||||||
|
/// This helps fill small gaps between buildings. Adaptive expansion may increase this.
|
||||||
|
/// Default: 2.
|
||||||
|
pub cell_expansion: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UrbanGroundConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
cell_size: 64, // Smaller cells for better granularity (4 chunks instead of 6)
|
||||||
|
min_buildings_per_cell: 1,
|
||||||
|
min_buildings_for_cluster: 5,
|
||||||
|
concavity: 2.0,
|
||||||
|
expand_hull: true,
|
||||||
|
cell_expansion: 2, // Larger expansion to connect spread-out buildings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a detected urban cluster with its buildings and computed boundary.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct UrbanCluster {
|
||||||
|
/// Grid cells that belong to this cluster
|
||||||
|
cells: Vec<(i32, i32)>,
|
||||||
|
/// Building centroids within this cluster
|
||||||
|
building_centroids: Vec<(i32, i32)>,
|
||||||
|
/// Total number of buildings in the cluster
|
||||||
|
building_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A compact lookup structure for checking if a coordinate is in an urban area.
|
||||||
|
///
|
||||||
|
/// Instead of storing millions of individual coordinates, this stores only
|
||||||
|
/// the cell indices (thousands) and performs O(1) lookups. This reduces
|
||||||
|
/// memory usage by ~4000x compared to storing all coordinates.
|
||||||
|
///
|
||||||
|
/// # Memory Usage
|
||||||
|
/// - 7.8 km² area: ~17K cells × 16 bytes = ~270 KB (vs ~560 MB for coordinates)
|
||||||
|
/// - 100 km² area: ~220K cells × 16 bytes = ~3.5 MB (vs ~7 GB for coordinates)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UrbanGroundLookup {
|
||||||
|
/// Set of cell indices (cx, cz) that are urban
|
||||||
|
urban_cells: HashSet<(i32, i32)>,
|
||||||
|
/// Cell size used for coordinate-to-cell conversion
|
||||||
|
cell_size: i32,
|
||||||
|
/// Bounding box origin for coordinate conversion
|
||||||
|
bbox_min_x: i32,
|
||||||
|
bbox_min_z: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UrbanGroundLookup {
|
||||||
|
/// Creates an empty lookup (no urban areas).
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
urban_cells: HashSet::new(),
|
||||||
|
cell_size: 64,
|
||||||
|
bbox_min_x: 0,
|
||||||
|
bbox_min_z: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given world coordinate is in an urban area.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_urban(&self, x: i32, z: i32) -> bool {
|
||||||
|
if self.urban_cells.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let cx = (x - self.bbox_min_x) / self.cell_size;
|
||||||
|
let cz = (z - self.bbox_min_z) / self.cell_size;
|
||||||
|
self.urban_cells.contains(&(cx, cz))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of urban cells.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn cell_count(&self) -> usize {
|
||||||
|
self.urban_cells.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there are no urban areas.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.urban_cells.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes urban ground areas from building locations.
|
||||||
|
pub struct UrbanGroundComputer {
|
||||||
|
config: UrbanGroundConfig,
|
||||||
|
building_centroids: Vec<(i32, i32)>,
|
||||||
|
xzbbox: XZBBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UrbanGroundComputer {
|
||||||
|
/// Creates a new urban ground computer with the given world bounds and configuration.
|
||||||
|
pub fn new(xzbbox: XZBBox, config: UrbanGroundConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
building_centroids: Vec::new(),
|
||||||
|
xzbbox,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new urban ground computer with default configuration.
|
||||||
|
pub fn with_defaults(xzbbox: XZBBox) -> Self {
|
||||||
|
Self::new(xzbbox, UrbanGroundConfig::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a building centroid to be considered for urban area detection.
|
||||||
|
#[inline]
|
||||||
|
pub fn add_building_centroid(&mut self, x: i32, z: i32) {
|
||||||
|
// Only add if within bounds
|
||||||
|
if x >= self.xzbbox.min_x()
|
||||||
|
&& x <= self.xzbbox.max_x()
|
||||||
|
&& z >= self.xzbbox.min_z()
|
||||||
|
&& z <= self.xzbbox.max_z()
|
||||||
|
{
|
||||||
|
self.building_centroids.push((x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds multiple building centroids from an iterator.
|
||||||
|
pub fn add_building_centroids<I>(&mut self, centroids: I)
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (i32, i32)>,
|
||||||
|
{
|
||||||
|
for (x, z) in centroids {
|
||||||
|
self.add_building_centroid(x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of buildings added.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn building_count(&self) -> usize {
|
||||||
|
self.building_centroids.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes all urban ground coordinates.
|
||||||
|
///
|
||||||
|
/// Returns a list of (x, z) coordinates that should have stone ground.
|
||||||
|
/// The coordinates are clipped to the world bounding box.
|
||||||
|
///
|
||||||
|
/// Performance: Uses cell-based filling for O(cells) complexity instead of
|
||||||
|
/// flood-filling complex hulls which would be O(area). For a city with 1000
|
||||||
|
/// buildings in 100 cells, this is ~100x faster than flood fill.
|
||||||
|
///
|
||||||
|
/// NOTE: For better performance and memory usage, prefer `compute_lookup()`.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn compute(&self, _timeout: Option<&Duration>) -> Vec<(i32, i32)> {
|
||||||
|
// Not enough buildings for any urban area
|
||||||
|
if self.building_centroids.len() < self.config.min_buildings_for_cluster {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Create density grid (cell -> buildings in that cell)
|
||||||
|
let grid = self.create_density_grid();
|
||||||
|
|
||||||
|
// Step 2: Find connected urban regions and get their expanded cells
|
||||||
|
let clusters = self.find_urban_clusters(&grid);
|
||||||
|
|
||||||
|
if clusters.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Fill cells directly instead of using expensive flood fill on hulls
|
||||||
|
// This is much faster: O(cells × cell_size²) vs O(hull_area) for flood fill
|
||||||
|
let mut all_coords = Vec::new();
|
||||||
|
for cluster in clusters {
|
||||||
|
let coords = self.fill_cluster_cells(&cluster);
|
||||||
|
all_coords.extend(coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
all_coords
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes urban ground and returns a compact lookup structure.
|
||||||
|
///
|
||||||
|
/// This is the preferred method for production use. Instead of returning
|
||||||
|
/// millions of coordinates (high memory), it returns a lookup structure
|
||||||
|
/// that stores only cell indices (~4000x less memory) and provides O(1)
|
||||||
|
/// coordinate lookups.
|
||||||
|
///
|
||||||
|
/// # Memory Comparison
|
||||||
|
/// - `compute()`: ~560 MB for 7.8 km² area
|
||||||
|
/// - `compute_lookup()`: ~270 KB for same area
|
||||||
|
pub fn compute_lookup(&self) -> UrbanGroundLookup {
|
||||||
|
// Not enough buildings for any urban area
|
||||||
|
if self.building_centroids.len() < self.config.min_buildings_for_cluster {
|
||||||
|
return UrbanGroundLookup::empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Create density grid (cell -> buildings in that cell)
|
||||||
|
let grid = self.create_density_grid();
|
||||||
|
|
||||||
|
// Step 2: Find connected urban regions and get their expanded cells
|
||||||
|
let clusters = self.find_urban_clusters(&grid);
|
||||||
|
|
||||||
|
if clusters.is_empty() {
|
||||||
|
return UrbanGroundLookup::empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Collect all expanded cells from all clusters into a HashSet
|
||||||
|
let mut urban_cells = HashSet::new();
|
||||||
|
for cluster in clusters {
|
||||||
|
urban_cells.extend(cluster.cells.iter().copied());
|
||||||
|
}
|
||||||
|
|
||||||
|
UrbanGroundLookup {
|
||||||
|
urban_cells,
|
||||||
|
cell_size: self.config.cell_size,
|
||||||
|
bbox_min_x: self.xzbbox.min_x(),
|
||||||
|
bbox_min_z: self.xzbbox.min_z(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fills all cells in a cluster directly, returning coordinates.
|
||||||
|
/// This is much faster than computing a hull and flood-filling it.
|
||||||
|
fn fill_cluster_cells(&self, cluster: &UrbanCluster) -> Vec<(i32, i32)> {
|
||||||
|
let mut coords = Vec::new();
|
||||||
|
let cell_size = self.config.cell_size;
|
||||||
|
|
||||||
|
// Pre-calculate bounds once
|
||||||
|
let bbox_min_x = self.xzbbox.min_x();
|
||||||
|
let bbox_max_x = self.xzbbox.max_x();
|
||||||
|
let bbox_min_z = self.xzbbox.min_z();
|
||||||
|
let bbox_max_z = self.xzbbox.max_z();
|
||||||
|
|
||||||
|
for &(cx, cz) in &cluster.cells {
|
||||||
|
// Calculate cell bounds in world coordinates
|
||||||
|
let cell_min_x = (bbox_min_x + cx * cell_size).max(bbox_min_x);
|
||||||
|
let cell_max_x = (bbox_min_x + (cx + 1) * cell_size - 1).min(bbox_max_x);
|
||||||
|
let cell_min_z = (bbox_min_z + cz * cell_size).max(bbox_min_z);
|
||||||
|
let cell_max_z = (bbox_min_z + (cz + 1) * cell_size - 1).min(bbox_max_z);
|
||||||
|
|
||||||
|
// Skip if cell is entirely outside bbox
|
||||||
|
if cell_min_x > bbox_max_x
|
||||||
|
|| cell_max_x < bbox_min_x
|
||||||
|
|| cell_min_z > bbox_max_z
|
||||||
|
|| cell_max_z < bbox_min_z
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill all coordinates in this cell
|
||||||
|
for x in cell_min_x..=cell_max_x {
|
||||||
|
for z in cell_min_z..=cell_max_z {
|
||||||
|
coords.push((x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coords
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a density grid mapping cell coordinates to buildings in that cell.
|
||||||
|
fn create_density_grid(&self) -> HashMap<(i32, i32), Vec<(i32, i32)>> {
|
||||||
|
let mut grid: HashMap<(i32, i32), Vec<(i32, i32)>> = HashMap::new();
|
||||||
|
|
||||||
|
for &(x, z) in &self.building_centroids {
|
||||||
|
let cell_x = (x - self.xzbbox.min_x()) / self.config.cell_size;
|
||||||
|
let cell_z = (z - self.xzbbox.min_z()) / self.config.cell_size;
|
||||||
|
grid.entry((cell_x, cell_z)).or_default().push((x, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
grid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds connected clusters of urban cells.
|
||||||
|
fn find_urban_clusters(
|
||||||
|
&self,
|
||||||
|
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||||
|
) -> Vec<UrbanCluster> {
|
||||||
|
// Step 1: Identify cells that meet minimum density threshold
|
||||||
|
let dense_cells: HashSet<(i32, i32)> = grid
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, buildings)| buildings.len() >= self.config.min_buildings_per_cell)
|
||||||
|
.map(|(&cell, _)| cell)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if dense_cells.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Calculate adaptive expansion based on building density
|
||||||
|
// For spread-out cities, we need more expansion to connect buildings
|
||||||
|
let adaptive_expansion = self.calculate_adaptive_expansion(&dense_cells, grid);
|
||||||
|
|
||||||
|
// Step 3: Expand dense cells to connect nearby clusters
|
||||||
|
let expanded_cells = self.expand_cells_adaptive(&dense_cells, adaptive_expansion);
|
||||||
|
|
||||||
|
// Step 4: Find connected components using flood fill
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
let mut clusters = Vec::new();
|
||||||
|
|
||||||
|
for &cell in &expanded_cells {
|
||||||
|
if visited.contains(&cell) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS to find connected component
|
||||||
|
let mut component_cells = Vec::new();
|
||||||
|
let mut queue = VecDeque::new();
|
||||||
|
queue.push_back(cell);
|
||||||
|
visited.insert(cell);
|
||||||
|
|
||||||
|
while let Some(current) = queue.pop_front() {
|
||||||
|
component_cells.push(current);
|
||||||
|
|
||||||
|
// Check 8-connected neighbors (including diagonals for better connectivity)
|
||||||
|
for dz in -1..=1 {
|
||||||
|
for dx in -1..=1 {
|
||||||
|
if dx == 0 && dz == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let neighbor = (current.0 + dx, current.1 + dz);
|
||||||
|
if expanded_cells.contains(&neighbor) && !visited.contains(&neighbor) {
|
||||||
|
visited.insert(neighbor);
|
||||||
|
queue.push_back(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect buildings from the original dense cells only (not expanded empty cells)
|
||||||
|
let mut cluster_buildings = Vec::new();
|
||||||
|
for &cell in &component_cells {
|
||||||
|
if let Some(buildings) = grid.get(&cell) {
|
||||||
|
cluster_buildings.extend(buildings.iter().copied());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let building_count = cluster_buildings.len();
|
||||||
|
|
||||||
|
// Only keep clusters with enough buildings
|
||||||
|
if building_count >= self.config.min_buildings_for_cluster {
|
||||||
|
clusters.push(UrbanCluster {
|
||||||
|
cells: component_cells,
|
||||||
|
building_centroids: cluster_buildings,
|
||||||
|
building_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clusters
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates adaptive expansion based on building density.
|
||||||
|
///
|
||||||
|
/// For spread-out cities (low density), we need more expansion to connect
|
||||||
|
/// buildings that are farther apart. For dense cities, less expansion is needed.
|
||||||
|
fn calculate_adaptive_expansion(
|
||||||
|
&self,
|
||||||
|
dense_cells: &HashSet<(i32, i32)>,
|
||||||
|
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||||
|
) -> i32 {
|
||||||
|
if dense_cells.is_empty() {
|
||||||
|
return self.config.cell_expansion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total buildings and average per occupied cell
|
||||||
|
let total_buildings: usize = dense_cells
|
||||||
|
.iter()
|
||||||
|
.filter_map(|cell| grid.get(cell))
|
||||||
|
.map(|buildings| buildings.len())
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
let avg_buildings_per_cell = total_buildings as f64 / dense_cells.len() as f64;
|
||||||
|
|
||||||
|
// Calculate the "spread" of cells - how far apart are occupied cells?
|
||||||
|
// Find bounding box of occupied cells
|
||||||
|
if dense_cells.len() < 2 {
|
||||||
|
return self.config.cell_expansion;
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_x = dense_cells.iter().map(|(x, _)| x).min().unwrap();
|
||||||
|
let max_x = dense_cells.iter().map(|(x, _)| x).max().unwrap();
|
||||||
|
let min_z = dense_cells.iter().map(|(_, z)| z).min().unwrap();
|
||||||
|
let max_z = dense_cells.iter().map(|(_, z)| z).max().unwrap();
|
||||||
|
|
||||||
|
let grid_span_x = (max_x - min_x + 1) as f64;
|
||||||
|
let grid_span_z = (max_z - min_z + 1) as f64;
|
||||||
|
let total_possible_cells = grid_span_x * grid_span_z;
|
||||||
|
|
||||||
|
// Cell occupancy ratio: what fraction of the bounding box has buildings?
|
||||||
|
let occupancy = dense_cells.len() as f64 / total_possible_cells;
|
||||||
|
|
||||||
|
// Adaptive expansion logic:
|
||||||
|
// - High density (many buildings per cell) AND high occupancy = dense city, use base expansion
|
||||||
|
// - Low density OR low occupancy = spread-out city, need more expansion
|
||||||
|
|
||||||
|
let base_expansion = self.config.cell_expansion;
|
||||||
|
|
||||||
|
// Scale factor: lower density = higher factor
|
||||||
|
// avg_buildings_per_cell < 2 → spread out
|
||||||
|
// occupancy < 0.3 → sparse grid with gaps
|
||||||
|
let density_factor = if avg_buildings_per_cell < 3.0 {
|
||||||
|
1.5
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let occupancy_factor = if occupancy < 0.4 {
|
||||||
|
1.5
|
||||||
|
} else if occupancy < 0.6 {
|
||||||
|
1.25
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let adaptive = (base_expansion as f64 * density_factor * occupancy_factor).ceil() as i32;
|
||||||
|
|
||||||
|
// Cap at reasonable maximum (4 cells = 256 blocks with 64-block cells)
|
||||||
|
adaptive.min(4).max(base_expansion)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands the set of cells by adding neighbors within expansion distance.
|
||||||
|
fn expand_cells_adaptive(
|
||||||
|
&self,
|
||||||
|
cells: &HashSet<(i32, i32)>,
|
||||||
|
expansion: i32,
|
||||||
|
) -> HashSet<(i32, i32)> {
|
||||||
|
if expansion <= 0 {
|
||||||
|
return cells.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut expanded = cells.clone();
|
||||||
|
|
||||||
|
for &(cx, cz) in cells {
|
||||||
|
for dz in -expansion..=expansion {
|
||||||
|
for dx in -expansion..=expansion {
|
||||||
|
expanded.insert((cx + dx, cz + dz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands the set of cells by adding neighbors within expansion distance.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn expand_cells(&self, cells: &HashSet<(i32, i32)>) -> HashSet<(i32, i32)> {
|
||||||
|
self.expand_cells_adaptive(cells, self.config.cell_expansion)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes ground coordinates for a single urban cluster.
|
||||||
|
///
|
||||||
|
/// NOTE: This hull-based method is kept for reference but not used in production.
|
||||||
|
/// The cell-based `fill_cluster_cells` method is much faster.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn compute_cluster_ground(
|
||||||
|
&self,
|
||||||
|
cluster: &UrbanCluster,
|
||||||
|
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||||
|
timeout: Option<&Duration>,
|
||||||
|
) -> Vec<(i32, i32)> {
|
||||||
|
// Need at least 3 points for a hull
|
||||||
|
if cluster.building_centroids.len() < 3 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect points for hull computation
|
||||||
|
// Include building centroids plus cell corner points for better coverage
|
||||||
|
let mut hull_points: Vec<(f64, f64)> = cluster
|
||||||
|
.building_centroids
|
||||||
|
.iter()
|
||||||
|
.map(|&(x, z)| (x as f64, z as f64))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Add cell boundary points if expand_hull is enabled
|
||||||
|
// This ensures the hull extends slightly beyond buildings
|
||||||
|
if self.config.expand_hull {
|
||||||
|
for &(cx, cz) in &cluster.cells {
|
||||||
|
// Only add corners for cells that actually have buildings
|
||||||
|
if grid.get(&(cx, cz)).map(|b| !b.is_empty()).unwrap_or(false) {
|
||||||
|
let base_x = (self.xzbbox.min_x() + cx * self.config.cell_size) as f64;
|
||||||
|
let base_z = (self.xzbbox.min_z() + cz * self.config.cell_size) as f64;
|
||||||
|
let size = self.config.cell_size as f64;
|
||||||
|
|
||||||
|
// Add cell corners with small padding
|
||||||
|
let pad = size * 0.1; // 10% padding
|
||||||
|
hull_points.push((base_x - pad, base_z - pad));
|
||||||
|
hull_points.push((base_x + size + pad, base_z - pad));
|
||||||
|
hull_points.push((base_x - pad, base_z + size + pad));
|
||||||
|
hull_points.push((base_x + size + pad, base_z + size + pad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to geo MultiPoint
|
||||||
|
let multi_point: MultiPoint<f64> =
|
||||||
|
hull_points.iter().map(|&(x, z)| Point::new(x, z)).collect();
|
||||||
|
|
||||||
|
// Compute hull based on point count
|
||||||
|
let hull: Polygon<f64> = if hull_points.len() < 10 {
|
||||||
|
// Too few points for concave hull, use convex
|
||||||
|
multi_point.convex_hull()
|
||||||
|
} else {
|
||||||
|
// Use concave hull for better fit
|
||||||
|
multi_point.concave_hull(self.config.concavity)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplify the hull to reduce vertex count (improves flood fill performance)
|
||||||
|
let hull = hull.simplify(2.0);
|
||||||
|
|
||||||
|
// Convert hull to integer coordinates for flood fill
|
||||||
|
self.fill_hull_polygon(&hull, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fills a hull polygon and returns all interior coordinates.
|
||||||
|
///
|
||||||
|
/// NOTE: This method is kept for reference but not used in production.
|
||||||
|
/// The cell-based approach is much faster.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn fill_hull_polygon(
|
||||||
|
&self,
|
||||||
|
polygon: &Polygon<f64>,
|
||||||
|
timeout: Option<&Duration>,
|
||||||
|
) -> Vec<(i32, i32)> {
|
||||||
|
// Convert polygon exterior to integer coordinates
|
||||||
|
let exterior: Vec<(i32, i32)> = polygon
|
||||||
|
.exterior()
|
||||||
|
.coords()
|
||||||
|
.map(|c| (c.x.round() as i32, c.y.round() as i32))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if exterior.len() < 3 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicate consecutive points (can cause flood fill issues)
|
||||||
|
let mut clean_exterior = Vec::with_capacity(exterior.len());
|
||||||
|
for point in exterior {
|
||||||
|
if clean_exterior.last() != Some(&point) {
|
||||||
|
clean_exterior.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the polygon is closed
|
||||||
|
if clean_exterior.first() != clean_exterior.last() && !clean_exterior.is_empty() {
|
||||||
|
clean_exterior.push(clean_exterior[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if clean_exterior.len() < 4 {
|
||||||
|
// Need at least 3 unique points + closing point
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing flood fill, clipping to bbox
|
||||||
|
let filled = flood_fill_area(&clean_exterior, timeout);
|
||||||
|
|
||||||
|
// Filter to only include points within world bounds
|
||||||
|
filled
|
||||||
|
.into_iter()
|
||||||
|
.filter(|&(x, z)| {
|
||||||
|
x >= self.xzbbox.min_x()
|
||||||
|
&& x <= self.xzbbox.max_x()
|
||||||
|
&& z >= self.xzbbox.min_z()
|
||||||
|
&& z <= self.xzbbox.max_z()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the centroid of a set of coordinates.
|
||||||
|
///
|
||||||
|
/// Returns None if the slice is empty.
|
||||||
|
#[inline]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn compute_centroid(coords: &[(i32, i32)]) -> Option<(i32, i32)> {
|
||||||
|
if coords.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sum_x: i64 = coords.iter().map(|(x, _)| i64::from(*x)).sum();
|
||||||
|
let sum_z: i64 = coords.iter().map(|(_, z)| i64::from(*z)).sum();
|
||||||
|
let len = coords.len() as i64;
|
||||||
|
Some(((sum_x / len) as i32, (sum_z / len) as i32))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function to compute urban ground from building centroids.
|
||||||
|
///
|
||||||
|
/// NOTE: This function is kept for backward compatibility and tests.
|
||||||
|
/// For production use, prefer `compute_urban_ground_lookup` which uses
|
||||||
|
/// ~4000x less memory.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn compute_urban_ground(
|
||||||
|
building_centroids: Vec<(i32, i32)>,
|
||||||
|
xzbbox: &XZBBox,
|
||||||
|
timeout: Option<&Duration>,
|
||||||
|
) -> Vec<(i32, i32)> {
|
||||||
|
let mut computer = UrbanGroundComputer::with_defaults(xzbbox.clone());
|
||||||
|
computer.add_building_centroids(building_centroids);
|
||||||
|
computer.compute(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes urban ground and returns a compact lookup structure.
|
||||||
|
///
|
||||||
|
/// This is the preferred entry point for production use. Returns a lookup
|
||||||
|
/// structure that uses ~270 KB instead of ~560 MB for a typical city area.
|
||||||
|
pub fn compute_urban_ground_lookup(
|
||||||
|
building_centroids: Vec<(i32, i32)>,
|
||||||
|
xzbbox: &XZBBox,
|
||||||
|
) -> UrbanGroundLookup {
|
||||||
|
let mut computer = UrbanGroundComputer::with_defaults(xzbbox.clone());
|
||||||
|
computer.add_building_centroids(building_centroids);
|
||||||
|
computer.compute_lookup()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn create_test_bbox() -> XZBBox {
|
||||||
|
XZBBox::rect_from_xz_lengths(1000.0, 1000.0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_buildings() {
|
||||||
|
let computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||||
|
let result = computer.compute(None);
|
||||||
|
assert!(result.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_few_scattered_buildings() {
|
||||||
|
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||||
|
// Add a few scattered buildings (not enough for a cluster)
|
||||||
|
computer.add_building_centroid(100, 100);
|
||||||
|
computer.add_building_centroid(500, 500);
|
||||||
|
computer.add_building_centroid(900, 900);
|
||||||
|
|
||||||
|
let result = computer.compute(None);
|
||||||
|
assert!(
|
||||||
|
result.is_empty(),
|
||||||
|
"Scattered buildings should not form urban area"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dense_cluster() {
|
||||||
|
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||||
|
|
||||||
|
// Add a dense cluster of buildings
|
||||||
|
for i in 0..30 {
|
||||||
|
for j in 0..30 {
|
||||||
|
if (i + j) % 3 == 0 {
|
||||||
|
// Add building every 3rd position
|
||||||
|
computer.add_building_centroid(100 + i * 10, 100 + j * 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = computer.compute(None);
|
||||||
|
assert!(
|
||||||
|
!result.is_empty(),
|
||||||
|
"Dense cluster should produce urban area"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compute_centroid() {
|
||||||
|
let coords = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
|
||||||
|
let centroid = compute_centroid(&coords);
|
||||||
|
assert_eq!(centroid, Some((5, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compute_centroid_empty() {
|
||||||
|
let coords: Vec<(i32, i32)> = vec![];
|
||||||
|
let centroid = compute_centroid(&coords);
|
||||||
|
assert_eq!(centroid, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spread_out_buildings() {
|
||||||
|
// Simulate a spread-out city like Erding where buildings are farther apart
|
||||||
|
// This should still be detected as urban due to adaptive expansion
|
||||||
|
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||||
|
|
||||||
|
// Add buildings spread across a larger area with gaps
|
||||||
|
// Buildings are ~100-150 blocks apart (would fail with small expansion)
|
||||||
|
let building_positions = [
|
||||||
|
(100, 100),
|
||||||
|
(250, 100),
|
||||||
|
(400, 100),
|
||||||
|
(100, 250),
|
||||||
|
(250, 250),
|
||||||
|
(400, 250),
|
||||||
|
(100, 400),
|
||||||
|
(250, 400),
|
||||||
|
(400, 400),
|
||||||
|
// Add a few more to ensure cluster threshold is met
|
||||||
|
(175, 175),
|
||||||
|
(325, 175),
|
||||||
|
(175, 325),
|
||||||
|
(325, 325),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (x, z) in building_positions {
|
||||||
|
computer.add_building_centroid(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = computer.compute(None);
|
||||||
|
assert!(
|
||||||
|
!result.is_empty(),
|
||||||
|
"Spread-out buildings should still form urban area with adaptive expansion"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_adaptive_expansion_calculated() {
|
||||||
|
let bbox = create_test_bbox();
|
||||||
|
let computer = UrbanGroundComputer::with_defaults(bbox);
|
||||||
|
|
||||||
|
// Create a sparse grid with low occupancy
|
||||||
|
let mut dense_cells = HashSet::new();
|
||||||
|
// Only 4 cells in a 10x10 potential grid = 4% occupancy
|
||||||
|
dense_cells.insert((0, 0));
|
||||||
|
dense_cells.insert((5, 0));
|
||||||
|
dense_cells.insert((0, 5));
|
||||||
|
dense_cells.insert((5, 5));
|
||||||
|
|
||||||
|
let mut grid = HashMap::new();
|
||||||
|
// Only 1 building per cell (low density)
|
||||||
|
grid.insert((0, 0), vec![(10, 10)]);
|
||||||
|
grid.insert((5, 0), vec![(330, 10)]);
|
||||||
|
grid.insert((0, 5), vec![(10, 330)]);
|
||||||
|
grid.insert((5, 5), vec![(330, 330)]);
|
||||||
|
|
||||||
|
let expansion = computer.calculate_adaptive_expansion(&dense_cells, &grid);
|
||||||
|
|
||||||
|
// Should be higher than base (2) due to low occupancy and density
|
||||||
|
assert!(
|
||||||
|
expansion > 2,
|
||||||
|
"Sparse grid should trigger higher expansion, got {}",
|
||||||
|
expansion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lookup_empty() {
|
||||||
|
let lookup = UrbanGroundLookup::empty();
|
||||||
|
assert!(lookup.is_empty());
|
||||||
|
assert!(!lookup.is_urban(100, 100));
|
||||||
|
assert_eq!(lookup.cell_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lookup_membership() {
|
||||||
|
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||||
|
|
||||||
|
// Create a dense cluster of buildings
|
||||||
|
for x in 0..10 {
|
||||||
|
for z in 0..10 {
|
||||||
|
computer.add_building_centroid(100 + x * 10, 100 + z * 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lookup = computer.compute_lookup();
|
||||||
|
assert!(!lookup.is_empty());
|
||||||
|
|
||||||
|
// Points inside the cluster should be urban
|
||||||
|
assert!(
|
||||||
|
lookup.is_urban(150, 150),
|
||||||
|
"Center of cluster should be urban"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Points far outside the cluster should not be urban
|
||||||
|
assert!(
|
||||||
|
!lookup.is_urban(900, 900),
|
||||||
|
"Point far from cluster should not be urban"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lookup_vs_compute_consistency() {
|
||||||
|
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||||
|
|
||||||
|
// Create a medium-sized cluster
|
||||||
|
for x in 0..5 {
|
||||||
|
for z in 0..5 {
|
||||||
|
computer.add_building_centroid(200 + x * 20, 200 + z * 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let coords = computer.compute(None);
|
||||||
|
let lookup = computer.compute_lookup();
|
||||||
|
|
||||||
|
// Every coordinate from compute() should be marked urban in lookup
|
||||||
|
for (x, z) in &coords {
|
||||||
|
assert!(
|
||||||
|
lookup.is_urban(*x, *z),
|
||||||
|
"Coordinate ({}, {}) should be urban in lookup",
|
||||||
|
x,
|
||||||
|
z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,14 @@ use crate::ground::Ground;
|
|||||||
use crate::progress::emit_gui_progress_update;
|
use crate::progress::emit_gui_progress_update;
|
||||||
|
|
||||||
use bedrockrs_level::level::db_interface::bedrock_key::ChunkKey;
|
use bedrockrs_level::level::db_interface::bedrock_key::ChunkKey;
|
||||||
use bedrockrs_level::level::db_interface::rusty::RustyDBInterface;
|
use bedrockrs_level::level::db_interface::key_level::KeyTypeTag;
|
||||||
|
use bedrockrs_level::level::db_interface::rusty::{mcpe_options, RustyDBInterface};
|
||||||
use bedrockrs_level::level::file_interface::RawWorldTrait;
|
use bedrockrs_level::level::file_interface::RawWorldTrait;
|
||||||
use bedrockrs_shared::world::dimension::Dimension;
|
use bedrockrs_shared::world::dimension::Dimension;
|
||||||
use byteorder::{LittleEndian, WriteBytesExt};
|
use byteorder::{LittleEndian, WriteBytesExt};
|
||||||
|
use fastnbt::Value;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use rusty_leveldb::DB;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap as StdHashMap;
|
use std::collections::HashMap as StdHashMap;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
@@ -82,6 +85,8 @@ impl From<serde_json::Error> for BedrockSaveError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_BEDROCK_COMPRESSION_LEVEL: u8 = 6;
|
||||||
|
|
||||||
/// Metadata for Bedrock worlds
|
/// Metadata for Bedrock worlds
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct BedrockMetadata {
|
struct BedrockMetadata {
|
||||||
@@ -402,7 +407,7 @@ impl BedrockWriter {
|
|||||||
// Open LevelDB with Bedrock-compatible options
|
// Open LevelDB with Bedrock-compatible options
|
||||||
let mut state = ();
|
let mut state = ();
|
||||||
let mut db: RustyDBInterface<()> =
|
let mut db: RustyDBInterface<()> =
|
||||||
RustyDBInterface::new(db_path.into_boxed_path(), true, &mut state)
|
RustyDBInterface::new(db_path.clone().into_boxed_path(), true, &mut state)
|
||||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||||
|
|
||||||
// Count total chunks for progress
|
// Count total chunks for progress
|
||||||
@@ -416,63 +421,128 @@ impl BedrockWriter {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
{
|
||||||
progress_bar.set_style(
|
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||||
ProgressStyle::default_bar()
|
progress_bar.set_style(
|
||||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
ProgressStyle::default_bar()
|
||||||
.unwrap()
|
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||||
.progress_chars("█▓░"),
|
.unwrap()
|
||||||
);
|
.progress_chars("█▓░"),
|
||||||
|
);
|
||||||
|
|
||||||
let mut chunks_processed: usize = 0;
|
let mut chunks_processed: usize = 0;
|
||||||
|
|
||||||
|
// Process each region and chunk
|
||||||
|
for ((region_x, region_z), region) in &world.regions {
|
||||||
|
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
||||||
|
// Calculate absolute chunk coordinates
|
||||||
|
let abs_chunk_x = region_x * 32 + local_chunk_x;
|
||||||
|
let abs_chunk_z = region_z * 32 + local_chunk_z;
|
||||||
|
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
|
||||||
|
|
||||||
|
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
|
||||||
|
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
||||||
|
db.set_subchunk_raw(version_key, &[42], &mut state)
|
||||||
|
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||||
|
|
||||||
|
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
||||||
|
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
||||||
|
let data3d = self.create_data3d(chunk);
|
||||||
|
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
||||||
|
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||||
|
|
||||||
|
// Process each section (subchunk)
|
||||||
|
for (§ion_y, section) in &chunk.sections {
|
||||||
|
// Encode the subchunk
|
||||||
|
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
|
||||||
|
|
||||||
|
// Write to database
|
||||||
|
let subchunk_key =
|
||||||
|
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
|
||||||
|
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
|
||||||
|
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks_processed += 1;
|
||||||
|
progress_bar.inc(1);
|
||||||
|
|
||||||
|
// Update GUI progress (92% to 97% range for chunk writing)
|
||||||
|
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
|
||||||
|
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
|
||||||
|
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
|
||||||
|
emit_gui_progress_update(gui_progress, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress_bar.finish_with_message("Chunks written to LevelDB");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the RustyDBInterface handle is dropped before opening another DB for the same path.
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
self.write_chunk_entities(world, &db_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_chunk_entities(
|
||||||
|
&self,
|
||||||
|
world: &WorldToModify,
|
||||||
|
db_path: &std::path::Path,
|
||||||
|
) -> Result<(), BedrockSaveError> {
|
||||||
|
let mut opts = mcpe_options(DEFAULT_BEDROCK_COMPRESSION_LEVEL);
|
||||||
|
opts.create_if_missing = true;
|
||||||
|
let mut db = DB::open(db_path.to_path_buf().into_boxed_path(), opts)
|
||||||
|
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||||
|
|
||||||
// Process each region and chunk
|
|
||||||
for ((region_x, region_z), region) in &world.regions {
|
for ((region_x, region_z), region) in &world.regions {
|
||||||
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
||||||
// Calculate absolute chunk coordinates
|
let chunk_pos =
|
||||||
let abs_chunk_x = region_x * 32 + local_chunk_x;
|
Vec2::new(region_x * 32 + local_chunk_x, region_z * 32 + local_chunk_z);
|
||||||
let abs_chunk_z = region_z * 32 + local_chunk_z;
|
|
||||||
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
|
|
||||||
|
|
||||||
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
|
self.write_compound_list_record(
|
||||||
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
&mut db,
|
||||||
db.set_subchunk_raw(version_key, &[42], &mut state)
|
chunk_pos,
|
||||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
KeyTypeTag::BlockEntity,
|
||||||
|
chunk.other.get("block_entities"),
|
||||||
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
)?;
|
||||||
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
self.write_compound_list_record(
|
||||||
let data3d = self.create_data3d(chunk);
|
&mut db,
|
||||||
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
chunk_pos,
|
||||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
KeyTypeTag::Entity,
|
||||||
|
chunk.other.get("entities"),
|
||||||
// Process each section (subchunk)
|
)?;
|
||||||
for (§ion_y, section) in &chunk.sections {
|
|
||||||
// Encode the subchunk
|
|
||||||
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
|
|
||||||
|
|
||||||
// Write to database
|
|
||||||
let subchunk_key =
|
|
||||||
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
|
|
||||||
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
|
|
||||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks_processed += 1;
|
|
||||||
progress_bar.inc(1);
|
|
||||||
|
|
||||||
// Update GUI progress (92% to 97% range for chunk writing)
|
|
||||||
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
|
|
||||||
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
|
|
||||||
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
|
|
||||||
emit_gui_progress_update(gui_progress, "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress_bar.finish_with_message("Chunks written to LevelDB");
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// LevelDB writes are flushed when the database is dropped
|
fn write_compound_list_record(
|
||||||
drop(db);
|
&self,
|
||||||
|
db: &mut DB,
|
||||||
|
chunk_pos: Vec2<i32>,
|
||||||
|
key_type: KeyTypeTag,
|
||||||
|
value: Option<&Value>,
|
||||||
|
) -> Result<(), BedrockSaveError> {
|
||||||
|
let Some(Value::List(values)) = value else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if values.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let deduped = dedup_compound_list(values);
|
||||||
|
if deduped.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = nbtx::to_le_bytes(&deduped).map_err(|e| BedrockSaveError::Nbt(e.to_string()))?;
|
||||||
|
let key = build_chunk_key_bytes(chunk_pos, Dimension::Overworld, key_type, None);
|
||||||
|
db.put(&key, &data)
|
||||||
|
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -737,6 +807,91 @@ fn bedrock_bits_per_block(palette_count: u32) -> u8 {
|
|||||||
16 // Maximum
|
16 // Maximum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_chunk_key_bytes(
|
||||||
|
chunk_pos: Vec2<i32>,
|
||||||
|
dimension: Dimension,
|
||||||
|
key_type: KeyTypeTag,
|
||||||
|
y_index: Option<i8>,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut buffer = Vec::with_capacity(
|
||||||
|
9 + if dimension != Dimension::Overworld {
|
||||||
|
4
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
} + 1,
|
||||||
|
);
|
||||||
|
buffer.extend_from_slice(&chunk_pos.x.to_le_bytes());
|
||||||
|
buffer.extend_from_slice(&chunk_pos.y.to_le_bytes());
|
||||||
|
|
||||||
|
if dimension != Dimension::Overworld {
|
||||||
|
buffer.extend_from_slice(&i32::from(dimension).to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(key_type.to_byte());
|
||||||
|
if let Some(y) = y_index {
|
||||||
|
buffer.push(y as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dedup_compound_list(values: &[Value]) -> Vec<Value> {
|
||||||
|
let mut coord_index: StdHashMap<(i32, i32, i32), usize> = StdHashMap::new();
|
||||||
|
let mut deduped: Vec<Value> = Vec::with_capacity(values.len());
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
if let Value::Compound(map) = value {
|
||||||
|
if let Some(coords) = get_entity_coords(map) {
|
||||||
|
if let Some(idx) = coord_index.get(&coords).copied() {
|
||||||
|
deduped[idx] = value.clone();
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
coord_index.insert(coords, deduped.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deduped.push(value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
deduped
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_entity_coords(entity: &StdHashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||||
|
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||||
|
if pos.len() == 3 {
|
||||||
|
if let (Some(x), Some(y), Some(z)) = (
|
||||||
|
value_to_i32(&pos[0]),
|
||||||
|
value_to_i32(&pos[1]),
|
||||||
|
value_to_i32(&pos[2]),
|
||||||
|
) {
|
||||||
|
return Some((x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (Some(x), Some(y), Some(z)) = (
|
||||||
|
entity.get("x").and_then(value_to_i32),
|
||||||
|
entity.get("y").and_then(value_to_i32),
|
||||||
|
entity.get("z").and_then(value_to_i32),
|
||||||
|
) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((x, y, z))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_i32(value: &Value) -> Option<i32> {
|
||||||
|
match value {
|
||||||
|
Value::Byte(v) => Some(i32::from(*v)),
|
||||||
|
Value::Short(v) => Some(i32::from(*v)),
|
||||||
|
Value::Int(v) => Some(*v),
|
||||||
|
Value::Long(v) => i32::try_from(*v).ok(),
|
||||||
|
Value::Float(v) => Some(*v as i32),
|
||||||
|
Value::Double(v) => Some(*v as i32),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Level.dat structure for Bedrock Edition
|
/// Level.dat structure for Bedrock Edition
|
||||||
/// This struct contains all required fields for a valid Bedrock world
|
/// This struct contains all required fields for a valid Bedrock world
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub(crate) struct Chunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Section within a chunk (16x16x16 blocks)
|
/// Section within a chunk (16x16x16 blocks)
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub(crate) struct Section {
|
pub(crate) struct Section {
|
||||||
pub block_states: Blockstates,
|
pub block_states: Blockstates,
|
||||||
#[serde(rename = "Y")]
|
#[serde(rename = "Y")]
|
||||||
@@ -37,7 +37,7 @@ pub(crate) struct Section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Block states within a section
|
/// Block states within a section
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub(crate) struct Blockstates {
|
pub(crate) struct Blockstates {
|
||||||
pub palette: Vec<PaletteItem>,
|
pub palette: Vec<PaletteItem>,
|
||||||
pub data: Option<LongArray>,
|
pub data: Option<LongArray>,
|
||||||
@@ -46,7 +46,7 @@ pub(crate) struct Blockstates {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Palette item for block state encoding
|
/// Palette item for block state encoding
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub(crate) struct PaletteItem {
|
pub(crate) struct PaletteItem {
|
||||||
#[serde(rename = "Name")]
|
#[serde(rename = "Name")]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -16,6 +16,24 @@ use std::collections::HashMap;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// Cached base chunk sections (grass at Y=-62)
|
||||||
|
/// Computed once on first use and reused for all empty chunks
|
||||||
|
static BASE_CHUNK_SECTIONS: OnceLock<Vec<Section>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Get or create the cached base chunk sections
|
||||||
|
fn get_base_chunk_sections() -> &'static [Section] {
|
||||||
|
BASE_CHUNK_SECTIONS.get_or_init(|| {
|
||||||
|
let mut chunk = ChunkToModify::default();
|
||||||
|
for x in 0..16 {
|
||||||
|
for z in 0..16 {
|
||||||
|
chunk.set_block(x, -62, z, GRASS_BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunk.sections().collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
use crate::telemetry::{send_log, LogLevel};
|
use crate::telemetry::{send_log, LogLevel};
|
||||||
@@ -47,23 +65,18 @@ impl<'a> WorldEditor<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to create a base chunk with grass blocks at Y -62
|
/// Helper function to create a base chunk with grass blocks at Y -62
|
||||||
|
/// Uses cached sections for efficiency - only serialization happens per chunk
|
||||||
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
|
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
|
||||||
let mut chunk = ChunkToModify::default();
|
// Use cached sections (computed once on first call)
|
||||||
|
let sections = get_base_chunk_sections();
|
||||||
|
|
||||||
// Fill the bottom layer with grass blocks at Y -62
|
// Prepare chunk data with cloned sections
|
||||||
for x in 0..16 {
|
|
||||||
for z in 0..16 {
|
|
||||||
chunk.set_block(x, -62, z, GRASS_BLOCK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare chunk data
|
|
||||||
let chunk_data = Chunk {
|
let chunk_data = Chunk {
|
||||||
sections: chunk.sections().collect(),
|
sections: sections.to_vec(),
|
||||||
x_pos: abs_chunk_x,
|
x_pos: abs_chunk_x,
|
||||||
z_pos: abs_chunk_z,
|
z_pos: abs_chunk_z,
|
||||||
is_light_on: 0,
|
is_light_on: 0,
|
||||||
other: chunk.other,
|
other: FnvHashMap::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the Level wrapper
|
// Create the Level wrapper
|
||||||
@@ -128,7 +141,8 @@ impl<'a> WorldEditor<'a> {
|
|||||||
|
|
||||||
/// Saves a single region to disk.
|
/// Saves a single region to disk.
|
||||||
///
|
///
|
||||||
/// This is extracted to allow streaming mode to save and release regions one at a time.
|
/// Optimized for new world creation, writes chunks directly without reading existing data.
|
||||||
|
/// This assumes we're creating a fresh world, not modifying an existing one.
|
||||||
fn save_single_region(
|
fn save_single_region(
|
||||||
&self,
|
&self,
|
||||||
region_x: i32,
|
region_x: i32,
|
||||||
@@ -138,81 +152,19 @@ impl<'a> WorldEditor<'a> {
|
|||||||
let mut region = self.create_region(region_x, region_z);
|
let mut region = self.create_region(region_x, region_z);
|
||||||
let mut ser_buffer = Vec::with_capacity(8192);
|
let mut ser_buffer = Vec::with_capacity(8192);
|
||||||
|
|
||||||
|
// First pass: write all chunks that have content
|
||||||
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
||||||
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
||||||
// Read existing chunk data if it exists
|
// Create chunk directly, we're writing to a fresh region file
|
||||||
let existing_data = region
|
// so there's no existing data to preserve
|
||||||
.read_chunk(chunk_x as usize, chunk_z as usize)
|
let chunk = Chunk {
|
||||||
.unwrap()
|
sections: chunk_to_modify.sections().collect(),
|
||||||
.unwrap_or_default();
|
x_pos: chunk_x + (region_x * 32),
|
||||||
|
z_pos: chunk_z + (region_z * 32),
|
||||||
// Parse existing chunk or create new one
|
is_light_on: 0,
|
||||||
let mut chunk: Chunk = if !existing_data.is_empty() {
|
other: chunk_to_modify.other.clone(),
|
||||||
fastnbt::from_bytes(&existing_data).unwrap()
|
|
||||||
} else {
|
|
||||||
Chunk {
|
|
||||||
sections: Vec::new(),
|
|
||||||
x_pos: chunk_x + (region_x * 32),
|
|
||||||
z_pos: chunk_z + (region_z * 32),
|
|
||||||
is_light_on: 0,
|
|
||||||
other: FnvHashMap::default(),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update sections while preserving existing data
|
|
||||||
let new_sections: Vec<Section> = chunk_to_modify.sections().collect();
|
|
||||||
for new_section in new_sections {
|
|
||||||
if let Some(existing_section) =
|
|
||||||
chunk.sections.iter_mut().find(|s| s.y == new_section.y)
|
|
||||||
{
|
|
||||||
// Merge block states
|
|
||||||
existing_section.block_states.palette = new_section.block_states.palette;
|
|
||||||
existing_section.block_states.data = new_section.block_states.data;
|
|
||||||
} else {
|
|
||||||
// Add new section if it doesn't exist
|
|
||||||
chunk.sections.push(new_section);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve existing block entities and merge with new ones
|
|
||||||
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
|
|
||||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
|
||||||
if let (Value::List(existing), Value::List(new)) =
|
|
||||||
(existing_entities, new_entities)
|
|
||||||
{
|
|
||||||
// Remove old entities that are replaced by new ones
|
|
||||||
existing.retain(|e| {
|
|
||||||
if let Value::Compound(map) = e {
|
|
||||||
let (x, y, z) = get_entity_coords(map);
|
|
||||||
!new.iter().any(|new_e| {
|
|
||||||
if let Value::Compound(new_map) = new_e {
|
|
||||||
let (nx, ny, nz) = get_entity_coords(new_map);
|
|
||||||
x == nx && y == ny && z == nz
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Add new entities
|
|
||||||
existing.extend(new.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no existing entities, just add the new ones
|
|
||||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
|
||||||
chunk
|
|
||||||
.other
|
|
||||||
.insert("block_entities".to_string(), new_entities.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update chunk coordinates and flags
|
|
||||||
chunk.x_pos = chunk_x + (region_x * 32);
|
|
||||||
chunk.z_pos = chunk_z + (region_z * 32);
|
|
||||||
|
|
||||||
// Create Level wrapper and save
|
// Create Level wrapper and save
|
||||||
let level_data = create_level_wrapper(&chunk);
|
let level_data = create_level_wrapper(&chunk);
|
||||||
ser_buffer.clear();
|
ser_buffer.clear();
|
||||||
@@ -223,7 +175,7 @@ impl<'a> WorldEditor<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: ensure all chunks exist
|
// Second pass: ensure all chunks exist (fill with base layer if not)
|
||||||
for chunk_x in 0..32 {
|
for chunk_x in 0..32 {
|
||||||
for chunk_z in 0..32 {
|
for chunk_z in 0..32 {
|
||||||
let abs_chunk_x = chunk_x + (region_x * 32);
|
let abs_chunk_x = chunk_x + (region_x * 32);
|
||||||
@@ -245,88 +197,138 @@ impl<'a> WorldEditor<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to get entity coordinates
|
/// Helper function to get entity coordinates
|
||||||
|
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
|
#[allow(dead_code)]
|
||||||
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
|
fn get_entity_coords(entity: &HashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||||
*x
|
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||||
} else {
|
if pos.len() == 3 {
|
||||||
0
|
if let (Some(x), Some(y), Some(z)) = (
|
||||||
|
value_to_i32(&pos[0]),
|
||||||
|
value_to_i32(&pos[1]),
|
||||||
|
value_to_i32(&pos[2]),
|
||||||
|
) {
|
||||||
|
return Some((x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (Some(x), Some(y), Some(z)) = (
|
||||||
|
entity.get("x").and_then(value_to_i32),
|
||||||
|
entity.get("y").and_then(value_to_i32),
|
||||||
|
entity.get("z").and_then(value_to_i32),
|
||||||
|
) else {
|
||||||
|
return None;
|
||||||
};
|
};
|
||||||
let y = if let Value::Int(y) = entity.get("y").unwrap_or(&Value::Int(0)) {
|
|
||||||
*y
|
Some((x, y, z))
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
|
|
||||||
*z
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
(x, y, z)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Level wrapper for chunk data (Java Edition format)
|
/// Creates a Level wrapper for chunk data (Java Edition format)
|
||||||
#[inline]
|
#[inline]
|
||||||
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
||||||
HashMap::from([(
|
let mut level_map = HashMap::from([
|
||||||
"Level".to_string(),
|
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||||
Value::Compound(HashMap::from([
|
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
(
|
||||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
"isLightOn".to_string(),
|
||||||
(
|
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
||||||
"isLightOn".to_string(),
|
),
|
||||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
(
|
||||||
),
|
"sections".to_string(),
|
||||||
(
|
Value::List(
|
||||||
"sections".to_string(),
|
chunk
|
||||||
Value::List(
|
.sections
|
||||||
chunk
|
.iter()
|
||||||
.sections
|
.map(|section| {
|
||||||
.iter()
|
let mut block_states = HashMap::from([(
|
||||||
.map(|section| {
|
"palette".to_string(),
|
||||||
let mut block_states = HashMap::from([(
|
Value::List(
|
||||||
"palette".to_string(),
|
section
|
||||||
Value::List(
|
.block_states
|
||||||
section
|
.palette
|
||||||
.block_states
|
.iter()
|
||||||
.palette
|
.map(|item| {
|
||||||
.iter()
|
let mut palette_item = HashMap::from([(
|
||||||
.map(|item| {
|
"Name".to_string(),
|
||||||
let mut palette_item = HashMap::from([(
|
Value::String(item.name.clone()),
|
||||||
"Name".to_string(),
|
)]);
|
||||||
Value::String(item.name.clone()),
|
if let Some(props) = &item.properties {
|
||||||
)]);
|
palette_item
|
||||||
if let Some(props) = &item.properties {
|
.insert("Properties".to_string(), props.clone());
|
||||||
palette_item.insert(
|
}
|
||||||
"Properties".to_string(),
|
Value::Compound(palette_item)
|
||||||
props.clone(),
|
})
|
||||||
);
|
.collect(),
|
||||||
}
|
),
|
||||||
Value::Compound(palette_item)
|
)]);
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
)]);
|
|
||||||
|
|
||||||
// Only add the `data` attribute if it's non-empty
|
// Only add the `data` attribute if it's non-empty
|
||||||
// to maintain compatibility with third-party tools like Dynmap
|
// to maintain compatibility with third-party tools like Dynmap
|
||||||
if let Some(data) = §ion.block_states.data {
|
if let Some(data) = §ion.block_states.data {
|
||||||
if !data.is_empty() {
|
if !data.is_empty() {
|
||||||
block_states.insert(
|
block_states
|
||||||
"data".to_string(),
|
.insert("data".to_string(), Value::LongArray(data.to_owned()));
|
||||||
Value::LongArray(data.to_owned()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Value::Compound(HashMap::from([
|
Value::Compound(HashMap::from([
|
||||||
("Y".to_string(), Value::Byte(section.y)),
|
("Y".to_string(), Value::Byte(section.y)),
|
||||||
("block_states".to_string(), Value::Compound(block_states)),
|
("block_states".to_string(), Value::Compound(block_states)),
|
||||||
]))
|
]))
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
])),
|
),
|
||||||
)])
|
]);
|
||||||
|
|
||||||
|
for (key, value) in &chunk.other {
|
||||||
|
level_map.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap::from([("Level".to_string(), Value::Compound(level_map))])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge compound lists (entities, block_entities) from chunk_to_modify into chunk
|
||||||
|
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn merge_compound_list(chunk: &mut Chunk, chunk_to_modify: &ChunkToModify, key: &str) {
|
||||||
|
if let Some(existing_entities) = chunk.other.get_mut(key) {
|
||||||
|
if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
||||||
|
if let (Value::List(existing), Value::List(new)) = (existing_entities, new_entities) {
|
||||||
|
existing.retain(|e| {
|
||||||
|
if let Value::Compound(map) = e {
|
||||||
|
if let Some((x, y, z)) = get_entity_coords(map) {
|
||||||
|
return !new.iter().any(|new_e| {
|
||||||
|
if let Value::Compound(new_map) = new_e {
|
||||||
|
get_entity_coords(new_map) == Some((x, y, z))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
existing.extend(new.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
||||||
|
chunk.other.insert(key.to_string(), new_entities.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert NBT Value to i32
|
||||||
|
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn value_to_i32(value: &Value) -> Option<i32> {
|
||||||
|
match value {
|
||||||
|
Value::Byte(v) => Some(i32::from(*v)),
|
||||||
|
Value::Short(v) => Some(i32::from(*v)),
|
||||||
|
Value::Int(v) => Some(*v),
|
||||||
|
Value::Long(v) => i32::try_from(*v).ok(),
|
||||||
|
Value::Float(v) => Some(*v as i32),
|
||||||
|
Value::Double(v) => Some(*v as i32),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ use crate::coordinate_system::geographic::LLBBox;
|
|||||||
use crate::ground::Ground;
|
use crate::ground::Ground;
|
||||||
use crate::progress::emit_gui_progress_update;
|
use crate::progress::emit_gui_progress_update;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use fastnbt::Value;
|
use fastnbt::{IntArray, Value};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::{hash_map::Entry, HashMap};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -243,6 +243,212 @@ impl<'a> WorldEditor<'a> {
|
|||||||
self.set_block(SIGN, x, y, z, None, None);
|
self.set_block(SIGN, x, y, z, None, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds an entity at the given coordinates (Y is ground-relative).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn add_entity(
|
||||||
|
&mut self,
|
||||||
|
id: &str,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
z: i32,
|
||||||
|
extra_data: Option<HashMap<String, Value>>,
|
||||||
|
) {
|
||||||
|
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let absolute_y = self.get_absolute_y(x, y, z);
|
||||||
|
|
||||||
|
let mut entity = HashMap::new();
|
||||||
|
entity.insert("id".to_string(), Value::String(id.to_string()));
|
||||||
|
entity.insert(
|
||||||
|
"Pos".to_string(),
|
||||||
|
Value::List(vec![
|
||||||
|
Value::Double(x as f64 + 0.5),
|
||||||
|
Value::Double(absolute_y as f64),
|
||||||
|
Value::Double(z as f64 + 0.5),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
entity.insert(
|
||||||
|
"Motion".to_string(),
|
||||||
|
Value::List(vec![
|
||||||
|
Value::Double(0.0),
|
||||||
|
Value::Double(0.0),
|
||||||
|
Value::Double(0.0),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
entity.insert(
|
||||||
|
"Rotation".to_string(),
|
||||||
|
Value::List(vec![Value::Float(0.0), Value::Float(0.0)]),
|
||||||
|
);
|
||||||
|
entity.insert("OnGround".to_string(), Value::Byte(1));
|
||||||
|
entity.insert("FallDistance".to_string(), Value::Float(0.0));
|
||||||
|
entity.insert("Fire".to_string(), Value::Short(-20));
|
||||||
|
entity.insert("Air".to_string(), Value::Short(300));
|
||||||
|
entity.insert("PortalCooldown".to_string(), Value::Int(0));
|
||||||
|
entity.insert(
|
||||||
|
"UUID".to_string(),
|
||||||
|
Value::IntArray(build_deterministic_uuid(id, x, absolute_y, z)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(extra) = extra_data {
|
||||||
|
for (key, value) in extra {
|
||||||
|
entity.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_x: i32 = x >> 4;
|
||||||
|
let chunk_z: i32 = z >> 4;
|
||||||
|
let region_x: i32 = chunk_x >> 5;
|
||||||
|
let region_z: i32 = chunk_z >> 5;
|
||||||
|
|
||||||
|
let region = self.world.get_or_create_region(region_x, region_z);
|
||||||
|
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||||
|
|
||||||
|
match chunk.other.entry("entities".to_string()) {
|
||||||
|
Entry::Occupied(mut entry) => {
|
||||||
|
if let Value::List(list) = entry.get_mut() {
|
||||||
|
list.push(Value::Compound(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry::Vacant(entry) => {
|
||||||
|
entry.insert(Value::List(vec![Value::Compound(entity)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Places a chest with the provided items at the given coordinates (ground-relative Y).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_chest_with_items(
|
||||||
|
&mut self,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
z: i32,
|
||||||
|
items: Vec<HashMap<String, Value>>,
|
||||||
|
) {
|
||||||
|
let absolute_y = self.get_absolute_y(x, y, z);
|
||||||
|
self.set_chest_with_items_absolute(x, absolute_y, z, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Places a chest with the provided items at the given coordinates (absolute Y).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_chest_with_items_absolute(
|
||||||
|
&mut self,
|
||||||
|
x: i32,
|
||||||
|
absolute_y: i32,
|
||||||
|
z: i32,
|
||||||
|
items: Vec<HashMap<String, Value>>,
|
||||||
|
) {
|
||||||
|
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_x: i32 = x >> 4;
|
||||||
|
let chunk_z: i32 = z >> 4;
|
||||||
|
let region_x: i32 = chunk_x >> 5;
|
||||||
|
let region_z: i32 = chunk_z >> 5;
|
||||||
|
|
||||||
|
let mut chest_data = HashMap::new();
|
||||||
|
chest_data.insert(
|
||||||
|
"id".to_string(),
|
||||||
|
Value::String("minecraft:chest".to_string()),
|
||||||
|
);
|
||||||
|
chest_data.insert("x".to_string(), Value::Int(x));
|
||||||
|
chest_data.insert("y".to_string(), Value::Int(absolute_y));
|
||||||
|
chest_data.insert("z".to_string(), Value::Int(z));
|
||||||
|
chest_data.insert(
|
||||||
|
"Items".to_string(),
|
||||||
|
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||||
|
);
|
||||||
|
chest_data.insert("keepPacked".to_string(), Value::Byte(0));
|
||||||
|
|
||||||
|
let region = self.world.get_or_create_region(region_x, region_z);
|
||||||
|
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||||
|
|
||||||
|
match chunk.other.entry("block_entities".to_string()) {
|
||||||
|
Entry::Occupied(mut entry) => {
|
||||||
|
if let Value::List(list) = entry.get_mut() {
|
||||||
|
list.push(Value::Compound(chest_data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry::Vacant(entry) => {
|
||||||
|
entry.insert(Value::List(vec![Value::Compound(chest_data)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_block_absolute(CHEST, x, absolute_y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Places a block entity with items at the given coordinates (ground-relative Y).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_block_entity_with_items(
|
||||||
|
&mut self,
|
||||||
|
block_with_props: BlockWithProperties,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
z: i32,
|
||||||
|
block_entity_id: &str,
|
||||||
|
items: Vec<HashMap<String, Value>>,
|
||||||
|
) {
|
||||||
|
let absolute_y = self.get_absolute_y(x, y, z);
|
||||||
|
self.set_block_entity_with_items_absolute(
|
||||||
|
block_with_props,
|
||||||
|
x,
|
||||||
|
absolute_y,
|
||||||
|
z,
|
||||||
|
block_entity_id,
|
||||||
|
items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Places a block entity with items at the given coordinates (absolute Y).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_block_entity_with_items_absolute(
|
||||||
|
&mut self,
|
||||||
|
block_with_props: BlockWithProperties,
|
||||||
|
x: i32,
|
||||||
|
absolute_y: i32,
|
||||||
|
z: i32,
|
||||||
|
block_entity_id: &str,
|
||||||
|
items: Vec<HashMap<String, Value>>,
|
||||||
|
) {
|
||||||
|
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_x: i32 = x >> 4;
|
||||||
|
let chunk_z: i32 = z >> 4;
|
||||||
|
let region_x: i32 = chunk_x >> 5;
|
||||||
|
let region_z: i32 = chunk_z >> 5;
|
||||||
|
|
||||||
|
let mut block_entity = HashMap::new();
|
||||||
|
block_entity.insert("id".to_string(), Value::String(block_entity_id.to_string()));
|
||||||
|
block_entity.insert("x".to_string(), Value::Int(x));
|
||||||
|
block_entity.insert("y".to_string(), Value::Int(absolute_y));
|
||||||
|
block_entity.insert("z".to_string(), Value::Int(z));
|
||||||
|
block_entity.insert(
|
||||||
|
"Items".to_string(),
|
||||||
|
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||||
|
);
|
||||||
|
block_entity.insert("keepPacked".to_string(), Value::Byte(0));
|
||||||
|
|
||||||
|
let region = self.world.get_or_create_region(region_x, region_z);
|
||||||
|
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||||
|
|
||||||
|
match chunk.other.entry("block_entities".to_string()) {
|
||||||
|
Entry::Occupied(mut entry) => {
|
||||||
|
if let Value::List(list) = entry.get_mut() {
|
||||||
|
list.push(Value::Compound(block_entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry::Vacant(entry) => {
|
||||||
|
entry.insert(Value::List(vec![Value::Compound(block_entity)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_block_with_properties_absolute(block_with_props, x, absolute_y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets a block of the specified type at the given coordinates.
|
/// Sets a block of the specified type at the given coordinates.
|
||||||
///
|
///
|
||||||
/// Y value is interpreted as an offset from ground level.
|
/// Y value is interpreted as an offset from ground level.
|
||||||
@@ -599,3 +805,30 @@ impl<'a> WorldEditor<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn build_deterministic_uuid(id: &str, x: i32, y: i32, z: i32) -> IntArray {
|
||||||
|
let mut hash: i64 = 17;
|
||||||
|
for byte in id.bytes() {
|
||||||
|
hash = hash.wrapping_mul(31).wrapping_add(byte as i64);
|
||||||
|
}
|
||||||
|
|
||||||
|
let seed_a = hash ^ (x as i64).wrapping_shl(32) ^ (y as i64).wrapping_mul(17);
|
||||||
|
let seed_b = hash.rotate_left(7) ^ (z as i64).wrapping_mul(31) ^ (x as i64).wrapping_mul(13);
|
||||||
|
|
||||||
|
IntArray::new(vec![
|
||||||
|
(seed_a >> 32) as i32,
|
||||||
|
seed_a as i32,
|
||||||
|
(seed_b >> 32) as i32,
|
||||||
|
seed_b as i32,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn single_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||||
|
let mut item = HashMap::new();
|
||||||
|
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||||
|
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||||
|
item.insert("Count".to_string(), Value::Byte(count));
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|||||||
207
src/world_utils.rs
Normal file
207
src/world_utils.rs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
use crate::coordinate_system::geographic::LLBBox;
|
||||||
|
use crate::retrieve_data;
|
||||||
|
use fastnbt::Value;
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::{fs, io::Write};
|
||||||
|
|
||||||
|
/// Returns the Desktop directory for Bedrock .mcworld file output.
|
||||||
|
/// Falls back to home directory, then current directory.
|
||||||
|
pub fn get_bedrock_output_directory() -> PathBuf {
|
||||||
|
dirs::desktop_dir()
|
||||||
|
.or_else(dirs::home_dir)
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the area name for a given bounding box using the center point.
|
||||||
|
pub fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
||||||
|
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
||||||
|
let center_lon = (bbox.min().lng() + bbox.max().lng()) / 2.0;
|
||||||
|
|
||||||
|
match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||||
|
Ok(Some(name)) => name,
|
||||||
|
_ => "Unknown Location".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitizes an area name for safe use in filesystem paths.
|
||||||
|
/// Replaces characters that are invalid on Windows/macOS/Linux, trims whitespace,
|
||||||
|
/// and limits length to prevent excessively long filenames.
|
||||||
|
pub fn sanitize_for_filename(name: &str) -> String {
|
||||||
|
let invalid_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*'];
|
||||||
|
let mut sanitized: String = name
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
if c.is_control() || invalid_chars.contains(&c) {
|
||||||
|
'_'
|
||||||
|
} else {
|
||||||
|
c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
sanitized = sanitized.trim().to_string();
|
||||||
|
|
||||||
|
// Limit length to avoid excessively long filenames
|
||||||
|
const MAX_LEN: usize = 64;
|
||||||
|
if sanitized.len() > MAX_LEN {
|
||||||
|
// Find a valid UTF-8 char boundary at or before MAX_LEN bytes
|
||||||
|
let cutoff = sanitized
|
||||||
|
.char_indices()
|
||||||
|
.take_while(|(idx, _)| *idx < MAX_LEN)
|
||||||
|
.last()
|
||||||
|
.map(|(idx, ch)| idx + ch.len_utf8())
|
||||||
|
.unwrap_or(0);
|
||||||
|
sanitized.truncate(cutoff);
|
||||||
|
sanitized = sanitized.trim_end().to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if sanitized.is_empty() {
|
||||||
|
"Unknown Location".to_string()
|
||||||
|
} else {
|
||||||
|
sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the Bedrock output path and level name for a given bounding box.
|
||||||
|
/// Combines area name lookup, sanitization, and path construction.
|
||||||
|
pub fn build_bedrock_output(bbox: &LLBBox, output_dir: PathBuf) -> (PathBuf, String) {
|
||||||
|
let area_name = get_area_name_for_bedrock(bbox);
|
||||||
|
let safe_name = sanitize_for_filename(&area_name);
|
||||||
|
let filename = format!("Arnis {safe_name}.mcworld");
|
||||||
|
let lvl_name = format!("Arnis World: {safe_name}");
|
||||||
|
(output_dir.join(&filename), lvl_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Java Edition world in the given base directory.
|
||||||
|
///
|
||||||
|
/// Generates a unique "Arnis World N" name, creates the directory structure
|
||||||
|
/// (with a `region/` subdirectory), writes the region template, level.dat
|
||||||
|
/// (with updated name, timestamp, and spawn position), and icon.png.
|
||||||
|
///
|
||||||
|
/// Returns the full path to the newly created world directory.
|
||||||
|
pub fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||||
|
// Generate a unique world name with proper counter
|
||||||
|
// Check for both "Arnis World X" and "Arnis World X: Location" patterns
|
||||||
|
let mut counter: i32 = 1;
|
||||||
|
let unique_name: String = loop {
|
||||||
|
let candidate_name: String = format!("Arnis World {counter}");
|
||||||
|
let candidate_path: PathBuf = base_path.join(&candidate_name);
|
||||||
|
|
||||||
|
// Check for exact match (no location suffix)
|
||||||
|
let exact_match_exists = candidate_path.exists();
|
||||||
|
|
||||||
|
// Check for worlds with location suffix (Arnis World X: Location)
|
||||||
|
let location_pattern = format!("Arnis World {counter}: ");
|
||||||
|
let location_match_exists = fs::read_dir(base_path)
|
||||||
|
.map(|entries| {
|
||||||
|
entries
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||||
|
.any(|name| name.starts_with(&location_pattern))
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !exact_match_exists && !location_match_exists {
|
||||||
|
break candidate_name;
|
||||||
|
}
|
||||||
|
counter += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_world_path: PathBuf = base_path.join(&unique_name);
|
||||||
|
|
||||||
|
// Create the new world directory structure
|
||||||
|
fs::create_dir_all(new_world_path.join("region"))
|
||||||
|
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
||||||
|
|
||||||
|
// Copy the region template file
|
||||||
|
const REGION_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/region.template");
|
||||||
|
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
||||||
|
fs::write(®ion_path, REGION_TEMPLATE)
|
||||||
|
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
||||||
|
|
||||||
|
// Add the level.dat file
|
||||||
|
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/level.dat");
|
||||||
|
|
||||||
|
// Decompress the gzipped level.template
|
||||||
|
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
||||||
|
let mut decompressed_data = Vec::new();
|
||||||
|
decoder
|
||||||
|
.read_to_end(&mut decompressed_data)
|
||||||
|
.map_err(|e| format!("Failed to decompress level.template: {e}"))?;
|
||||||
|
|
||||||
|
// Parse the decompressed NBT data
|
||||||
|
let mut level_data: Value = fastnbt::from_bytes(&decompressed_data)
|
||||||
|
.map_err(|e| format!("Failed to parse level.dat template: {e}"))?;
|
||||||
|
|
||||||
|
// Modify the LevelName, LastPlayed and player position fields
|
||||||
|
if let Value::Compound(ref mut root) = level_data {
|
||||||
|
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
||||||
|
// Update LevelName
|
||||||
|
data.insert("LevelName".to_string(), Value::String(unique_name.clone()));
|
||||||
|
|
||||||
|
// Update LastPlayed to the current Unix time in milliseconds
|
||||||
|
let current_time = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map_err(|e| format!("Failed to get current time: {e}"))?;
|
||||||
|
let current_time_millis = current_time.as_millis() as i64;
|
||||||
|
data.insert("LastPlayed".to_string(), Value::Long(current_time_millis));
|
||||||
|
|
||||||
|
// Update player position and rotation
|
||||||
|
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||||
|
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||||
|
if pos.len() < 3 {
|
||||||
|
return Err(
|
||||||
|
"Invalid level.dat template: Player Pos list has fewer than 3 elements"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Value::Double(ref mut x) = pos[0] {
|
||||||
|
*x = -5.0;
|
||||||
|
}
|
||||||
|
if let Value::Double(ref mut y) = pos[1] {
|
||||||
|
*y = -61.0;
|
||||||
|
}
|
||||||
|
if let Value::Double(ref mut z) = pos[2] {
|
||||||
|
*z = -5.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::List(ref mut rot)) = player.get_mut("Rotation") {
|
||||||
|
if rot.is_empty() {
|
||||||
|
return Err(
|
||||||
|
"Invalid level.dat template: Player Rotation list is empty".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Value::Float(ref mut x) = rot[0] {
|
||||||
|
*x = -45.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the updated NBT data back to bytes
|
||||||
|
let serialized_level_data: Vec<u8> = fastnbt::to_bytes(&level_data)
|
||||||
|
.map_err(|e| format!("Failed to serialize updated level.dat: {e}"))?;
|
||||||
|
|
||||||
|
// Compress the serialized data back to gzip
|
||||||
|
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||||
|
encoder
|
||||||
|
.write_all(&serialized_level_data)
|
||||||
|
.map_err(|e| format!("Failed to compress updated level.dat: {e}"))?;
|
||||||
|
let compressed_level_data = encoder
|
||||||
|
.finish()
|
||||||
|
.map_err(|e| format!("Failed to finalize compression for level.dat: {e}"))?;
|
||||||
|
|
||||||
|
// Write the level.dat file
|
||||||
|
fs::write(new_world_path.join("level.dat"), compressed_level_data)
|
||||||
|
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
||||||
|
|
||||||
|
// Add the icon.png file
|
||||||
|
const ICON_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/icon.png");
|
||||||
|
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
||||||
|
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
||||||
|
|
||||||
|
Ok(new_world_path.display().to_string())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user