mirror of
https://github.com/louis-e/arnis.git
synced 2026-02-19 07:26:53 -05:00
Compare commits
45 Commits
fix/depend
...
v2.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffbdd22fe9 | ||
|
|
1e6ab65b7a | ||
|
|
58cc17a174 | ||
|
|
d84fa1243d | ||
|
|
c2f6632c93 | ||
|
|
2ef79875cc | ||
|
|
21bd94c142 | ||
|
|
1d170d88bc | ||
|
|
b4af81c49a | ||
|
|
3ee0f27a53 | ||
|
|
8dcc8a3cb9 | ||
|
|
1d5d9116ae | ||
|
|
6f5b1229e0 | ||
|
|
f563d4dd26 | ||
|
|
e888163bd4 | ||
|
|
8b3eb516b8 | ||
|
|
5257a045b2 | ||
|
|
9582da2081 | ||
|
|
4787e07e05 | ||
|
|
09a6d2135c | ||
|
|
c369e9bae5 | ||
|
|
38adb1f589 | ||
|
|
1e87ff53ca | ||
|
|
6c67610bd0 | ||
|
|
cd7e3363e7 | ||
|
|
614da8da7c | ||
|
|
7d907208d1 | ||
|
|
f9c009c173 | ||
|
|
dfe799bac0 | ||
|
|
57a44500f4 | ||
|
|
8514a07fca | ||
|
|
ea9f11d427 | ||
|
|
882b18410e | ||
|
|
0c083b3b82 | ||
|
|
674591945f | ||
|
|
c5e5239062 | ||
|
|
e22a1b4f73 | ||
|
|
67a14a7f4b | ||
|
|
230d233737 | ||
|
|
b975ea19d7 | ||
|
|
e4939dc4bb | ||
|
|
11d624e734 | ||
|
|
d1d3bf22c5 | ||
|
|
32695555aa | ||
|
|
ee2356d734 |
109
Cargo.lock
generated
109
Cargo.lock
generated
@@ -177,7 +177,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arnis"
|
||||
version = "2.4.1"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bedrockrs_level",
|
||||
@@ -195,11 +195,12 @@ dependencies = [
|
||||
"image 0.25.9",
|
||||
"indicatif",
|
||||
"itertools 0.14.0",
|
||||
"jsonwebtoken 10.3.0",
|
||||
"log",
|
||||
"nbtx",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
@@ -214,7 +215,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"vek",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -563,7 +564,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bedrockrs_macros",
|
||||
"byteorder",
|
||||
"jsonwebtoken",
|
||||
"jsonwebtoken 9.3.1",
|
||||
"nbtx",
|
||||
"paste",
|
||||
"seq-macro",
|
||||
@@ -2924,6 +2925,22 @@ dependencies = [
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "10.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"getrandom 0.2.17",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"signature",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.7.0"
|
||||
@@ -5208,6 +5225,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
@@ -5511,7 +5537,7 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
@@ -5598,7 +5624,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5746,7 +5772,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5772,7 +5798,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"wry",
|
||||
]
|
||||
|
||||
@@ -6698,7 +6724,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
@@ -6722,7 +6748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
@@ -6784,11 +6810,23 @@ version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-collections 0.2.0",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-future 0.2.1",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections 0.3.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future 0.3.2",
|
||||
"windows-numerics 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6800,6 +6838,15 @@ dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
@@ -6834,7 +6881,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading",
|
||||
"windows-threading 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-threading 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6881,6 +6939,16 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
@@ -7030,6 +7098,15 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.7"
|
||||
@@ -7256,7 +7333,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.4.1"
|
||||
version = "2.5.0"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -23,7 +23,7 @@ tauri-build = {version = "2", optional = true}
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
dirs = "6.0.0"
|
||||
fastanvil = "0.32.0"
|
||||
@@ -35,10 +35,11 @@ geo = "0.31.0"
|
||||
image = "0.25"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
jsonwebtoken = "10.3.0"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
rand = { version = "0.9.1", features = ["std", "std_rng"] }
|
||||
rand_chacha = "0.9"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.13.1", features = ["blocking", "json", "query"] }
|
||||
rfd = { version = "0.16.0", optional = true }
|
||||
@@ -57,7 +58,7 @@ zip = { version = "0.6", default-features = false, features = ["deflate"], optio
|
||||
rusty-leveldb = { version = "3", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
windows = { version = "0.62.0", features = ["Win32_System_Console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.23.0"
|
||||
|
||||
@@ -39,6 +39,8 @@ GUI Build: ```cargo run```<br>
|
||||
|
||||
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
|
||||
|
||||
If you are using Nix, you can run the program directly with `nix run github:louis-e/arnis -- --terrain --path=YOUR_PATH/.minecraft/saves/worldname --bbox="min_lat,min_lng,max_lat,max_lng"`
|
||||
|
||||
## :star: Star History
|
||||
|
||||
<a href="https://star-history.com/#louis-e/arnis&Date">
|
||||
|
||||
34
flake.lock
generated
34
flake.lock
generated
@@ -1,23 +1,5 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1755615617,
|
||||
@@ -35,24 +17,8 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
70
flake.nix
70
flake.nix
@@ -1,36 +1,64 @@
|
||||
{
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
flake-utils,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
outputs = { self, nixpkgs }: {
|
||||
|
||||
stdenv = if pkgs.stdenv.isLinux then pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv else pkgs.stdenv;
|
||||
packages = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
lib = pkgs.lib;
|
||||
toml = lib.importTOML ./Cargo.toml;
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell.override { inherit stdenv; } {
|
||||
default = self.packages.${system}.arnis;
|
||||
arnis = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "arnis";
|
||||
version = toml.package.version;
|
||||
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"bedrockrs_core-0.1.0" = "sha256-0HP6p2x6sulZ2u8FzEfAiNAeyaUjQQWgGyK/kPo0PuQ=";
|
||||
"nbtx-0.1.0" = "sha256-JoNSL1vrUbxX6hKWB4i/DX02+hsQemANJhQaEELlT2o=";
|
||||
};
|
||||
};
|
||||
|
||||
# Checks use internet connection, so we disable them in nix sandboxed environment
|
||||
doCheck = false;
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
openssl.dev
|
||||
pkg-config
|
||||
wayland
|
||||
glib
|
||||
gdk-pixbuf
|
||||
pango
|
||||
gtk3
|
||||
libsoup_3.dev
|
||||
webkitgtk_4_1.dev
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
gtk3
|
||||
pango
|
||||
gdk-pixbuf
|
||||
glib
|
||||
wayland
|
||||
pkg-config
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Generate any location from the real world in Minecraft Java Edition with a high level of detail.";
|
||||
homepage = toml.package.homepage;
|
||||
license = lib.licenses.asl20;
|
||||
maintainers = [ ];
|
||||
mainProgram = "arnis";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
apps = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system: {
|
||||
default = self.apps.${system}.arnis;
|
||||
arnis = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.arnis}/bin/arnis";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub struct Args {
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub fillground: bool,
|
||||
|
||||
/// Enable city boundary ground generation (optional)
|
||||
/// Enable city 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)]
|
||||
|
||||
@@ -1039,7 +1039,7 @@ pub static INDUSTRIAL_WINDOW_OPTIONS: [Block; 4] = [
|
||||
// Window types for different building styles (non-deterministic, for backwards compatibility)
|
||||
pub fn get_window_block_for_building_type(building_type: &str) -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
get_window_block_for_building_type_with_rng(building_type, &mut rng)
|
||||
}
|
||||
|
||||
@@ -1050,18 +1050,18 @@ pub fn get_window_block_for_building_type_with_rng(
|
||||
) -> Block {
|
||||
match building_type {
|
||||
"residential" | "house" | "apartment" | "apartments" => {
|
||||
RESIDENTIAL_WINDOW_OPTIONS[rng.gen_range(0..RESIDENTIAL_WINDOW_OPTIONS.len())]
|
||||
RESIDENTIAL_WINDOW_OPTIONS[rng.random_range(0..RESIDENTIAL_WINDOW_OPTIONS.len())]
|
||||
}
|
||||
"hospital" | "school" | "university" => {
|
||||
INSTITUTIONAL_WINDOW_OPTIONS[rng.gen_range(0..INSTITUTIONAL_WINDOW_OPTIONS.len())]
|
||||
INSTITUTIONAL_WINDOW_OPTIONS[rng.random_range(0..INSTITUTIONAL_WINDOW_OPTIONS.len())]
|
||||
}
|
||||
"hotel" | "restaurant" => {
|
||||
HOSPITALITY_WINDOW_OPTIONS[rng.gen_range(0..HOSPITALITY_WINDOW_OPTIONS.len())]
|
||||
HOSPITALITY_WINDOW_OPTIONS[rng.random_range(0..HOSPITALITY_WINDOW_OPTIONS.len())]
|
||||
}
|
||||
"industrial" | "warehouse" => {
|
||||
INDUSTRIAL_WINDOW_OPTIONS[rng.gen_range(0..INDUSTRIAL_WINDOW_OPTIONS.len())]
|
||||
INDUSTRIAL_WINDOW_OPTIONS[rng.random_range(0..INDUSTRIAL_WINDOW_OPTIONS.len())]
|
||||
}
|
||||
_ => WINDOW_VARIATIONS[rng.gen_range(0..WINDOW_VARIATIONS.len())],
|
||||
_ => WINDOW_VARIATIONS[rng.random_range(0..WINDOW_VARIATIONS.len())],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,13 +1080,13 @@ pub static FLOOR_BLOCK_OPTIONS: [Block; 8] = [
|
||||
// Random floor block selection (non-deterministic, for backwards compatibility)
|
||||
pub fn get_random_floor_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
FLOOR_BLOCK_OPTIONS[rng.gen_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||
let mut rng = rand::rng();
|
||||
FLOOR_BLOCK_OPTIONS[rng.random_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||
}
|
||||
|
||||
/// Deterministic floor block selection using provided RNG
|
||||
pub fn get_floor_block_with_rng(rng: &mut impl rand::Rng) -> Block {
|
||||
FLOOR_BLOCK_OPTIONS[rng.gen_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||
FLOOR_BLOCK_OPTIONS[rng.random_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||
}
|
||||
|
||||
// Define all predefined colors with their blocks
|
||||
@@ -1221,7 +1221,7 @@ static DEFINED_COLORS: &[ColorBlockMapping] = &[
|
||||
// Function to randomly select building wall block with alternatives
|
||||
pub fn get_building_wall_block_for_color(color: RGBTuple) -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Find the closest color match
|
||||
let closest_color = DEFINED_COLORS
|
||||
@@ -1229,7 +1229,7 @@ pub fn get_building_wall_block_for_color(color: RGBTuple) -> Block {
|
||||
.min_by_key(|(defined_color, _)| crate::colors::rgb_distance(&color, defined_color));
|
||||
|
||||
if let Some((_, options)) = closest_color {
|
||||
options[rng.gen_range(0..options.len())]
|
||||
options[rng.random_range(0..options.len())]
|
||||
} else {
|
||||
// This should never happen, but fallback just in case
|
||||
get_fallback_building_block()
|
||||
@@ -1239,7 +1239,7 @@ pub fn get_building_wall_block_for_color(color: RGBTuple) -> Block {
|
||||
// Function to get a random fallback building block when no color attribute is specified
|
||||
pub fn get_fallback_building_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let fallback_options = [
|
||||
BLACKSTONE,
|
||||
@@ -1269,13 +1269,13 @@ pub fn get_fallback_building_block() -> Block {
|
||||
WHITE_CONCRETE,
|
||||
WHITE_TERRACOTTA,
|
||||
];
|
||||
fallback_options[rng.gen_range(0..fallback_options.len())]
|
||||
fallback_options[rng.random_range(0..fallback_options.len())]
|
||||
}
|
||||
|
||||
// Function to get a random castle wall block
|
||||
pub fn get_castle_wall_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let castle_wall_options = [
|
||||
STONE_BRICKS,
|
||||
@@ -1289,5 +1289,5 @@ pub fn get_castle_wall_block() -> Block {
|
||||
SMOOTH_STONE,
|
||||
BRICK,
|
||||
];
|
||||
castle_wall_options[rng.gen_range(0..castle_wall_options.len())]
|
||||
castle_wall_options[rng.random_range(0..castle_wall_options.len())]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::element_processing::*;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::ground::Ground;
|
||||
use crate::map_renderer;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole};
|
||||
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
@@ -14,6 +14,7 @@ use crate::urban_ground;
|
||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -89,6 +90,33 @@ pub fn generate_world_with_options(
|
||||
let mut current_progress_prcs: f64 = 25.0;
|
||||
let mut last_emitted_progress: f64 = current_progress_prcs;
|
||||
|
||||
// Pre-scan: detect building relation outlines that should be suppressed.
|
||||
// Only applies to type=building relations (NOT type=multipolygon).
|
||||
// When a type=building relation has "part" members, the outline way should not
|
||||
// render as a standalone building, the individual parts render instead.
|
||||
let suppressed_building_outlines: HashSet<u64> = {
|
||||
let mut outlines = HashSet::new();
|
||||
for element in &elements {
|
||||
if let ProcessedElement::Relation(rel) = element {
|
||||
let is_building_type = rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building_type {
|
||||
let has_parts = rel
|
||||
.members
|
||||
.iter()
|
||||
.any(|m| m.role == ProcessedMemberRole::Part);
|
||||
if has_parts {
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
outlines.insert(member.way.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outlines
|
||||
};
|
||||
|
||||
// Process all elements
|
||||
for element in elements.into_iter() {
|
||||
process_pb.inc(1);
|
||||
@@ -111,7 +139,18 @@ pub fn generate_world_with_options(
|
||||
match &element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
buildings::generate_buildings(&mut editor, way, args, None, &flood_fill_cache);
|
||||
// Skip building outlines that are suppressed by building relations with parts.
|
||||
// The individual building:part ways will render instead.
|
||||
if !suppressed_building_outlines.contains(&way.id) {
|
||||
buildings::generate_buildings(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
None,
|
||||
None,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
}
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(
|
||||
&mut editor,
|
||||
@@ -166,6 +205,8 @@ pub fn generate_world_with_options(
|
||||
highways::generate_aeroway(&mut editor, way, args);
|
||||
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
||||
highways::generate_siding(&mut editor, way);
|
||||
} else if way.tags.get("tomb") == Some(&"pyramid".to_string()) {
|
||||
historic::generate_pyramid(&mut editor, way, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(&mut editor, &element, args);
|
||||
} else if way.tags.contains_key("power") {
|
||||
@@ -216,12 +257,16 @@ pub fn generate_world_with_options(
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
let is_building_relation = rel.tags.contains_key("building")
|
||||
|| rel.tags.contains_key("building:part")
|
||||
|| rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building_relation {
|
||||
buildings::generate_building_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&xzbbox,
|
||||
);
|
||||
} else if rel.tags.contains_key("water")
|
||||
|| rel
|
||||
@@ -341,28 +386,24 @@ pub fn generate_world_with_options(
|
||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||
if is_urban {
|
||||
// Urban area: smooth stone ground
|
||||
editor.set_block_absolute(SMOOTH_STONE, x, ground_y, z, None, None);
|
||||
editor.set_block_if_absent_absolute(SMOOTH_STONE, x, ground_y, z);
|
||||
} else {
|
||||
// Rural/natural area: grass and dirt
|
||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||
editor.set_block_if_absent_absolute(GRASS_BLOCK, x, ground_y, z);
|
||||
}
|
||||
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_if_absent_absolute(DIRT, x, ground_y - 1, z);
|
||||
editor.set_block_if_absent_absolute(DIRT, x, ground_y - 2, z);
|
||||
}
|
||||
|
||||
// Fill underground with stone
|
||||
if args.fillground {
|
||||
// Fill from bedrock+1 to 3 blocks below ground with stone
|
||||
editor.fill_blocks_absolute(
|
||||
editor.fill_column_absolute(
|
||||
STONE,
|
||||
x,
|
||||
z,
|
||||
MIN_Y + 1,
|
||||
z,
|
||||
x,
|
||||
ground_y - 3,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
true, // skip_existing: don't overwrite blocks placed by element processing
|
||||
);
|
||||
}
|
||||
// Generate a bedrock level at MIN_Y
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! let mut rng = element_rng(element_id);
|
||||
//! let color = rng.gen_bool(0.5); // Always same result for same element_id
|
||||
//! let color = rng.random_bool(0.5); // Always same result for same element_id
|
||||
//! ```
|
||||
|
||||
use rand::SeedableRng;
|
||||
@@ -77,7 +77,7 @@ mod tests {
|
||||
|
||||
// Same seed should produce same sequence
|
||||
for _ in 0..100 {
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
assert_eq!(rng1.random::<u64>(), rng2.random::<u64>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ mod tests {
|
||||
let mut rng2 = element_rng(12346);
|
||||
|
||||
// Different seeds should (almost certainly) produce different values
|
||||
let v1: u64 = rng1.gen();
|
||||
let v2: u64 = rng2.gen();
|
||||
let v1: u64 = rng1.random();
|
||||
let v2: u64 = rng2.random();
|
||||
assert_ne!(v1, v2);
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ mod tests {
|
||||
let mut rng1 = element_rng(12345);
|
||||
let mut rng2 = element_rng_salted(12345, 1);
|
||||
|
||||
let v1: u64 = rng1.gen();
|
||||
let v2: u64 = rng2.gen();
|
||||
let v1: u64 = rng1.random();
|
||||
let v2: u64 = rng2.random();
|
||||
assert_ne!(v1, v2);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ mod tests {
|
||||
let mut rng1 = coord_rng(100, 200, 12345);
|
||||
let mut rng2 = coord_rng(100, 200, 12345);
|
||||
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
assert_eq!(rng1.random::<u64>(), rng2.random::<u64>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -116,12 +116,12 @@ mod tests {
|
||||
let mut rng1 = coord_rng(-100, -200, 12345);
|
||||
let mut rng2 = coord_rng(-100, -200, 12345);
|
||||
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
assert_eq!(rng1.random::<u64>(), rng2.random::<u64>());
|
||||
|
||||
// Ensure different negative coords produce different seeds
|
||||
let mut rng3 = coord_rng(-100, -200, 12345);
|
||||
let mut rng4 = coord_rng(-101, -200, 12345);
|
||||
|
||||
assert_ne!(rng3.gen::<u64>(), rng4.gen::<u64>());
|
||||
assert_ne!(rng3.random::<u64>(), rng4.random::<u64>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ fn generate_advertising_flag(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
ORANGE_WOOL,
|
||||
WHITE_WOOL,
|
||||
];
|
||||
let flag_block = flag_colors[rng.gen_range(0..flag_colors.len())];
|
||||
let flag_block = flag_colors[rng.random_range(0..flag_colors.len())];
|
||||
|
||||
// Flag extends to one side (2-3 blocks)
|
||||
let flag_length = 3;
|
||||
|
||||
@@ -8,7 +8,10 @@ use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use fastnbt::Value;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use rand::{
|
||||
prelude::{IndexedRandom, SliceRandom},
|
||||
Rng,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub fn generate_amenities(
|
||||
@@ -47,7 +50,7 @@ pub fn generate_amenities(
|
||||
}
|
||||
|
||||
if let Some(pt) = first_node {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
let loot_pool = build_recycling_loot_pool(element.tags());
|
||||
let items = build_recycling_items(&loot_pool, &mut rng);
|
||||
|
||||
@@ -132,7 +135,7 @@ pub fn generate_amenities(
|
||||
// Use deterministic RNG for consistent bench orientation across region boundaries
|
||||
let mut rng = element_rng(element.id());
|
||||
// 50% chance to 90 degrees rotate the bench
|
||||
if rng.gen_bool(0.5) {
|
||||
if rng.random_bool(0.5) {
|
||||
editor.set_block(SMOOTH_STONE, pt.x, 1, pt.z, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x + 1, 1, pt.z, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x - 1, 1, pt.z, None, None);
|
||||
@@ -389,8 +392,8 @@ fn build_recycling_items(
|
||||
|
||||
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 rng.random_bool(0.2) {
|
||||
let kind = loot_pool[rng.random_range(0..loot_pool.len())];
|
||||
if let Some(item) = build_item_for_kind(kind, slot as i8, rng) {
|
||||
items.push(item);
|
||||
}
|
||||
@@ -429,7 +432,10 @@ fn build_display_item_for_category(
|
||||
) -> 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::Paper => Some(make_display_item(
|
||||
"minecraft:paper",
|
||||
rng.random_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)),
|
||||
@@ -440,7 +446,7 @@ fn build_display_item_for_category(
|
||||
"minecraft:gold_ingot",
|
||||
];
|
||||
let metal = metals.choose(rng)?;
|
||||
Some(make_display_item(metal, rng.gen_range(1..=2)))
|
||||
Some(make_display_item(metal, rng.random_range(1..=2)))
|
||||
}
|
||||
LootCategory::GreenWaste => {
|
||||
let options = [
|
||||
@@ -451,7 +457,7 @@ fn build_display_item_for_category(
|
||||
"minecraft:wheat_seeds",
|
||||
];
|
||||
let choice = options.choose(rng)?;
|
||||
Some(make_display_item(choice, rng.gen_range(1..=3)))
|
||||
Some(make_display_item(choice, rng.random_range(1..=3)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +469,7 @@ fn place_item_frame_on_random_side(
|
||||
z: i32,
|
||||
item: HashMap<String, Value>,
|
||||
) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
let mut directions = [
|
||||
((0, 0, -1), 2), // North
|
||||
((0, 0, 1), 3), // South
|
||||
@@ -556,12 +562,12 @@ fn build_item_for_kind(
|
||||
RecyclingLootKind::GlassBottle => Some(make_basic_item(
|
||||
"minecraft:glass_bottle",
|
||||
slot,
|
||||
rng.gen_range(1..=4),
|
||||
rng.random_range(1..=4),
|
||||
)),
|
||||
RecyclingLootKind::Paper => Some(make_basic_item(
|
||||
"minecraft:paper",
|
||||
slot,
|
||||
rng.gen_range(1..=10),
|
||||
rng.random_range(1..=10),
|
||||
)),
|
||||
RecyclingLootKind::GlassBlock => Some(build_glass_item(false, slot, rng)),
|
||||
RecyclingLootKind::GlassPane => Some(build_glass_item(true, slot, rng)),
|
||||
@@ -578,26 +584,26 @@ fn build_item_for_kind(
|
||||
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);
|
||||
let count = rng.random_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)),
|
||||
let (id, count) = match rng.random_range(0..8) {
|
||||
0 => ("minecraft:tall_grass", rng.random_range(1..=4)),
|
||||
1 => ("minecraft:sweet_berries", rng.random_range(2..=6)),
|
||||
2 => ("minecraft:oak_sapling", rng.random_range(1..=2)),
|
||||
3 => ("minecraft:birch_sapling", rng.random_range(1..=2)),
|
||||
4 => ("minecraft:spruce_sapling", rng.random_range(1..=2)),
|
||||
5 => ("minecraft:jungle_sapling", rng.random_range(1..=2)),
|
||||
6 => ("minecraft:acacia_sapling", rng.random_range(1..=2)),
|
||||
_ => ("minecraft:dark_oak_sapling", rng.random_range(1..=2)),
|
||||
};
|
||||
|
||||
// 25% chance to replace with seeds instead
|
||||
let id = if rng.gen_bool(0.25) {
|
||||
match rng.gen_range(0..4) {
|
||||
let id = if rng.random_bool(0.25) {
|
||||
match rng.random_range(0..4) {
|
||||
0 => "minecraft:wheat_seeds",
|
||||
1 => "minecraft:pumpkin_seeds",
|
||||
2 => "minecraft:melon_seeds",
|
||||
@@ -630,7 +636,7 @@ fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<Stri
|
||||
"black",
|
||||
];
|
||||
|
||||
let use_colorless = rng.gen_bool(0.7);
|
||||
let use_colorless = rng.random_bool(0.7);
|
||||
|
||||
let id = if use_colorless {
|
||||
if is_pane {
|
||||
@@ -650,9 +656,9 @@ fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<Stri
|
||||
};
|
||||
|
||||
let count = if is_pane {
|
||||
rng.gen_range(4..=16)
|
||||
rng.random_range(4..=16)
|
||||
} else {
|
||||
rng.gen_range(1..=6)
|
||||
rng.random_range(1..=6)
|
||||
};
|
||||
|
||||
make_basic_item(&id, slot, count)
|
||||
@@ -692,21 +698,21 @@ fn biased_damage(max_damage: i32, rng: &mut impl Rng) -> i32 {
|
||||
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);
|
||||
let heavy_wear = rng.random_range(lower..=upper);
|
||||
let random_wear = rng.random_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))
|
||||
if rng.random_bool(0.3) {
|
||||
Some(rng.random_range(0..=0x00FF_FFFF))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn random_leather_piece(rng: &mut impl Rng) -> LeatherPiece {
|
||||
match rng.gen_range(0..4) {
|
||||
match rng.random_range(0..4) {
|
||||
0 => LeatherPiece::Helmet,
|
||||
1 => LeatherPiece::Chestplate,
|
||||
2 => LeatherPiece::Leggings,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::clipping::clip_way_to_bbox;
|
||||
use crate::colors::color_text_to_rgb_tuple;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::deterministic_rng::{coord_rng, element_rng};
|
||||
use crate::element_processing::historic;
|
||||
use crate::element_processing::subprocessor::buildings_interior::generate_building_interior;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use fastnbt::Value;
|
||||
use rand::Rng;
|
||||
@@ -24,6 +26,12 @@ pub(crate) enum RoofType {
|
||||
Flat, // Default flat roof
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct HolePolygon {
|
||||
way: ProcessedWay,
|
||||
add_walls: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Building Style System
|
||||
// ============================================================================
|
||||
@@ -703,7 +711,7 @@ impl BuildingStyle {
|
||||
) {
|
||||
const SKYSCRAPER_ROOF_CAP_OPTIONS: [Block; 3] =
|
||||
[POLISHED_ANDESITE, BLACKSTONE, NETHER_BRICK];
|
||||
SKYSCRAPER_ROOF_CAP_OPTIONS[rng.gen_range(0..SKYSCRAPER_ROOF_CAP_OPTIONS.len())]
|
||||
SKYSCRAPER_ROOF_CAP_OPTIONS[rng.random_range(0..SKYSCRAPER_ROOF_CAP_OPTIONS.len())]
|
||||
} else {
|
||||
get_floor_block_with_rng(rng)
|
||||
}
|
||||
@@ -720,7 +728,7 @@ impl BuildingStyle {
|
||||
let accent_block = preset.accent_block.unwrap_or_else(|| {
|
||||
if category == BuildingCategory::GlassySkyscraper {
|
||||
const GLASSY_ACCENT_OPTIONS: [Block; 2] = [WHITE_STAINED_GLASS, BLACKSTONE];
|
||||
GLASSY_ACCENT_OPTIONS[rng.gen_range(0..GLASSY_ACCENT_OPTIONS.len())]
|
||||
GLASSY_ACCENT_OPTIONS[rng.random_range(0..GLASSY_ACCENT_OPTIONS.len())]
|
||||
} else if category == BuildingCategory::ModernSkyscraper {
|
||||
const MODERN_ACCENT_OPTIONS: [Block; 5] = [
|
||||
POLISHED_ANDESITE,
|
||||
@@ -729,9 +737,9 @@ impl BuildingStyle {
|
||||
NETHER_BRICK,
|
||||
STONE_BRICKS,
|
||||
];
|
||||
MODERN_ACCENT_OPTIONS[rng.gen_range(0..MODERN_ACCENT_OPTIONS.len())]
|
||||
MODERN_ACCENT_OPTIONS[rng.random_range(0..MODERN_ACCENT_OPTIONS.len())]
|
||||
} else {
|
||||
ACCENT_BLOCK_OPTIONS[rng.gen_range(0..ACCENT_BLOCK_OPTIONS.len())]
|
||||
ACCENT_BLOCK_OPTIONS[rng.random_range(0..ACCENT_BLOCK_OPTIONS.len())]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -739,7 +747,7 @@ impl BuildingStyle {
|
||||
|
||||
let use_vertical_windows = preset
|
||||
.use_vertical_windows
|
||||
.unwrap_or_else(|| rng.gen_bool(0.7));
|
||||
.unwrap_or_else(|| rng.random_bool(0.7));
|
||||
|
||||
// Horizontal windows: full-width bands, used by modern skyscrapers
|
||||
let use_horizontal_windows = preset
|
||||
@@ -750,7 +758,7 @@ impl BuildingStyle {
|
||||
|
||||
let use_accent_roof_line = preset
|
||||
.use_accent_roof_line
|
||||
.unwrap_or_else(|| rng.gen_bool(0.25));
|
||||
.unwrap_or_else(|| rng.random_bool(0.25));
|
||||
|
||||
// Accent lines only for multi-floor buildings
|
||||
// Glassy skyscrapers get 60% chance, Modern skyscrapers always have them
|
||||
@@ -758,16 +766,16 @@ impl BuildingStyle {
|
||||
if category == BuildingCategory::ModernSkyscraper {
|
||||
true // Stone bands always present on modern skyscrapers
|
||||
} else if category == BuildingCategory::GlassySkyscraper {
|
||||
rng.gen_bool(0.6)
|
||||
rng.random_bool(0.6)
|
||||
} else {
|
||||
has_multiple_floors && rng.gen_bool(0.2)
|
||||
has_multiple_floors && rng.random_bool(0.2)
|
||||
}
|
||||
});
|
||||
|
||||
// Vertical accent: only if no accent lines and multi-floor
|
||||
let use_vertical_accent = preset
|
||||
.use_vertical_accent
|
||||
.unwrap_or_else(|| has_multiple_floors && !use_accent_lines && rng.gen_bool(0.1));
|
||||
.unwrap_or_else(|| has_multiple_floors && !use_accent_lines && rng.random_bool(0.1));
|
||||
|
||||
// === Roof ===
|
||||
|
||||
@@ -784,7 +792,7 @@ impl BuildingStyle {
|
||||
} else if qualifies_for_auto_gabled_roof(building_type) {
|
||||
// Auto-generate gabled roof for residential buildings
|
||||
const MAX_FOOTPRINT_FOR_GABLED: usize = 800;
|
||||
if footprint_size <= MAX_FOOTPRINT_FOR_GABLED && rng.gen_bool(0.9) {
|
||||
if footprint_size <= MAX_FOOTPRINT_FOR_GABLED && rng.random_bool(0.9) {
|
||||
(RoofType::Gabled, true)
|
||||
} else {
|
||||
(RoofType::Flat, false)
|
||||
@@ -811,7 +819,7 @@ impl BuildingStyle {
|
||||
let suitable_roof = matches!(roof_type, RoofType::Gabled | RoofType::Hipped);
|
||||
let suitable_size = (30..=400).contains(&footprint_size);
|
||||
|
||||
is_residential && suitable_roof && suitable_size && rng.gen_bool(0.55)
|
||||
is_residential && suitable_roof && suitable_size && rng.random_bool(0.55)
|
||||
});
|
||||
|
||||
// Roof block: specific material for roofs
|
||||
@@ -995,28 +1003,28 @@ fn determine_wall_block(
|
||||
fn get_wall_block_for_category(category: BuildingCategory, rng: &mut impl Rng) -> Block {
|
||||
match category {
|
||||
BuildingCategory::House | BuildingCategory::Residential => {
|
||||
RESIDENTIAL_WALL_OPTIONS[rng.gen_range(0..RESIDENTIAL_WALL_OPTIONS.len())]
|
||||
RESIDENTIAL_WALL_OPTIONS[rng.random_range(0..RESIDENTIAL_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Commercial | BuildingCategory::Office | BuildingCategory::Hotel => {
|
||||
COMMERCIAL_WALL_OPTIONS[rng.gen_range(0..COMMERCIAL_WALL_OPTIONS.len())]
|
||||
COMMERCIAL_WALL_OPTIONS[rng.random_range(0..COMMERCIAL_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Industrial | BuildingCategory::Warehouse => {
|
||||
INDUSTRIAL_WALL_OPTIONS[rng.gen_range(0..INDUSTRIAL_WALL_OPTIONS.len())]
|
||||
INDUSTRIAL_WALL_OPTIONS[rng.random_range(0..INDUSTRIAL_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Religious => {
|
||||
RELIGIOUS_WALL_OPTIONS[rng.gen_range(0..RELIGIOUS_WALL_OPTIONS.len())]
|
||||
RELIGIOUS_WALL_OPTIONS[rng.random_range(0..RELIGIOUS_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::School | BuildingCategory::Hospital => {
|
||||
INSTITUTIONAL_WALL_OPTIONS[rng.gen_range(0..INSTITUTIONAL_WALL_OPTIONS.len())]
|
||||
INSTITUTIONAL_WALL_OPTIONS[rng.random_range(0..INSTITUTIONAL_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Farm => FARM_WALL_OPTIONS[rng.gen_range(0..FARM_WALL_OPTIONS.len())],
|
||||
BuildingCategory::Farm => FARM_WALL_OPTIONS[rng.random_range(0..FARM_WALL_OPTIONS.len())],
|
||||
BuildingCategory::Historic => {
|
||||
HISTORIC_WALL_OPTIONS[rng.gen_range(0..HISTORIC_WALL_OPTIONS.len())]
|
||||
HISTORIC_WALL_OPTIONS[rng.random_range(0..HISTORIC_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Garage => {
|
||||
GARAGE_WALL_OPTIONS[rng.gen_range(0..GARAGE_WALL_OPTIONS.len())]
|
||||
GARAGE_WALL_OPTIONS[rng.random_range(0..GARAGE_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Shed => SHED_WALL_OPTIONS[rng.gen_range(0..SHED_WALL_OPTIONS.len())],
|
||||
BuildingCategory::Shed => SHED_WALL_OPTIONS[rng.random_range(0..SHED_WALL_OPTIONS.len())],
|
||||
BuildingCategory::Tower => {
|
||||
const TOWER_WALL_OPTIONS: [Block; 8] = [
|
||||
STONE_BRICKS,
|
||||
@@ -1028,14 +1036,14 @@ fn get_wall_block_for_category(category: BuildingCategory, rng: &mut impl Rng) -
|
||||
DEEPSLATE_BRICKS,
|
||||
SMOOTH_STONE,
|
||||
];
|
||||
TOWER_WALL_OPTIONS[rng.gen_range(0..TOWER_WALL_OPTIONS.len())]
|
||||
TOWER_WALL_OPTIONS[rng.random_range(0..TOWER_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Greenhouse => {
|
||||
GREENHOUSE_WALL_OPTIONS[rng.gen_range(0..GREENHOUSE_WALL_OPTIONS.len())]
|
||||
GREENHOUSE_WALL_OPTIONS[rng.random_range(0..GREENHOUSE_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::TallBuilding => {
|
||||
// Tall buildings use commercial palette (glass, concrete, stone)
|
||||
COMMERCIAL_WALL_OPTIONS[rng.gen_range(0..COMMERCIAL_WALL_OPTIONS.len())]
|
||||
COMMERCIAL_WALL_OPTIONS[rng.random_range(0..COMMERCIAL_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::ModernSkyscraper => {
|
||||
// Modern skyscrapers use clean concrete/stone wall materials
|
||||
@@ -1047,7 +1055,8 @@ fn get_wall_block_for_category(category: BuildingCategory, rng: &mut impl Rng) -
|
||||
SMOOTH_STONE,
|
||||
QUARTZ_BLOCK,
|
||||
];
|
||||
MODERN_SKYSCRAPER_WALL_OPTIONS[rng.gen_range(0..MODERN_SKYSCRAPER_WALL_OPTIONS.len())]
|
||||
MODERN_SKYSCRAPER_WALL_OPTIONS
|
||||
[rng.random_range(0..MODERN_SKYSCRAPER_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::GlassySkyscraper => {
|
||||
// Glass-facade skyscrapers use stained glass as wall material
|
||||
@@ -1057,7 +1066,7 @@ fn get_wall_block_for_category(category: BuildingCategory, rng: &mut impl Rng) -
|
||||
BLUE_STAINED_GLASS,
|
||||
LIGHT_BLUE_STAINED_GLASS,
|
||||
];
|
||||
GLASSY_WALL_OPTIONS[rng.gen_range(0..GLASSY_WALL_OPTIONS.len())]
|
||||
GLASSY_WALL_OPTIONS[rng.random_range(0..GLASSY_WALL_OPTIONS.len())]
|
||||
}
|
||||
BuildingCategory::Default => get_fallback_building_block(),
|
||||
}
|
||||
@@ -1251,30 +1260,180 @@ fn generate_roof_only_structure(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
cached_floor_area: &[(i32, i32)],
|
||||
args: &Args,
|
||||
) {
|
||||
let roof_height: i32 = 5;
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
let scale_factor = args.scale;
|
||||
let abs_terrain_offset = if !args.terrain { args.ground_level } else { 0 };
|
||||
|
||||
for node in &element.nodes {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
// Determine where the roof structure starts vertically.
|
||||
// Priority: min_height → building:min_level → layer hint → default.
|
||||
let min_level_offset = if let Some(mh) = element.tags.get("min_height") {
|
||||
// min_height is in meters; convert via scale factor.
|
||||
mh.trim_end_matches('m')
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.map(|h| (h * scale_factor) as i32)
|
||||
.unwrap_or(0)
|
||||
} else if let Some(ml) = element.tags.get("building:min_level") {
|
||||
ml.parse::<i32>()
|
||||
.ok()
|
||||
.map(|l| multiply_scale(l * 4, scale_factor))
|
||||
.unwrap_or(0)
|
||||
} else if let Some(layer) = element.tags.get("layer") {
|
||||
// For building:part=roof elements without explicit height tags, interpret
|
||||
// the layer tag as a coarse vertical-placement hint. Each layer maps to
|
||||
// 4 blocks, producing reasonable stacking for multi-shell roof structures.
|
||||
layer
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.filter(|&l| l > 0)
|
||||
.map(|l| multiply_scale(l * 4, scale_factor))
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if let Some(prev) = previous_node {
|
||||
let bresenham_points = bresenham_line(prev.0, roof_height, prev.1, x, roof_height, z);
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
editor.set_block(STONE_BRICK_SLAB, bx, roof_height, bz, None, None);
|
||||
let start_y_offset = calculate_start_y_offset(editor, element, args, min_level_offset);
|
||||
|
||||
// Determine roof thickness / height.
|
||||
let roof_thickness: i32 = if let Some(h) = element.tags.get("height") {
|
||||
let total = h
|
||||
.trim_end_matches('m')
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.map(|v| (v * scale_factor) as i32)
|
||||
.unwrap_or(5);
|
||||
// If we already applied a min_height offset, the thickness is just
|
||||
// the difference. Otherwise keep the parsed value.
|
||||
if element.tags.contains_key("min_height") {
|
||||
(total - min_level_offset).max(3)
|
||||
} else {
|
||||
total.max(3)
|
||||
}
|
||||
} else if let Some(levels) = element.tags.get("building:levels") {
|
||||
levels
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.map(|l| multiply_scale(l * 4 + 2, scale_factor).max(3))
|
||||
.unwrap_or(5)
|
||||
} else {
|
||||
5 // Default thickness for thin roof / canopy structures
|
||||
};
|
||||
|
||||
// Pick a block for the roof surface.
|
||||
let roof_block = if element
|
||||
.tags
|
||||
.get("material")
|
||||
.or_else(|| element.tags.get("roof:material"))
|
||||
.map(|s| s.as_str())
|
||||
== Some("glass")
|
||||
{
|
||||
GLASS
|
||||
} else if element.tags.get("colour").map(|s| s.as_str()) == Some("white")
|
||||
|| element.tags.get("building:colour").map(|s| s.as_str()) == Some("white")
|
||||
{
|
||||
SMOOTH_QUARTZ
|
||||
} else {
|
||||
STONE_BRICK_SLAB
|
||||
};
|
||||
|
||||
// Determine the roof shape from tags.
|
||||
let roof_type = element
|
||||
.tags
|
||||
.get("roof:shape")
|
||||
.map(|s| parse_roof_type(s))
|
||||
.unwrap_or(RoofType::Flat);
|
||||
|
||||
match roof_type {
|
||||
RoofType::Dome | RoofType::Hipped | RoofType::Pyramidal => {
|
||||
// Standalone roof parts with curved or sloped shapes are rendered
|
||||
// as domes. Without supporting walls, the dome approximation
|
||||
// produces the best visual result for shell-like roof structures.
|
||||
if !cached_floor_area.is_empty() {
|
||||
let (min_x, max_x, min_z, max_z) = cached_floor_area.iter().fold(
|
||||
(i32::MAX, i32::MIN, i32::MAX, i32::MIN),
|
||||
|(min_x, max_x, min_z, max_z), &(x, z)| {
|
||||
(min_x.min(x), max_x.max(x), min_z.min(z), max_z.max(z))
|
||||
},
|
||||
);
|
||||
// For roof-only structures, base_height is the elevation where
|
||||
// the dome starts (not on top of walls as in from_roof_area),
|
||||
// since there is no building body underneath.
|
||||
let config = RoofConfig {
|
||||
min_x,
|
||||
max_x,
|
||||
min_z,
|
||||
max_z,
|
||||
center_x: (min_x + max_x) >> 1,
|
||||
center_z: (min_z + max_z) >> 1,
|
||||
base_height: start_y_offset,
|
||||
abs_terrain_offset,
|
||||
roof_block,
|
||||
};
|
||||
generate_dome_roof(editor, cached_floor_area, &config);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Flat / unsupported shape: pillars at outline nodes + slab fill.
|
||||
let slab_y = start_y_offset + roof_thickness;
|
||||
|
||||
for y in 1..=(roof_height - 1) {
|
||||
editor.set_block(COBBLESTONE_WALL, x, y, z, None, None);
|
||||
// Outline pillars and edge slabs.
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
for node in &element.nodes {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
if let Some(prev) = previous_node {
|
||||
let pts = bresenham_line(prev.0, slab_y, prev.1, x, slab_y, z);
|
||||
for (bx, _, bz) in pts {
|
||||
editor.set_block_absolute(
|
||||
roof_block,
|
||||
bx,
|
||||
slab_y + abs_terrain_offset,
|
||||
bz,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the pillar base in the same coordinate system as
|
||||
// slab_y. When terrain is enabled, both values are absolute
|
||||
// world coordinates. When terrain is disabled, both are
|
||||
// relative to ground (abs_terrain_offset is added separately).
|
||||
let pillar_base = if args.terrain {
|
||||
editor.get_ground_level(x, z)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for y in (pillar_base + 1)..slab_y {
|
||||
editor.set_block_absolute(
|
||||
COBBLESTONE_WALL,
|
||||
x,
|
||||
y + abs_terrain_offset,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
previous_node = Some((x, z));
|
||||
}
|
||||
|
||||
// Slab fill across the floor area.
|
||||
for &(x, z) in cached_floor_area {
|
||||
editor.set_block_absolute(
|
||||
roof_block,
|
||||
x,
|
||||
slab_y + abs_terrain_offset,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
previous_node = Some((x, z));
|
||||
}
|
||||
|
||||
for &(x, z) in cached_floor_area {
|
||||
editor.set_block(STONE_BRICK_SLAB, x, roof_height, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1282,11 +1441,11 @@ fn generate_roof_only_structure(
|
||||
// Building Component Generators
|
||||
// ============================================================================
|
||||
|
||||
/// Generates the walls of a building including foundations, windows, and accent blocks
|
||||
/// Builds a wall ring (outer shell or inner courtyard) for a set of nodes.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn generate_building_walls(
|
||||
fn build_wall_ring(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
nodes: &[ProcessedNode],
|
||||
config: &BuildingConfig,
|
||||
args: &Args,
|
||||
has_sloped_roof: bool,
|
||||
@@ -1295,7 +1454,7 @@ fn generate_building_walls(
|
||||
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
|
||||
let mut current_building: Vec<(i32, i32)> = Vec::new();
|
||||
|
||||
for node in &element.nodes {
|
||||
for node in nodes {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
@@ -1468,7 +1627,7 @@ fn generate_special_doors(
|
||||
// Place a single oak door somewhere on the wall
|
||||
// Pick a random position from the wall outline
|
||||
if !wall_outline.is_empty() {
|
||||
let door_idx = rng.gen_range(0..wall_outline.len());
|
||||
let door_idx = rng.random_range(0..wall_outline.len());
|
||||
let (door_x, door_z) = wall_outline[door_idx];
|
||||
|
||||
// Place single oak door (empty blacklist to overwrite wall blocks)
|
||||
@@ -1619,8 +1778,9 @@ fn generate_residential_window_decorations(
|
||||
|
||||
// --- Per-building random material choices ---
|
||||
let mut rng = element_rng(element.id);
|
||||
let trapdoor_base = SHUTTER_TRAPDOOR_OPTIONS[rng.gen_range(0..SHUTTER_TRAPDOOR_OPTIONS.len())];
|
||||
let sill_base = SILL_SLAB_OPTIONS[rng.gen_range(0..SILL_SLAB_OPTIONS.len())];
|
||||
let trapdoor_base =
|
||||
SHUTTER_TRAPDOOR_OPTIONS[rng.random_range(0..SHUTTER_TRAPDOOR_OPTIONS.len())];
|
||||
let sill_base = SILL_SLAB_OPTIONS[rng.random_range(0..SILL_SLAB_OPTIONS.len())];
|
||||
let sill_block = make_top_slab(sill_base);
|
||||
|
||||
// We need the building centroid so we can figure out which side of
|
||||
@@ -1700,8 +1860,8 @@ fn generate_residential_window_decorations(
|
||||
if mod6 == 3 || mod6 == 5 {
|
||||
let centre_sum = if mod6 == 3 { bx + bz - 2 } else { bx + bz + 2 };
|
||||
let shutter_roll =
|
||||
coord_rng(centre_sum, centre_sum, element.id).gen_range(0u32..100);
|
||||
if shutter_roll < 12 {
|
||||
coord_rng(centre_sum, centre_sum, element.id).random_range(0u32..100);
|
||||
if shutter_roll < 25 {
|
||||
for h in (config.start_y_offset + 1)
|
||||
..=(config.start_y_offset + config.building_height)
|
||||
{
|
||||
@@ -1746,11 +1906,11 @@ fn generate_residential_window_decorations(
|
||||
centre_sum.wrapping_add(floor_idx * 5),
|
||||
element.id,
|
||||
)
|
||||
.gen_range(0u32..100);
|
||||
.random_range(0u32..100);
|
||||
|
||||
let abs_y = h + config.abs_terrain_offset;
|
||||
|
||||
if decoration_roll < 6 {
|
||||
if decoration_roll < 15 {
|
||||
// ── Window sill ──
|
||||
let lx = bx + out_nx;
|
||||
let lz = bz + out_nz;
|
||||
@@ -1767,13 +1927,13 @@ fn generate_residential_window_decorations(
|
||||
let mut pot_rng =
|
||||
coord_rng(bx, bz.wrapping_add(floor_idx), element.id);
|
||||
let pot_here = if mod6 == 1 {
|
||||
pot_rng.gen_range(0u32..100) < 70
|
||||
pot_rng.random_range(0u32..100) < 70
|
||||
} else {
|
||||
pot_rng.gen_range(0u32..100) < 25
|
||||
pot_rng.random_range(0u32..100) < 25
|
||||
};
|
||||
if pot_here {
|
||||
let plant = POTTED_PLANT_OPTIONS
|
||||
[pot_rng.gen_range(0..POTTED_PLANT_OPTIONS.len())];
|
||||
[pot_rng.random_range(0..POTTED_PLANT_OPTIONS.len())];
|
||||
editor.set_block_absolute(
|
||||
plant,
|
||||
lx,
|
||||
@@ -1783,7 +1943,7 @@ fn generate_residential_window_decorations(
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if decoration_roll < 10 && mod6 == 1 {
|
||||
} else if decoration_roll < 23 && mod6 == 1 {
|
||||
// ── Balcony (placed once from centre col) ──
|
||||
// A small 3-wide × 2-deep platform with
|
||||
// open-trapdoor railing around the outer
|
||||
@@ -1876,12 +2036,12 @@ fn generate_residential_window_decorations(
|
||||
bz.wrapping_add(floor_idx * 17),
|
||||
element.id,
|
||||
);
|
||||
let furniture_roll = furn_rng.gen_range(0u32..100);
|
||||
let furniture_roll = furn_rng.random_range(0u32..100);
|
||||
|
||||
if furniture_roll < 30 {
|
||||
// Cauldron "planter" with a leaf block
|
||||
// on top, placed at depth 1 on one side
|
||||
let side = if furn_rng.gen_bool(0.5) { -1i32 } else { 1 };
|
||||
let side = if furn_rng.random_bool(0.5) { -1i32 } else { 1 };
|
||||
let cx = bx + tan_x * side + out_nx;
|
||||
let cz = bz + tan_z * side + out_nz;
|
||||
editor.set_block_absolute(
|
||||
@@ -1902,7 +2062,7 @@ fn generate_residential_window_decorations(
|
||||
);
|
||||
} else if furniture_roll < 55 {
|
||||
// Stair "chair" facing outward
|
||||
let side = if furn_rng.gen_bool(0.5) { -1i32 } else { 1 };
|
||||
let side = if furn_rng.random_bool(0.5) { -1i32 } else { 1 };
|
||||
let sx = bx + tan_x * side + out_nx;
|
||||
let sz = bz + tan_z * side + out_nz;
|
||||
let stair_facing = match facing_for_normal(-out_nx, -out_nz) {
|
||||
@@ -2115,7 +2275,7 @@ fn parse_roof_type(roof_shape: &str) -> RoofType {
|
||||
"hipped" | "half-hipped" | "gambrel" | "mansard" | "round" => RoofType::Hipped,
|
||||
"skillion" => RoofType::Skillion,
|
||||
"pyramidal" => RoofType::Pyramidal,
|
||||
"dome" | "onion" | "cone" => RoofType::Dome,
|
||||
"dome" | "onion" | "cone" | "circular" | "spherical" => RoofType::Dome,
|
||||
_ => RoofType::Flat,
|
||||
}
|
||||
}
|
||||
@@ -2138,6 +2298,7 @@ pub fn generate_buildings(
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
relation_levels: Option<i32>,
|
||||
hole_polygons: Option<&[HolePolygon]>,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Early return for underground buildings
|
||||
@@ -2145,6 +2306,12 @@ pub fn generate_buildings(
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept tomb=pyramid: generate a sandstone pyramid instead of a building
|
||||
if element.tags.get("tomb").map(|v| v.as_str()) == Some("pyramid") {
|
||||
historic::generate_pyramid(editor, element, args, flood_fill_cache);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse min_level from tags
|
||||
let min_level = element
|
||||
.tags
|
||||
@@ -2157,9 +2324,43 @@ pub fn generate_buildings(
|
||||
let min_level_offset = multiply_scale(min_level * 4, scale_factor);
|
||||
|
||||
// Get cached floor area
|
||||
let cached_floor_area: Vec<(i32, i32)> =
|
||||
let mut cached_floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
if let Some(holes) = hole_polygons {
|
||||
if !holes.is_empty() {
|
||||
let outer_area: HashSet<(i32, i32)> = cached_floor_area.iter().copied().collect();
|
||||
let mut hole_points: HashSet<(i32, i32)> = HashSet::new();
|
||||
|
||||
for hole in holes {
|
||||
if hole.way.nodes.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hole_area = flood_fill_cache.get_or_compute(&hole.way, args.timeout.as_ref());
|
||||
if hole_area.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !hole_area.iter().any(|pt| outer_area.contains(pt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for point in hole_area {
|
||||
hole_points.insert(point);
|
||||
}
|
||||
}
|
||||
|
||||
if !hole_points.is_empty() {
|
||||
cached_floor_area.retain(|point| !hole_points.contains(point));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cached_footprint_size = cached_floor_area.len();
|
||||
if cached_footprint_size == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate start Y offset
|
||||
let start_y_offset = calculate_start_y_offset(editor, element, args, min_level_offset);
|
||||
@@ -2181,6 +2382,15 @@ pub fn generate_buildings(
|
||||
return;
|
||||
}
|
||||
|
||||
// Route building:part="roof" to the roof-only structure generator.
|
||||
// This must be checked before the "building" tag match below, since elements
|
||||
// with building:part="roof" (but no "building" tag) would otherwise fall
|
||||
// through to the full building pipeline and render as small boxy buildings.
|
||||
if element.tags.get("building:part").map(|v| v.as_str()) == Some("roof") {
|
||||
generate_roof_only_structure(editor, element, &cached_floor_area, args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle special building types with early returns
|
||||
if let Some(btype) = element.tags.get("building") {
|
||||
match btype.as_str() {
|
||||
@@ -2195,7 +2405,7 @@ pub fn generate_buildings(
|
||||
return;
|
||||
}
|
||||
"roof" => {
|
||||
generate_roof_only_structure(editor, element, &cached_floor_area);
|
||||
generate_roof_only_structure(editor, element, &cached_floor_area, args);
|
||||
return;
|
||||
}
|
||||
"bridge" => {
|
||||
@@ -2274,7 +2484,15 @@ pub fn generate_buildings(
|
||||
// Generate walls, pass whether this building will have a sloped roof
|
||||
let has_sloped_roof = args.roof && style.generate_roof && style.roof_type != RoofType::Flat;
|
||||
let (wall_outline, corner_addup) =
|
||||
generate_building_walls(editor, element, &config, args, has_sloped_roof);
|
||||
build_wall_ring(editor, &element.nodes, &config, args, has_sloped_roof);
|
||||
|
||||
if let Some(holes) = hole_polygons {
|
||||
for hole in holes {
|
||||
if hole.add_walls {
|
||||
let _ = build_wall_ring(editor, &hole.way.nodes, &config, args, has_sloped_roof);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate special doors (garage doors, shed doors)
|
||||
if config.has_garage_door || config.has_single_door {
|
||||
@@ -2457,7 +2675,7 @@ fn generate_chimney(
|
||||
let center_z = (min_z + max_z) / 2;
|
||||
|
||||
// Choose which quadrant to place the chimney (deterministically)
|
||||
let quadrant = rng.gen_range(0..4);
|
||||
let quadrant = rng.random_range(0..4);
|
||||
|
||||
// Filter floor area points to the chosen quadrant and find one that's
|
||||
// offset from the edge (so it's actually on the roof, not at the wall)
|
||||
@@ -2493,7 +2711,7 @@ fn generate_chimney(
|
||||
}
|
||||
|
||||
// Pick a point from candidates
|
||||
let (chimney_x, chimney_z) = final_candidates[rng.gen_range(0..final_candidates.len())];
|
||||
let (chimney_x, chimney_z) = final_candidates[rng.random_range(0..final_candidates.len())];
|
||||
|
||||
// Chimney starts 2 blocks below roof peak to replace roof blocks properly
|
||||
// Height is exactly 4 brick blocks with a slab cap on top
|
||||
@@ -2596,7 +2814,7 @@ fn generate_roof_terrace(
|
||||
for &(x, z) in &interior {
|
||||
// Deterministic per-position decision using coord_rng
|
||||
let mut rng = coord_rng(x, z, element.id);
|
||||
let roll: u32 = rng.gen_range(0..100);
|
||||
let roll: u32 = rng.random_range(0..100);
|
||||
|
||||
// ~85% of interior tiles are empty (open terrace space)
|
||||
if roll >= 15 {
|
||||
@@ -2621,7 +2839,7 @@ fn generate_roof_terrace(
|
||||
// Planter: leaf block on top of cauldron
|
||||
editor.set_block_absolute(CAULDRON, x, terrace_y, z, None, Some(replace_any));
|
||||
// Vary the leaf type
|
||||
let leaf = match rng.gen_range(0..3) {
|
||||
let leaf = match rng.random_range(0..3) {
|
||||
0 => OAK_LEAVES,
|
||||
1 => BIRCH_LEAVES,
|
||||
_ => SPRUCE_LEAVES,
|
||||
@@ -2775,7 +2993,7 @@ fn generate_rooftop_equipment(
|
||||
}
|
||||
|
||||
let mut rng = coord_rng(x, z, element.id);
|
||||
let roll: u32 = rng.gen_range(0..1200);
|
||||
let roll: u32 = rng.random_range(0..1200);
|
||||
|
||||
// ~99.4% of tiles are empty, very sparse
|
||||
if roll >= 7 {
|
||||
@@ -2937,8 +3155,8 @@ impl RoofConfig {
|
||||
// 90% wall block, 10% accent block for variety (deterministic based on element ID)
|
||||
let mut rng = element_rng(element_id);
|
||||
// Advance RNG state to get different value than other style choices
|
||||
let _ = rng.gen::<u32>();
|
||||
let roof_block = if rng.gen_bool(0.1) {
|
||||
let _ = rng.random::<u32>();
|
||||
let roof_block = if rng.random_bool(0.1) {
|
||||
accent_block
|
||||
} else {
|
||||
wall_block
|
||||
@@ -3758,6 +3976,7 @@ pub fn generate_building_from_relation(
|
||||
relation: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
xzbbox: &crate::coordinate_system::cartesian::XZBBox,
|
||||
) {
|
||||
// Skip underground buildings/building parts
|
||||
// Check layer tag
|
||||
@@ -3792,33 +4011,159 @@ pub fn generate_building_from_relation(
|
||||
.and_then(|l: &String| l.parse::<i32>().ok())
|
||||
.unwrap_or(2); // Default to 2 levels
|
||||
|
||||
// Process the outer way to create the building walls
|
||||
for member in &relation.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
// Check if this is a type=building relation with part members.
|
||||
// Only type=building relations use Part roles; type=multipolygon relations
|
||||
// should always render their Outer members normally.
|
||||
let is_building_type = relation.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
let has_parts = is_building_type
|
||||
&& relation
|
||||
.members
|
||||
.iter()
|
||||
.any(|m| m.role == ProcessedMemberRole::Part);
|
||||
|
||||
if !has_parts {
|
||||
// Collect outer member node lists and merge open segments into closed rings.
|
||||
// Multipolygon relations commonly split the outline across many short way
|
||||
// segments that share endpoints. Without merging, each segment is processed
|
||||
// individually, producing degenerate polygons and empty flood fills (only
|
||||
// wall outlines, no filled floors/ceilings/roofs).
|
||||
let mut outer_rings: Vec<Vec<ProcessedNode>> = relation
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.role == ProcessedMemberRole::Outer)
|
||||
.map(|m| m.way.nodes.clone())
|
||||
.collect();
|
||||
|
||||
super::merge_way_segments(&mut outer_rings);
|
||||
|
||||
// Clip assembled rings to the world bounding box. Because member ways
|
||||
// were kept unclipped during parsing (to allow ring assembly), the
|
||||
// merged rings may extend beyond the requested area. Clipping prevents
|
||||
// oversized flood fills and unnecessary block placement.
|
||||
outer_rings = outer_rings
|
||||
.into_iter()
|
||||
.map(|ring| clip_way_to_bbox(&ring, xzbbox))
|
||||
.filter(|ring| ring.len() >= 4)
|
||||
.collect();
|
||||
|
||||
// Close rings that are nearly closed (endpoints within 1 block)
|
||||
for ring in &mut outer_rings {
|
||||
if ring.len() >= 3 {
|
||||
let first = &ring[0];
|
||||
let last = ring.last().unwrap();
|
||||
if first.id != last.id {
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
if dx <= 1 && dz <= 1 {
|
||||
let close_node = ring[0].clone();
|
||||
ring.push(close_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discard rings that are still open or too small
|
||||
outer_rings.retain(|ring| {
|
||||
if ring.len() < 4 {
|
||||
return false;
|
||||
}
|
||||
let first = &ring[0];
|
||||
let last = ring.last().unwrap();
|
||||
first.id == last.id || ((first.x - last.x).abs() <= 1 && (first.z - last.z).abs() <= 1)
|
||||
});
|
||||
|
||||
// Collect and assemble inner rings for courtyards/holes.
|
||||
let mut inner_rings: Vec<Vec<ProcessedNode>> = relation
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.role == ProcessedMemberRole::Inner)
|
||||
.map(|m| m.way.nodes.clone())
|
||||
.collect();
|
||||
|
||||
super::merge_way_segments(&mut inner_rings);
|
||||
|
||||
inner_rings = inner_rings
|
||||
.into_iter()
|
||||
.map(|ring| clip_way_to_bbox(&ring, xzbbox))
|
||||
.filter(|ring| ring.len() >= 4)
|
||||
.collect();
|
||||
|
||||
// Close rings that are nearly closed (endpoints within 1 block)
|
||||
for ring in &mut inner_rings {
|
||||
if ring.len() >= 3 {
|
||||
let first = &ring[0];
|
||||
let last = ring.last().unwrap();
|
||||
if first.id != last.id {
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
if dx <= 1 && dz <= 1 {
|
||||
let close_node = ring[0].clone();
|
||||
ring.push(close_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discard rings that are still open or too small
|
||||
inner_rings.retain(|ring| {
|
||||
if ring.len() < 4 {
|
||||
return false;
|
||||
}
|
||||
let first = &ring[0];
|
||||
let last = ring.last().unwrap();
|
||||
first.id == last.id || ((first.x - last.x).abs() <= 1 && (first.z - last.z).abs() <= 1)
|
||||
});
|
||||
|
||||
let hole_polygons: Option<Vec<HolePolygon>> = if inner_rings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
inner_rings
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ring_idx, ring)| {
|
||||
// Use a different index range from outer rings to avoid cache collisions.
|
||||
let ring_slot = 0x8000u64 | (ring_idx as u64 & 0x7FFF);
|
||||
let synthetic_id = (1u64 << 63) | (relation.id << 16) | ring_slot;
|
||||
HolePolygon {
|
||||
way: ProcessedWay {
|
||||
id: synthetic_id,
|
||||
tags: HashMap::new(),
|
||||
nodes: ring,
|
||||
},
|
||||
add_walls: true,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
// Build a synthetic ProcessedWay for each assembled ring and render it.
|
||||
// The relation tags are applied so that building type, levels, and roof
|
||||
// shape from the relation are honoured.
|
||||
//
|
||||
// Synthetic IDs use bit 63 as a flag combined with the relation ID and a
|
||||
// ring index. This prevents collisions with real way IDs in the flood
|
||||
// fill cache and the deterministic RNG seeded by element ID.
|
||||
for (ring_idx, ring) in outer_rings.into_iter().enumerate() {
|
||||
let synthetic_id = (1u64 << 63) | (relation.id << 16) | (ring_idx as u64 & 0xFFFF);
|
||||
let merged_way = ProcessedWay {
|
||||
id: synthetic_id,
|
||||
tags: relation.tags.clone(),
|
||||
nodes: ring,
|
||||
};
|
||||
generate_buildings(
|
||||
editor,
|
||||
&member.way,
|
||||
&merged_way,
|
||||
args,
|
||||
Some(relation_levels),
|
||||
hole_polygons.as_deref(),
|
||||
flood_fill_cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle inner ways (holes, courtyards, etc.)
|
||||
/*for member in &relation.members {
|
||||
if member.role == ProcessedMemberRole::Inner {
|
||||
let polygon_coords: Vec<(i32, i32)> =
|
||||
member.way.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||
let hole_area: Vec<(i32, i32)> =
|
||||
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
|
||||
for (x, z) in hole_area {
|
||||
// Remove blocks in the inner area to create a hole
|
||||
editor.set_block(AIR, x, ground_level, z, None, Some(&[SPONGE]));
|
||||
}
|
||||
}
|
||||
}*/
|
||||
// When has_parts: parts are rendered as standalone ways from the elements list.
|
||||
// The outline way is suppressed in data_processing to avoid overlaying the parts.
|
||||
}
|
||||
|
||||
/// Generates a bridge structure, paying attention to the "level" tag.
|
||||
|
||||
@@ -166,17 +166,28 @@ fn generate_highways_internal(
|
||||
// Check if this is a bridge - bridges need special elevation handling
|
||||
// to span across valleys instead of following terrain
|
||||
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
|
||||
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
|
||||
// Indoor highways are never treated as bridges (indoor corridors should not
|
||||
// generate elevated decks or support pillars).
|
||||
let is_indoor = element.tags().get("indoor").is_some_and(|v| v == "yes");
|
||||
let is_bridge = !is_indoor && element.tags().get("bridge").is_some_and(|v| v != "no");
|
||||
|
||||
// Parse the layer value for elevation calculation
|
||||
let layer_value = element
|
||||
let mut layer_value = element
|
||||
.tags()
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Treat negative layers as ground level (0)
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
if layer_value < 0 {
|
||||
layer_value = 0;
|
||||
}
|
||||
|
||||
// If the way is indoor, treat it as ground level to avoid creating
|
||||
// bridges/supports inside buildings (indoor=yes should not produce bridges)
|
||||
if is_indoor {
|
||||
layer_value = 0;
|
||||
}
|
||||
|
||||
// Skip if 'level' is negative in the tags (indoor mapping)
|
||||
if let Some(level) = element.tags().get("level") {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
//! This module handles historic OSM elements including:
|
||||
//! - `historic=memorial` - Memorials, monuments, and commemorative structures
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedNode, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
@@ -73,7 +75,7 @@ fn generate_memorial(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
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) {
|
||||
let statue_block = if rng.random_bool(0.5) {
|
||||
POLISHED_ANDESITE
|
||||
} else {
|
||||
POLISHED_DIORITE
|
||||
@@ -205,3 +207,134 @@ fn generate_cross(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
|
||||
editor.set_block(STONE_BRICK_WALL, x + 1, arm_y, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pyramid Generation (tomb=pyramid)
|
||||
// ============================================================================
|
||||
|
||||
/// Generates a solid sandstone pyramid from a way outline.
|
||||
///
|
||||
/// The pyramid is built by flood-filling the footprint at ground level,
|
||||
/// then shrinking the filled area inward by one block per layer until
|
||||
/// only a single apex block (or nothing) remains.
|
||||
pub fn generate_pyramid(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
if element.nodes.len() < 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the footprint via flood fill
|
||||
let footprint: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
if footprint.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine base Y from terrain or ground level
|
||||
// Use the MINIMUM ground level so the pyramid sits on the lowest point
|
||||
// and doesn't float in areas with elevation differences
|
||||
let base_y = if args.terrain {
|
||||
footprint
|
||||
.iter()
|
||||
.map(|&(x, z)| editor.get_ground_level(x, z))
|
||||
.min()
|
||||
.unwrap_or(args.ground_level)
|
||||
} else {
|
||||
args.ground_level
|
||||
};
|
||||
|
||||
// Bounding box of the footprint
|
||||
let min_x = footprint.iter().map(|&(x, _)| x).min().unwrap();
|
||||
let max_x = footprint.iter().map(|&(x, _)| x).max().unwrap();
|
||||
let min_z = footprint.iter().map(|&(_, z)| z).min().unwrap();
|
||||
let max_z = footprint.iter().map(|&(_, z)| z).max().unwrap();
|
||||
|
||||
let center_x = (min_x + max_x) as f64 / 2.0;
|
||||
let center_z = (min_z + max_z) as f64 / 2.0;
|
||||
|
||||
// The pyramid height is half the shorter side of the bounding box (classic proportions)
|
||||
let width = (max_x - min_x + 1) as f64;
|
||||
let length = (max_z - min_z + 1) as f64;
|
||||
let half_base = width.min(length) / 2.0;
|
||||
// Height = half the shorter side (classic pyramid proportions).
|
||||
// Footprint is already in scaled Minecraft coordinates, so no extra scale factor needed.
|
||||
let pyramid_height = half_base.max(3.0) as i32;
|
||||
|
||||
// Build the pyramid layer by layer.
|
||||
// For each layer, only place blocks whose Chebyshev distance from the
|
||||
// footprint centre is within the shrinking radius AND that were in the
|
||||
// original footprint.
|
||||
let mut last_placed_layer: Option<i32> = None;
|
||||
for layer in 0..pyramid_height {
|
||||
// The allowed radius shrinks linearly from half_base at layer 0 to 0
|
||||
let radius = half_base * (1.0 - layer as f64 / pyramid_height as f64);
|
||||
if radius < 0.0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let y = base_y + 1 + layer;
|
||||
let mut placed = false;
|
||||
|
||||
for &(x, z) in &footprint {
|
||||
let dx = (x as f64 - center_x).abs();
|
||||
let dz = (z as f64 - center_z).abs();
|
||||
|
||||
// Use Chebyshev distance (max of dx, dz) for a square-footprint pyramid
|
||||
if dx <= radius && dz <= radius {
|
||||
// Allow overwriting common terrain blocks so the pyramid is
|
||||
// solid even when it intersects higher ground.
|
||||
editor.set_block_absolute(
|
||||
SANDSTONE,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
Some(&[
|
||||
GRASS_BLOCK,
|
||||
DIRT,
|
||||
STONE,
|
||||
SAND,
|
||||
GRAVEL,
|
||||
COARSE_DIRT,
|
||||
PODZOL,
|
||||
DIRT_PATH,
|
||||
SANDSTONE,
|
||||
]),
|
||||
None,
|
||||
);
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if placed {
|
||||
last_placed_layer = Some(y);
|
||||
} else {
|
||||
break; // Nothing placed, we've reached the apex
|
||||
}
|
||||
}
|
||||
|
||||
// Cap with smooth sandstone one block above the last placed layer
|
||||
if let Some(top_y) = last_placed_layer {
|
||||
editor.set_block_absolute(
|
||||
SMOOTH_SANDSTONE,
|
||||
center_x.round() as i32,
|
||||
top_y + 1,
|
||||
center_z.round() as i32,
|
||||
Some(&[
|
||||
GRASS_BLOCK,
|
||||
DIRT,
|
||||
STONE,
|
||||
SAND,
|
||||
GRAVEL,
|
||||
COARSE_DIRT,
|
||||
PODZOL,
|
||||
DIRT_PATH,
|
||||
SANDSTONE,
|
||||
]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::{Tree, TreeType};
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_landuse(
|
||||
@@ -86,7 +87,7 @@ pub fn generate_landuse(
|
||||
// Apply per-block randomness for certain landuse types
|
||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
|
||||
let random_value = rng.gen_range(0..100);
|
||||
let random_value = rng.random_range(0..100);
|
||||
if random_value < 72 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 87 {
|
||||
@@ -98,7 +99,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
} else if landuse_tag == "commercial" {
|
||||
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
|
||||
let random_value = rng.gen_range(0..100);
|
||||
let random_value = rng.random_range(0..100);
|
||||
if random_value < 40 {
|
||||
SMOOTH_STONE
|
||||
} else if random_value < 70 {
|
||||
@@ -110,7 +111,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
} else if landuse_tag == "industrial" {
|
||||
// Industrial: primarily stone, with some stone bricks and smooth stone
|
||||
let random_value = rng.gen_range(0..100);
|
||||
let random_value = rng.random_range(0..100);
|
||||
if random_value < 70 {
|
||||
STONE
|
||||
} else if random_value < 90 {
|
||||
@@ -134,11 +135,11 @@ pub fn generate_landuse(
|
||||
match landuse_tag.as_str() {
|
||||
"cemetery" => {
|
||||
if (x % 3 == 0) && (z % 3 == 0) {
|
||||
let random_choice: i32 = rng.gen_range(0..100);
|
||||
let random_choice: i32 = rng.random_range(0..100);
|
||||
if random_choice < 15 {
|
||||
// Place graves
|
||||
if editor.check_for_block(x, 0, z, Some(&[PODZOL])) {
|
||||
if rng.gen_bool(0.5) {
|
||||
if rng.random_bool(0.5) {
|
||||
editor.set_block(COBBLESTONE, x - 1, 1, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x - 1, 2, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 1, z, None, None);
|
||||
@@ -168,7 +169,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
"forest" => {
|
||||
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.random_range(0..30);
|
||||
if random_choice == 20 {
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
@@ -180,7 +181,7 @@ pub fn generate_landuse(
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if random_choice == 2 {
|
||||
let flower_block: Block = match rng.gen_range(1..=6) {
|
||||
let flower_block: Block = match rng.random_range(1..=6) {
|
||||
1 => OAK_LEAVES,
|
||||
2 => RED_FLOWER,
|
||||
3 => BLUE_FLOWER,
|
||||
@@ -190,7 +191,7 @@ pub fn generate_landuse(
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if random_choice <= 12 {
|
||||
if rng.gen_range(0..100) < 12 {
|
||||
if rng.random_range(0..100) < 12 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
@@ -204,8 +205,8 @@ pub fn generate_landuse(
|
||||
if x % 9 == 0 && z % 9 == 0 {
|
||||
// Place water in dot pattern
|
||||
editor.set_block(WATER, x, 0, z, Some(&[FARMLAND]), None);
|
||||
} else if rng.gen_range(0..76) == 0 {
|
||||
let special_choice: i32 = rng.gen_range(1..=10);
|
||||
} else if rng.random_range(0..76) == 0 {
|
||||
let special_choice: i32 = rng.random_range(1..=10);
|
||||
if special_choice <= 4 {
|
||||
editor.set_block(HAY_BALE, x, 1, z, None, Some(&[SPONGE]));
|
||||
} else {
|
||||
@@ -214,14 +215,14 @@ pub fn generate_landuse(
|
||||
} else {
|
||||
// Set crops only if the block below is farmland
|
||||
if editor.check_for_block(x, 0, z, Some(&[FARMLAND])) {
|
||||
let crop_choice = [WHEAT, CARROTS, POTATOES][rng.gen_range(0..3)];
|
||||
let crop_choice = [WHEAT, CARROTS, POTATOES][rng.random_range(0..3)];
|
||||
editor.set_block(crop_choice, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"construction" => {
|
||||
let random_choice: i32 = rng.gen_range(0..1501);
|
||||
let random_choice: i32 = rng.random_range(0..1501);
|
||||
if random_choice < 15 {
|
||||
editor.set_block(SCAFFOLDING, x, 1, z, None, None);
|
||||
if random_choice < 2 {
|
||||
@@ -257,7 +258,7 @@ pub fn generate_landuse(
|
||||
FURNACE,
|
||||
];
|
||||
editor.set_block(
|
||||
construction_items[rng.gen_range(0..construction_items.len())],
|
||||
construction_items[rng.random_range(0..construction_items.len())],
|
||||
x,
|
||||
1,
|
||||
z,
|
||||
@@ -294,7 +295,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
"grass" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..200) {
|
||||
match rng.random_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=8 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
9..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
@@ -304,7 +305,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
"greenfield" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..200) {
|
||||
match rng.random_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
3..=16 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
@@ -314,7 +315,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
"meadow" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..1001);
|
||||
let random_choice: i32 = rng.random_range(0..1001);
|
||||
if random_choice < 5 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice < 6 {
|
||||
@@ -335,7 +336,7 @@ pub fn generate_landuse(
|
||||
if x % 18 == 0 && z % 10 == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..100) {
|
||||
match rng.random_range(0..100) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
3..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
@@ -357,7 +358,8 @@ pub fn generate_landuse(
|
||||
"clay" | "kaolinite" => CLAY,
|
||||
_ => STONE,
|
||||
};
|
||||
let random_choice: i32 = rng.gen_range(0..100 + editor.get_absolute_y(x, 0, z)); // The deeper it is the more resources are there
|
||||
let random_choice: i32 =
|
||||
rng.random_range(0..100 + editor.get_absolute_y(x, 0, z)); // The deeper it is the more resources are there
|
||||
if random_choice < 5 {
|
||||
editor.set_block(ore_block, x, 0, z, Some(&[STONE]), None);
|
||||
}
|
||||
@@ -366,6 +368,26 @@ pub fn generate_landuse(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a stone brick wall fence around cemeteries
|
||||
if landuse_tag == "cemetery" {
|
||||
generate_cemetery_fence(editor, element);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a stone-brick wall fence (with slab cap) along the outline of a
|
||||
/// cemetery way.
|
||||
fn generate_cemetery_fence(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
for i in 1..element.nodes.len() {
|
||||
let prev = &element.nodes[i - 1];
|
||||
let cur = &element.nodes[i];
|
||||
|
||||
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
|
||||
for (bx, _, bz) in points {
|
||||
editor.set_block(STONE_BRICK_WALL, bx, 1, bz, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, bx, 2, bz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_landuse_from_relation(
|
||||
|
||||
@@ -96,7 +96,7 @@ pub fn generate_leisure(
|
||||
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
|
||||
&& editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK]))
|
||||
{
|
||||
let random_choice: i32 = rng.gen_range(0..1000);
|
||||
let random_choice: i32 = rng.random_range(0..1000);
|
||||
|
||||
match random_choice {
|
||||
0..30 => {
|
||||
@@ -129,7 +129,7 @@ pub fn generate_leisure(
|
||||
|
||||
// Add playground or recreation ground features
|
||||
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
|
||||
let random_choice: i32 = rng.gen_range(0..5000);
|
||||
let random_choice: i32 = rng.random_range(0..5000);
|
||||
|
||||
match random_choice {
|
||||
0..10 => {
|
||||
|
||||
@@ -6,8 +6,7 @@ use crate::element_processing::tree::{Tree, TreeType};
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::Rng;
|
||||
use rand::{prelude::IndexedRandom, Rng};
|
||||
|
||||
pub fn generate_natural(
|
||||
editor: &mut WorldEditor,
|
||||
@@ -193,7 +192,7 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
if rng.gen_bool(0.6) {
|
||||
if rng.random_bool(0.6) {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
@@ -201,7 +200,7 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
let random_choice = rng.random_range(0..500);
|
||||
if random_choice < 33 {
|
||||
if random_choice <= 2 {
|
||||
editor.set_block(COBBLESTONE, x, 0, z, None, None);
|
||||
@@ -216,11 +215,11 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
let random_choice = rng.random_range(0..500);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
let flower_block = match rng.random_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
@@ -245,7 +244,7 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
let random_choice: i32 = rng.random_range(0..30);
|
||||
if random_choice == 0 {
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
@@ -257,7 +256,7 @@ pub fn generate_natural(
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
let flower_block = match rng.random_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
@@ -270,13 +269,13 @@ pub fn generate_natural(
|
||||
}
|
||||
"sand" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[SAND]))
|
||||
&& rng.gen_range(0..100) == 1
|
||||
&& rng.random_range(0..100) == 1
|
||||
{
|
||||
editor.set_block(DEAD_BUSH, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"shoal" => {
|
||||
if rng.gen_bool(0.05) {
|
||||
if rng.random_bool(0.05) {
|
||||
editor.set_block(WATER, x, 0, z, Some(&[SAND, GRAVEL]), None);
|
||||
}
|
||||
}
|
||||
@@ -284,14 +283,14 @@ pub fn generate_natural(
|
||||
if let Some(wetland_type) = element.tags().get("wetland") {
|
||||
// Wetland without water blocks
|
||||
if matches!(wetland_type.as_str(), "wet_meadow" | "fen") {
|
||||
if rng.gen_bool(0.3) {
|
||||
if rng.random_bool(0.3) {
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, Some(&[MUD]), None);
|
||||
}
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
continue;
|
||||
}
|
||||
// All the other types of wetland
|
||||
if rng.gen_bool(0.3) {
|
||||
if rng.random_bool(0.3) {
|
||||
editor.set_block(
|
||||
WATER,
|
||||
x,
|
||||
@@ -312,7 +311,7 @@ pub fn generate_natural(
|
||||
}
|
||||
"swamp" | "mangrove" => {
|
||||
// TODO implement mangrove
|
||||
let random_choice: i32 = rng.gen_range(0..40);
|
||||
let random_choice: i32 = rng.random_range(0..40);
|
||||
if random_choice == 0 {
|
||||
Tree::create(
|
||||
editor,
|
||||
@@ -324,7 +323,7 @@ pub fn generate_natural(
|
||||
}
|
||||
}
|
||||
"bog" => {
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
editor.set_block(
|
||||
MOSS_BLOCK,
|
||||
x,
|
||||
@@ -334,7 +333,7 @@ pub fn generate_natural(
|
||||
None,
|
||||
);
|
||||
}
|
||||
if rng.gen_bool(0.15) {
|
||||
if rng.random_bool(0.15) {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
@@ -347,7 +346,7 @@ pub fn generate_natural(
|
||||
}
|
||||
} else {
|
||||
// Generic natural=wetland without wetland=... tag
|
||||
if rng.gen_bool(0.3) {
|
||||
if rng.random_bool(0.3) {
|
||||
editor.set_block(WATER, x, 0, z, Some(&[MUD]), None);
|
||||
continue;
|
||||
}
|
||||
@@ -356,11 +355,11 @@ pub fn generate_natural(
|
||||
}
|
||||
"mountain_range" => {
|
||||
// Create block clusters instead of random placement
|
||||
let cluster_chance = rng.gen_range(0..1000);
|
||||
let cluster_chance = rng.random_range(0..1000);
|
||||
|
||||
if cluster_chance < 50 {
|
||||
// 5% chance to start a new cluster
|
||||
let cluster_block = match rng.gen_range(0..7) {
|
||||
let cluster_block = match rng.random_range(0..7) {
|
||||
0 => DIRT,
|
||||
1 => STONE,
|
||||
2 => GRAVEL,
|
||||
@@ -371,7 +370,7 @@ pub fn generate_natural(
|
||||
};
|
||||
|
||||
// Generate cluster size (5-10 blocks radius)
|
||||
let cluster_size = rng.gen_range(5..=10);
|
||||
let cluster_size = rng.random_range(5..=10);
|
||||
|
||||
// Create cluster around current position
|
||||
for dx in -(cluster_size as i32)..=(cluster_size as i32) {
|
||||
@@ -384,7 +383,7 @@ pub fn generate_natural(
|
||||
if distance <= cluster_size as f32 {
|
||||
// Probability decreases with distance from center
|
||||
let place_prob = 1.0 - (distance / cluster_size as f32);
|
||||
if rng.gen::<f32>() < place_prob {
|
||||
if rng.random::<f32>() < place_prob {
|
||||
editor.set_block(
|
||||
cluster_block,
|
||||
cluster_x,
|
||||
@@ -396,7 +395,8 @@ pub fn generate_natural(
|
||||
|
||||
// Add vegetation on grass blocks
|
||||
if cluster_block == GRASS_BLOCK {
|
||||
let vegetation_chance = rng.gen_range(0..100);
|
||||
let vegetation_chance =
|
||||
rng.random_range(0..100);
|
||||
if vegetation_chance == 0 {
|
||||
// 1% chance for rare trees
|
||||
Tree::create(
|
||||
@@ -426,7 +426,7 @@ pub fn generate_natural(
|
||||
}
|
||||
"saddle" => {
|
||||
// Saddle areas - lowest point between peaks, mix of stone and grass
|
||||
let terrain_chance = rng.gen_range(0..100);
|
||||
let terrain_chance = rng.random_range(0..100);
|
||||
if terrain_chance < 30 {
|
||||
// 30% chance for exposed stone
|
||||
editor.set_block(STONE, x, 0, z, None, None);
|
||||
@@ -436,7 +436,7 @@ pub fn generate_natural(
|
||||
} else {
|
||||
// 50% chance for grass
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, None, None);
|
||||
if rng.gen_bool(0.4) {
|
||||
if rng.random_bool(0.4) {
|
||||
// 40% chance for grass on top
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
@@ -444,10 +444,10 @@ pub fn generate_natural(
|
||||
}
|
||||
"ridge" => {
|
||||
// Ridge areas - elevated crest, mostly rocky with some vegetation
|
||||
let ridge_chance = rng.gen_range(0..100);
|
||||
let ridge_chance = rng.random_range(0..100);
|
||||
if ridge_chance < 60 {
|
||||
// 60% chance for stone/rocky terrain
|
||||
let rock_type = match rng.gen_range(0..4) {
|
||||
let rock_type = match rng.random_range(0..4) {
|
||||
0 => STONE,
|
||||
1 => COBBLESTONE,
|
||||
2 => GRANITE,
|
||||
@@ -457,7 +457,7 @@ pub fn generate_natural(
|
||||
} else {
|
||||
// 40% chance for grass with sparse vegetation
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, None, None);
|
||||
let vegetation_chance = rng.gen_range(0..100);
|
||||
let vegetation_chance = rng.random_range(0..100);
|
||||
if vegetation_chance < 20 {
|
||||
// 20% chance for grass
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
@@ -477,7 +477,7 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let tundra_chance = rng.gen_range(0..100);
|
||||
let tundra_chance = rng.random_range(0..100);
|
||||
if tundra_chance < 40 {
|
||||
// 40% chance for grass (sedges, grasses)
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
@@ -492,10 +492,10 @@ pub fn generate_natural(
|
||||
}
|
||||
"cliff" => {
|
||||
// Cliff areas - predominantly stone with minimal vegetation
|
||||
let cliff_chance = rng.gen_range(0..100);
|
||||
let cliff_chance = rng.random_range(0..100);
|
||||
if cliff_chance < 90 {
|
||||
// 90% chance for stone variants
|
||||
let stone_type = match rng.gen_range(0..4) {
|
||||
let stone_type = match rng.random_range(0..4) {
|
||||
0 => STONE,
|
||||
1 => COBBLESTONE,
|
||||
2 => ANDESITE,
|
||||
@@ -512,13 +512,13 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let hill_chance = rng.gen_range(0..1000);
|
||||
let hill_chance = rng.random_range(0..1000);
|
||||
if hill_chance == 0 {
|
||||
// 0.1% chance for rare trees
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if hill_chance < 50 {
|
||||
// 5% chance for flowers
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
let flower_block = match rng.random_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
|
||||
@@ -156,7 +156,7 @@ impl Tree<'_> {
|
||||
// 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) {
|
||||
let tree_type = match rng.random_range(1..=10) {
|
||||
1..=3 => TreeType::Oak,
|
||||
4..=5 => TreeType::Spruce,
|
||||
6..=7 => TreeType::Birch,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||
|
||||
use crate::clipping::clip_water_ring_to_bbox;
|
||||
use crate::{
|
||||
block_definitions::WATER,
|
||||
@@ -54,6 +51,7 @@ pub fn generate_water_areas_from_relation(
|
||||
match mem.role {
|
||||
ProcessedMemberRole::Outer => outers.push(mem.way.nodes.clone()),
|
||||
ProcessedMemberRole::Inner => inners.push(mem.way.nodes.clone()),
|
||||
ProcessedMemberRole::Part => {} // Not applicable to water areas
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +161,7 @@ fn generate_water_areas(
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
|
||||
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
|
||||
scanline_fill_water(min_x, min_z, max_x, max_z, &outers_xz, &inners_xz, editor);
|
||||
}
|
||||
|
||||
/// Verifies all rings are properly closed (first node matches last).
|
||||
@@ -189,156 +187,248 @@ fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
||||
valid
|
||||
}
|
||||
|
||||
// Water areas are absolutely huge. We can't easily flood fill the entire thing.
|
||||
// Instead, we'll iterate over all the blocks in our MC world, and check if each
|
||||
// one is in the river or not
|
||||
// ============================================================================
|
||||
// Scanline rasterization for water area filling
|
||||
// ============================================================================
|
||||
//
|
||||
// For each row (z coordinate) in the fill area, computes polygon edge
|
||||
// crossings to determine which x-ranges are inside the outer polygons but
|
||||
// outside the inner polygons, then fills those ranges with water blocks.
|
||||
//
|
||||
// Complexity: O(E * H + A) where E = total edges, H = height of fill area,
|
||||
// A = total filled area. This is dramatically faster than the previous
|
||||
// quadtree + per-block point-in-polygon approach O(A * V * P) for large or
|
||||
// complex water bodies (e.g. the Venetian Lagoon with dozens of inner island
|
||||
// rings).
|
||||
|
||||
/// A polygon edge segment for scanline intersection testing.
|
||||
struct ScanlineEdge {
|
||||
x1: f64,
|
||||
z1: f64,
|
||||
x2: f64,
|
||||
z2: f64,
|
||||
}
|
||||
|
||||
/// Collects all non-horizontal edges from a single polygon ring.
|
||||
///
|
||||
/// If the ring is not perfectly closed (last point != first point),
|
||||
/// the closing edge is added explicitly.
|
||||
fn collect_ring_edges(ring: &[XZPoint]) -> Vec<ScanlineEdge> {
|
||||
let mut edges = Vec::new();
|
||||
if ring.len() < 2 {
|
||||
return edges;
|
||||
}
|
||||
for i in 0..ring.len() - 1 {
|
||||
let a = &ring[i];
|
||||
let b = &ring[i + 1];
|
||||
// Skip horizontal edges, they produce no scanline crossings
|
||||
if a.z != b.z {
|
||||
edges.push(ScanlineEdge {
|
||||
x1: a.x as f64,
|
||||
z1: a.z as f64,
|
||||
x2: b.x as f64,
|
||||
z2: b.z as f64,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Add closing edge if the ring isn't perfectly closed by coordinates
|
||||
let first = ring.first().unwrap();
|
||||
let last = ring.last().unwrap();
|
||||
if first.z != last.z {
|
||||
edges.push(ScanlineEdge {
|
||||
x1: last.x as f64,
|
||||
z1: last.z as f64,
|
||||
x2: first.x as f64,
|
||||
z2: first.z as f64,
|
||||
});
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Collects edges from multiple rings into a single list.
|
||||
/// Used for inner rings where even-odd on combined edges is correct
|
||||
/// (inner rings of a valid multipolygon do not overlap).
|
||||
fn collect_all_ring_edges(rings: &[Vec<XZPoint>]) -> Vec<ScanlineEdge> {
|
||||
let mut edges = Vec::new();
|
||||
for ring in rings {
|
||||
edges.extend(collect_ring_edges(ring));
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Computes the integer x-spans that are "inside" the polygon rings at
|
||||
/// scanline `z`, using the even-odd (parity) rule.
|
||||
///
|
||||
/// The crossing test uses the same convention as `geo::Contains`:
|
||||
/// an edge crosses the scanline when one endpoint is strictly above `z`
|
||||
/// and the other is at or below.
|
||||
fn compute_scanline_spans(
|
||||
edges: &[ScanlineEdge],
|
||||
z: f64,
|
||||
min_x: i32,
|
||||
max_x: i32,
|
||||
) -> Vec<(i32, i32)> {
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for edge in edges {
|
||||
// Crossing test: (z1 > z) != (z2 > z)
|
||||
// Matches geo's convention (bottom-inclusive, top-exclusive).
|
||||
if (edge.z1 > z) != (edge.z2 > z) {
|
||||
let t = (z - edge.z1) / (edge.z2 - edge.z1);
|
||||
xs.push(edge.x1 + t * (edge.x2 - edge.x1));
|
||||
}
|
||||
}
|
||||
|
||||
if xs.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
xs.sort_unstable_by(|a, b| {
|
||||
a.partial_cmp(b)
|
||||
.expect("NaN encountered while sorting scanline intersections")
|
||||
});
|
||||
|
||||
debug_assert!(
|
||||
xs.len().is_multiple_of(2),
|
||||
"Odd number of scanline crossings ({}) at z={}, possible malformed polygon",
|
||||
xs.len(),
|
||||
z
|
||||
);
|
||||
|
||||
// Pair consecutive crossings into fill spans (even-odd rule)
|
||||
let mut spans = Vec::with_capacity(xs.len() / 2);
|
||||
let mut i = 0;
|
||||
while i + 1 < xs.len() {
|
||||
let start = (xs[i].ceil() as i32).max(min_x);
|
||||
let end = (xs[i + 1].floor() as i32).min(max_x);
|
||||
if start <= end {
|
||||
spans.push((start, end));
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
|
||||
spans
|
||||
}
|
||||
|
||||
/// Merges two sorted, non-overlapping span lists into their union.
|
||||
fn union_spans(a: &[(i32, i32)], b: &[(i32, i32)]) -> Vec<(i32, i32)> {
|
||||
if a.is_empty() {
|
||||
return b.to_vec();
|
||||
}
|
||||
if b.is_empty() {
|
||||
return a.to_vec();
|
||||
}
|
||||
|
||||
// Merge both sorted lists and combine overlapping/adjacent spans
|
||||
let mut all: Vec<(i32, i32)> = Vec::with_capacity(a.len() + b.len());
|
||||
all.extend_from_slice(a);
|
||||
all.extend_from_slice(b);
|
||||
all.sort_unstable_by_key(|&(start, _)| start);
|
||||
|
||||
let mut result: Vec<(i32, i32)> = Vec::new();
|
||||
let mut current = all[0];
|
||||
for &(start, end) in &all[1..] {
|
||||
if start <= current.1 + 1 {
|
||||
// Overlapping or adjacent, extend
|
||||
current.1 = current.1.max(end);
|
||||
} else {
|
||||
result.push(current);
|
||||
current = (start, end);
|
||||
}
|
||||
}
|
||||
result.push(current);
|
||||
result
|
||||
}
|
||||
|
||||
/// Subtracts spans in `b` from spans in `a`.
|
||||
///
|
||||
/// Both inputs must be sorted and non-overlapping.
|
||||
/// Returns sorted, non-overlapping spans representing `a \ b`.
|
||||
fn subtract_spans(a: &[(i32, i32)], b: &[(i32, i32)]) -> Vec<(i32, i32)> {
|
||||
if b.is_empty() {
|
||||
return a.to_vec();
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut bi = 0;
|
||||
|
||||
for &(a_start, a_end) in a {
|
||||
let mut pos = a_start;
|
||||
|
||||
// Skip B spans that end before this A span starts
|
||||
while bi < b.len() && b[bi].1 < a_start {
|
||||
bi += 1;
|
||||
}
|
||||
|
||||
// Walk through B spans that overlap with [pos .. a_end]
|
||||
let mut j = bi;
|
||||
while j < b.len() && b[j].0 <= a_end {
|
||||
if b[j].0 > pos {
|
||||
result.push((pos, (b[j].0 - 1).min(a_end)));
|
||||
}
|
||||
pos = pos.max(b[j].1 + 1);
|
||||
j += 1;
|
||||
}
|
||||
|
||||
if pos <= a_end {
|
||||
result.push((pos, a_end));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Fills water blocks using scanline rasterization.
|
||||
///
|
||||
/// For each row z in [min_z, max_z], computes which x positions are inside
|
||||
/// any outer polygon ring but outside all inner polygon rings, and places
|
||||
/// water blocks at those positions.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn inverse_floodfill(
|
||||
fn scanline_fill_water(
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
max_x: i32,
|
||||
max_z: i32,
|
||||
outers: Vec<Vec<XZPoint>>,
|
||||
inners: Vec<Vec<XZPoint>>,
|
||||
outers: &[Vec<XZPoint>],
|
||||
inners: &[Vec<XZPoint>],
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
// Convert to geo Polygons with normalized winding order
|
||||
let inners: Vec<_> = inners
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
Polygon::new(
|
||||
LineString::from(
|
||||
x.iter()
|
||||
.map(|pt| (pt.x as f64, pt.z as f64))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
// Collect edges per outer ring so we can union their spans correctly,
|
||||
// even if multiple outer rings happen to overlap (invalid OSM, but
|
||||
// we handle it gracefully).
|
||||
let outer_edge_groups: Vec<Vec<ScanlineEdge>> =
|
||||
outers.iter().map(|ring| collect_ring_edges(ring)).collect();
|
||||
let inner_edges = collect_all_ring_edges(inners);
|
||||
|
||||
let outers: Vec<_> = outers
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
Polygon::new(
|
||||
LineString::from(
|
||||
x.iter()
|
||||
.map(|pt| (pt.x as f64, pt.z as f64))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
for z in min_z..=max_z {
|
||||
let z_f = z as f64;
|
||||
|
||||
inverse_floodfill_recursive((min_x, min_z), (max_x, max_z), &outers, &inners, editor);
|
||||
}
|
||||
|
||||
fn inverse_floodfill_recursive(
|
||||
min: (i32, i32),
|
||||
max: (i32, i32),
|
||||
outers: &[Polygon],
|
||||
inners: &[Polygon],
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
// Check if we've exceeded 40 seconds
|
||||
// if start_time.elapsed().as_secs() > 40 {
|
||||
// println!("Water area generation exceeded 40 seconds, continuing anyway");
|
||||
// }
|
||||
|
||||
const ITERATIVE_THRES: i64 = 10_000;
|
||||
|
||||
if min.0 > max.0 || min.1 > max.1 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiply as i64 to avoid overflow; in release builds where unchecked math is
|
||||
// enabled, this could cause the rest of this code to end up in an infinite loop.
|
||||
if ((max.0 - min.0) as i64) * ((max.1 - min.1) as i64) < ITERATIVE_THRES {
|
||||
inverse_floodfill_iterative(min, max, 0, outers, inners, editor);
|
||||
return;
|
||||
}
|
||||
|
||||
let center_x: i32 = (min.0 + max.0) / 2;
|
||||
let center_z: i32 = (min.1 + max.1) / 2;
|
||||
let quadrants: [(i32, i32, i32, i32); 4] = [
|
||||
(min.0, center_x, min.1, center_z),
|
||||
(center_x, max.0, min.1, center_z),
|
||||
(min.0, center_x, center_z, max.1),
|
||||
(center_x, max.0, center_z, max.1),
|
||||
];
|
||||
|
||||
for (min_x, max_x, min_z, max_z) in quadrants {
|
||||
let rect: Rect = Rect::new(
|
||||
Point::new(min_x as f64, min_z as f64),
|
||||
Point::new(max_x as f64, max_z as f64),
|
||||
);
|
||||
|
||||
if outers.iter().any(|outer: &Polygon| outer.contains(&rect))
|
||||
&& !inners.iter().any(|inner: &Polygon| inner.intersects(&rect))
|
||||
{
|
||||
rect_fill(min_x, max_x, min_z, max_z, 0, editor);
|
||||
// Compute spans for each outer ring and union them together
|
||||
let mut outer_spans: Vec<(i32, i32)> = Vec::new();
|
||||
for ring_edges in &outer_edge_groups {
|
||||
let ring_spans = compute_scanline_spans(ring_edges, z_f, min_x, max_x);
|
||||
if !ring_spans.is_empty() {
|
||||
outer_spans = union_spans(&outer_spans, &ring_spans);
|
||||
}
|
||||
}
|
||||
if outer_spans.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let outers_intersects: Vec<_> = outers
|
||||
.iter()
|
||||
.filter(|poly| poly.intersects(&rect))
|
||||
.cloned()
|
||||
.collect();
|
||||
let inners_intersects: Vec<_> = inners
|
||||
.iter()
|
||||
.filter(|poly| poly.intersects(&rect))
|
||||
.cloned()
|
||||
.collect();
|
||||
let fill_spans = if inner_edges.is_empty() {
|
||||
outer_spans
|
||||
} else {
|
||||
let inner_spans = compute_scanline_spans(&inner_edges, z_f, min_x, max_x);
|
||||
if inner_spans.is_empty() {
|
||||
outer_spans
|
||||
} else {
|
||||
subtract_spans(&outer_spans, &inner_spans)
|
||||
}
|
||||
};
|
||||
|
||||
if !outers_intersects.is_empty() {
|
||||
inverse_floodfill_recursive(
|
||||
(min_x, min_z),
|
||||
(max_x, max_z),
|
||||
&outers_intersects,
|
||||
&inners_intersects,
|
||||
editor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// once we "zoom in" enough, it's more efficient to switch to iteration
|
||||
fn inverse_floodfill_iterative(
|
||||
min: (i32, i32),
|
||||
max: (i32, i32),
|
||||
ground_level: i32,
|
||||
outers: &[Polygon],
|
||||
inners: &[Polygon],
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
for x in min.0..max.0 {
|
||||
for z in min.1..max.1 {
|
||||
let p: Point = Point::new(x as f64, z as f64);
|
||||
|
||||
if outers.iter().any(|poly: &Polygon| poly.contains(&p))
|
||||
&& inners.iter().all(|poly: &Polygon| !poly.contains(&p))
|
||||
{
|
||||
editor.set_block(WATER, x, ground_level, z, None, None);
|
||||
for (start, end) in fill_spans {
|
||||
for x in start..=end {
|
||||
editor.set_block(WATER, x, 0, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_fill(
|
||||
min_x: i32,
|
||||
max_x: i32,
|
||||
min_z: i32,
|
||||
max_z: i32,
|
||||
ground_level: i32,
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
for x in min_x..max_x {
|
||||
for z in min_z..max_z {
|
||||
editor.set_block(WATER, x, ground_level, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,11 +326,6 @@ pub fn fetch_elevation_data(
|
||||
Ok(tile_data) => successful_tiles.push(tile_data),
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to download tile: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
&format!("Failed to download elevation tile: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
src/floodfill.rs
115
src/floodfill.rs
@@ -1,13 +1,64 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, LineString, Point, Polygon};
|
||||
use itertools::Itertools;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::collections::VecDeque;
|
||||
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.
|
||||
/// Polygons exceeding this are skipped to prevent excessive memory allocations.
|
||||
/// 25 million blocks ≈ 5000×5000; bitmap uses only ~3 MB at this size.
|
||||
const MAX_FLOOD_FILL_AREA: i64 = 25_000_000;
|
||||
|
||||
/// A compact bitmap for visited-coordinate tracking during flood fill.
|
||||
///
|
||||
/// Uses 1 bit per coordinate instead of ~48 bytes per entry in a `HashSet`.
|
||||
/// For a 5000×5000 bounding box this is ~3 MB instead of ~1.2 GB.
|
||||
struct FloodBitmap {
|
||||
bits: Vec<u8>,
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
width: usize,
|
||||
}
|
||||
|
||||
impl FloodBitmap {
|
||||
#[inline]
|
||||
fn new(min_x: i32, max_x: i32, min_z: i32, max_z: i32) -> Self {
|
||||
let width = (max_x - min_x + 1) as usize;
|
||||
let height = (max_z - min_z + 1) as usize;
|
||||
let num_bytes = (width * height).div_ceil(8);
|
||||
Self {
|
||||
bits: vec![0u8; num_bytes],
|
||||
min_x,
|
||||
min_z,
|
||||
width,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark (x, z) as visited. Returns `true` if it was NOT already visited
|
||||
/// (i.e. this is the first visit).
|
||||
#[inline]
|
||||
fn insert(&mut self, x: i32, z: i32) -> bool {
|
||||
let idx = (z - self.min_z) as usize * self.width + (x - self.min_x) as usize;
|
||||
let byte = idx / 8;
|
||||
let bit = idx % 8;
|
||||
let mask = 1u8 << bit;
|
||||
if self.bits[byte] & mask != 0 {
|
||||
false // already visited
|
||||
} else {
|
||||
self.bits[byte] |= mask;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn contains(&self, x: i32, z: i32) -> bool {
|
||||
let idx = (z - self.min_z) as usize * self.width + (x - self.min_x) as usize;
|
||||
let byte = idx / 8;
|
||||
let bit = idx % 8;
|
||||
(self.bits[byte] >> bit) & 1 == 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Main flood fill function with automatic algorithm selection
|
||||
/// Chooses the best algorithm based on polygon size and complexity
|
||||
pub fn flood_fill_area(
|
||||
@@ -62,15 +113,16 @@ fn optimized_flood_fill_area(
|
||||
let start_time = Instant::now();
|
||||
|
||||
let mut filled_area = Vec::new();
|
||||
let mut global_visited = HashSet::new();
|
||||
let mut visited = FloodBitmap::new(min_x, max_x, min_z, max_z);
|
||||
|
||||
// Create polygon for containment testing
|
||||
// Create polygon for containment testing, with normalized winding order
|
||||
// to avoid "polygon had no winding order" warnings from geo::Contains
|
||||
let exterior_coords: Vec<(f64, f64)> = polygon_coords
|
||||
.iter()
|
||||
.map(|&(x, z)| (x as f64, z as f64))
|
||||
.collect();
|
||||
let exterior = LineString::from(exterior_coords);
|
||||
let polygon = Polygon::new(exterior, vec![]);
|
||||
let polygon = Polygon::new(exterior, vec![]).orient(Direction::Default);
|
||||
|
||||
// Optimized step sizes: larger steps for efficiency, but still catch U-shapes
|
||||
let width = max_x - min_x + 1;
|
||||
@@ -93,16 +145,14 @@ fn optimized_flood_fill_area(
|
||||
}
|
||||
|
||||
// Skip if already visited or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
{
|
||||
if visited.contains(x, z) || !polygon.contains(&Point::new(x as f64, z as f64)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start flood fill from this seed point
|
||||
queue.clear(); // Reuse queue instead of creating new one
|
||||
queue.push_back((x, z));
|
||||
global_visited.insert((x, z));
|
||||
visited.insert(x, z);
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Add current point to filled area
|
||||
@@ -116,17 +166,16 @@ fn optimized_flood_fill_area(
|
||||
(curr_x, curr_z + 1),
|
||||
];
|
||||
|
||||
for (nx, nz) in neighbors.iter() {
|
||||
if *nx >= min_x
|
||||
&& *nx <= max_x
|
||||
&& *nz >= min_z
|
||||
&& *nz <= max_z
|
||||
&& !global_visited.contains(&(*nx, *nz))
|
||||
for &(nx, nz) in &neighbors {
|
||||
if nx >= min_x
|
||||
&& nx <= max_x
|
||||
&& nz >= min_z
|
||||
&& nz <= max_z
|
||||
&& visited.insert(nx, nz)
|
||||
{
|
||||
// Only check polygon containment for unvisited points
|
||||
if polygon.contains(&Point::new(*nx as f64, *nz as f64)) {
|
||||
global_visited.insert((*nx, *nz));
|
||||
queue.push_back((*nx, *nz));
|
||||
if polygon.contains(&Point::new(nx as f64, nz as f64)) {
|
||||
queue.push_back((nx, nz));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,15 +197,16 @@ fn original_flood_fill_area(
|
||||
) -> Vec<(i32, i32)> {
|
||||
let start_time = Instant::now();
|
||||
let mut filled_area: Vec<(i32, i32)> = Vec::new();
|
||||
let mut global_visited: HashSet<(i32, i32)> = HashSet::new();
|
||||
let mut visited = FloodBitmap::new(min_x, max_x, min_z, max_z);
|
||||
|
||||
// Convert input to a geo::Polygon for efficient point-in-polygon testing
|
||||
// Convert input to a geo::Polygon for efficient point-in-polygon testing,
|
||||
// with normalized winding order to avoid undefined Contains results
|
||||
let exterior_coords: Vec<(f64, f64)> = polygon_coords
|
||||
.iter()
|
||||
.map(|&(x, z)| (x as f64, z as f64))
|
||||
.collect::<Vec<_>>();
|
||||
let exterior: LineString = LineString::from(exterior_coords);
|
||||
let polygon: Polygon<f64> = Polygon::new(exterior, vec![]);
|
||||
let polygon: Polygon<f64> = Polygon::new(exterior, vec![]).orient(Direction::Default);
|
||||
|
||||
// Optimized step sizes for large polygons - coarser sampling for speed
|
||||
let width = max_x - min_x + 1;
|
||||
@@ -180,16 +230,14 @@ fn original_flood_fill_area(
|
||||
}
|
||||
|
||||
// Skip if already processed or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
{
|
||||
if visited.contains(x, z) || !polygon.contains(&Point::new(x as f64, z as f64)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start flood-fill from this seed point
|
||||
queue.clear(); // Reuse queue
|
||||
queue.push_back((x, z));
|
||||
global_visited.insert((x, z));
|
||||
visited.insert(x, z);
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Only check polygon containment once per point when adding to filled_area
|
||||
@@ -204,15 +252,14 @@ fn original_flood_fill_area(
|
||||
(curr_x, curr_z + 1),
|
||||
];
|
||||
|
||||
for (nx, nz) in neighbors.iter() {
|
||||
if *nx >= min_x
|
||||
&& *nx <= max_x
|
||||
&& *nz >= min_z
|
||||
&& *nz <= max_z
|
||||
&& !global_visited.contains(&(*nx, *nz))
|
||||
for &(nx, nz) in &neighbors {
|
||||
if nx >= min_x
|
||||
&& nx <= max_x
|
||||
&& nz >= min_z
|
||||
&& nz <= max_z
|
||||
&& visited.insert(nx, nz)
|
||||
{
|
||||
global_visited.insert((*nx, *nz));
|
||||
queue.push_back((*nx, *nz));
|
||||
queue.push_back((nx, nz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +330,6 @@ impl FloodFillCache {
|
||||
fn way_needs_flood_fill(way: &ProcessedWay) -> bool {
|
||||
way.tags.contains_key("building")
|
||||
|| way.tags.contains_key("building:part")
|
||||
|| way.tags.contains_key("boundary")
|
||||
|| way.tags.contains_key("landuse")
|
||||
|| way.tags.contains_key("leisure")
|
||||
|| way.tags.contains_key("amenity")
|
||||
@@ -342,6 +341,8 @@ impl FloodFillCache {
|
||||
// Highway areas (like pedestrian plazas) use flood fill when area=yes
|
||||
|| (way.tags.contains_key("highway")
|
||||
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false))
|
||||
// Historic tomb polygons (e.g. tomb=pyramid)
|
||||
|| way.tags.get("tomb").map(|v| v == "pyramid").unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Collects all building footprint coordinates from the pre-computed cache.
|
||||
@@ -370,7 +371,10 @@ impl FloodFillCache {
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
let is_building = rel.tags.contains_key("building")
|
||||
|| rel.tags.contains_key("building:part")
|
||||
|| rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building {
|
||||
for member in &rel.members {
|
||||
// Only treat outer members as building footprints.
|
||||
// Inner members represent courtyards/holes where trees can spawn.
|
||||
@@ -413,7 +417,10 @@ impl FloodFillCache {
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
let is_building = rel.tags.contains_key("building")
|
||||
|| rel.tags.contains_key("building:part")
|
||||
|| rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building {
|
||||
// For building relations, compute centroid from outer ways
|
||||
let mut all_coords = Vec::new();
|
||||
for member in &rel.members {
|
||||
|
||||
2
src/gui/index.html
vendored
2
src/gui/index.html
vendored
@@ -182,7 +182,7 @@
|
||||
<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>
|
||||
<span class="tooltip-icon" data-tooltip="Directory where new Minecraft Java 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">
|
||||
|
||||
@@ -111,6 +111,7 @@ pub struct ProcessedWay {
|
||||
pub enum ProcessedMemberRole {
|
||||
Outer,
|
||||
Inner,
|
||||
Part,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -272,17 +273,24 @@ pub fn parse_osm_data(
|
||||
continue;
|
||||
};
|
||||
|
||||
// Process multipolygons and boundary relations
|
||||
// Process multipolygons and building relations
|
||||
let relation_type = tags.get("type").map(|x: &String| x.as_str());
|
||||
if relation_type != Some("multipolygon") && relation_type != Some("boundary") {
|
||||
if relation_type != Some("multipolygon") && relation_type != Some("building") {
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_building_relation = relation_type == Some("building")
|
||||
|| tags.contains_key("building")
|
||||
|| tags.contains_key("building:part");
|
||||
|
||||
// Water relations require unclipped ways for ring merging in water_areas.rs
|
||||
// Boundary relations also require unclipped ways for proper ring assembly
|
||||
// Building multipolygon relations also need unclipped ways so that
|
||||
// open outer-way segments can be merged into closed rings before clipping
|
||||
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 is_building_multipolygon = (tags.contains_key("building")
|
||||
|| tags.contains_key("building:part"))
|
||||
&& relation_type == Some("multipolygon");
|
||||
let keep_unclipped = is_water_relation || is_building_multipolygon;
|
||||
|
||||
let members: Vec<ProcessedMember> = element
|
||||
.members
|
||||
@@ -293,10 +301,25 @@ pub fn parse_osm_data(
|
||||
return None;
|
||||
}
|
||||
|
||||
let role = match mem.role.as_str() {
|
||||
"outer" => ProcessedMemberRole::Outer,
|
||||
"inner" => ProcessedMemberRole::Inner,
|
||||
_ => return None,
|
||||
let trimmed_role = mem.role.trim();
|
||||
let role = if trimmed_role.eq_ignore_ascii_case("outer")
|
||||
|| trimmed_role.eq_ignore_ascii_case("outline")
|
||||
{
|
||||
ProcessedMemberRole::Outer
|
||||
} else if trimmed_role.eq_ignore_ascii_case("inner") {
|
||||
ProcessedMemberRole::Inner
|
||||
} else if trimmed_role.eq_ignore_ascii_case("part") {
|
||||
if relation_type == Some("building") {
|
||||
// "part" role only applies to type=building relations.
|
||||
ProcessedMemberRole::Part
|
||||
} else {
|
||||
// For multipolygon relations, "part" is not a valid role, skip.
|
||||
return None;
|
||||
}
|
||||
} else if is_building_relation {
|
||||
ProcessedMemberRole::Outer
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Check if the way exists in ways_map
|
||||
@@ -308,8 +331,8 @@ pub fn parse_osm_data(
|
||||
}
|
||||
};
|
||||
|
||||
// Water and boundary relations: keep unclipped for ring merging
|
||||
// Other relations: clip member ways now
|
||||
// If keep_unclipped is true (e.g., certain water or building multipolygon
|
||||
// relations), keep member ways unclipped for ring merging; otherwise clip now.
|
||||
let final_way = if keep_unclipped {
|
||||
way
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::progress::{emit_gui_error, emit_gui_progress_update, is_running_with_
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use colored::Colorize;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::blocking::ClientBuilder;
|
||||
use serde::Deserialize;
|
||||
@@ -117,7 +117,7 @@ pub fn fetch_data_from_overpass(
|
||||
];
|
||||
let fallback_api_servers: Vec<&str> =
|
||||
vec!["https://maps.mail.ru/osm/tools/overpass/api/interpreter"];
|
||||
let mut url: &&str = api_servers.choose(&mut rand::thread_rng()).unwrap();
|
||||
let mut url: &&str = api_servers.choose(&mut rand::rng()).unwrap();
|
||||
|
||||
// Generate Overpass API query for bounding box
|
||||
let query: String = format!(
|
||||
@@ -139,7 +139,6 @@ pub fn fetch_data_from_overpass(
|
||||
nwr["barrier"];
|
||||
nwr["entrance"];
|
||||
nwr["door"];
|
||||
nwr["boundary"];
|
||||
nwr["power"];
|
||||
nwr["historic"];
|
||||
nwr["emergency"];
|
||||
@@ -186,9 +185,7 @@ pub fn fetch_data_from_overpass(
|
||||
}
|
||||
|
||||
println!("Request failed. Switching to fallback url...");
|
||||
url = fallback_api_servers
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap();
|
||||
url = fallback_api_servers.choose(&mut rand::rng()).unwrap();
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,7 +663,7 @@ impl BedrockWriter {
|
||||
for z in 0..16usize {
|
||||
for y in 0..16usize {
|
||||
let internal_idx = y * 256 + z * 16 + x;
|
||||
let block = section.blocks[internal_idx];
|
||||
let block = section.get_block_at_index(internal_idx);
|
||||
|
||||
// Get stored properties for this block position (if any)
|
||||
let properties = section.properties.get(&internal_idx);
|
||||
|
||||
@@ -54,9 +54,118 @@ pub(crate) struct PaletteItem {
|
||||
pub properties: Option<Value>,
|
||||
}
|
||||
|
||||
/// Block storage strategy for a 16×16×16 section.
|
||||
///
|
||||
/// **Memory optimisation**: instead of always allocating a 4 096-byte array,
|
||||
/// we distinguish two cases:
|
||||
///
|
||||
/// * `Uniform(block)` – every position holds the same block (1 byte).
|
||||
/// This covers freshly-created (all-AIR) sections, and sections that were
|
||||
/// entirely filled with one type (e.g. STONE underground with `--fillground`).
|
||||
///
|
||||
/// * `Full(Vec<Block>)` – the general case, equivalent to the old `[Block; 4096]`
|
||||
/// but heap-allocated via `Vec` so the *inline* size inside the parent
|
||||
/// `FnvHashMap` entry is only 24 bytes (pointer + length + capacity) instead
|
||||
/// of 4 096 bytes. This eliminates huge HashMap-slot waste from unused
|
||||
/// capacity slots.
|
||||
pub(crate) enum BlockStorage {
|
||||
/// Every position is the same block (commonly AIR).
|
||||
Uniform(Block),
|
||||
/// Mixed blocks – always exactly 4 096 entries.
|
||||
Full(Vec<Block>),
|
||||
}
|
||||
|
||||
impl BlockStorage {
|
||||
/// Read block at flat `index` (0..4095).
|
||||
#[inline(always)]
|
||||
pub fn get(&self, index: usize) -> Block {
|
||||
match self {
|
||||
BlockStorage::Uniform(b) => *b,
|
||||
BlockStorage::Full(v) => v[index],
|
||||
}
|
||||
}
|
||||
|
||||
/// Write block at flat `index`.
|
||||
/// Promotes `Uniform` → `Full` on the first differing write.
|
||||
#[inline]
|
||||
pub fn set(&mut self, index: usize, block: Block) {
|
||||
match self {
|
||||
BlockStorage::Uniform(b) if *b == block => {
|
||||
// No-op – writing the same value.
|
||||
}
|
||||
BlockStorage::Uniform(base) => {
|
||||
let base = *base;
|
||||
let mut v = vec![base; 4096];
|
||||
v[index] = block;
|
||||
*self = BlockStorage::Full(v);
|
||||
}
|
||||
BlockStorage::Full(v) => {
|
||||
v[index] = block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all 4 096 blocks.
|
||||
#[inline]
|
||||
pub fn iter(&self) -> BlockStorageIter<'_> {
|
||||
match self {
|
||||
BlockStorage::Uniform(b) => BlockStorageIter::Uniform(*b, 0),
|
||||
BlockStorage::Full(v) => BlockStorageIter::Full(v.iter()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to collapse a `Full` vec back to `Uniform` if every entry
|
||||
/// is the same block. Frees the 4 KiB heap allocation.
|
||||
pub fn try_compact(&mut self) {
|
||||
if let BlockStorage::Full(v) = self {
|
||||
if let Some(&first) = v.first() {
|
||||
if v.iter().all(|&b| b == first) {
|
||||
*self = BlockStorage::Uniform(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator returned by [`BlockStorage::iter`].
|
||||
pub(crate) enum BlockStorageIter<'a> {
|
||||
Uniform(Block, usize),
|
||||
Full(std::slice::Iter<'a, Block>),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BlockStorageIter<'a> {
|
||||
type Item = Block;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Block> {
|
||||
match self {
|
||||
BlockStorageIter::Uniform(b, count) => {
|
||||
if *count < 4096 {
|
||||
*count += 1;
|
||||
Some(*b)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
BlockStorageIter::Full(it) => it.next().copied(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let rem = match self {
|
||||
BlockStorageIter::Uniform(_, c) => 4096 - *c,
|
||||
BlockStorageIter::Full(it) => it.len(),
|
||||
};
|
||||
(rem, Some(rem))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for BlockStorageIter<'_> {}
|
||||
|
||||
/// A section being modified (16x16x16 blocks)
|
||||
pub(crate) struct SectionToModify {
|
||||
pub blocks: [Block; 4096],
|
||||
pub storage: BlockStorage,
|
||||
/// Store properties for blocks that have them, indexed by the same index as blocks array
|
||||
pub properties: FnvHashMap<usize, Value>,
|
||||
}
|
||||
@@ -64,7 +173,7 @@ pub(crate) struct SectionToModify {
|
||||
impl SectionToModify {
|
||||
#[inline]
|
||||
pub fn get_block(&self, x: u8, y: u8, z: u8) -> Option<Block> {
|
||||
let b = self.blocks[Self::index(x, y, z)];
|
||||
let b = self.storage.get(Self::index(x, y, z));
|
||||
if b == AIR {
|
||||
return None;
|
||||
}
|
||||
@@ -73,7 +182,9 @@ impl SectionToModify {
|
||||
|
||||
#[inline]
|
||||
pub fn set_block(&mut self, x: u8, y: u8, z: u8, block: Block) {
|
||||
self.blocks[Self::index(x, y, z)] = block;
|
||||
let index = Self::index(x, y, z);
|
||||
self.storage.set(index, block);
|
||||
self.properties.remove(&index);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -85,7 +196,7 @@ impl SectionToModify {
|
||||
block_with_props: BlockWithProperties,
|
||||
) {
|
||||
let index = Self::index(x, y, z);
|
||||
self.blocks[index] = block_with_props.block;
|
||||
self.storage.set(index, block_with_props.block);
|
||||
|
||||
// Store properties if they exist
|
||||
if let Some(props) = block_with_props.properties {
|
||||
@@ -96,20 +207,56 @@ impl SectionToModify {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read block at a raw flat index (used by Bedrock serialiser).
|
||||
#[inline(always)]
|
||||
pub fn get_block_at_index(&self, index: usize) -> Block {
|
||||
self.storage.get(index)
|
||||
}
|
||||
|
||||
/// Calculate index from coordinates (YZX order)
|
||||
#[inline(always)]
|
||||
pub fn index(x: u8, y: u8, z: u8) -> usize {
|
||||
usize::from(y) % 16 * 256 + usize::from(z) * 16 + usize::from(x)
|
||||
}
|
||||
|
||||
/// Try to collapse the block array back to `Uniform` if every entry
|
||||
/// is the same block and there are no properties.
|
||||
pub fn compact(&mut self) {
|
||||
if self.properties.is_empty() {
|
||||
self.storage.try_compact();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to Java Edition section format
|
||||
pub fn to_section(&self, y: i8) -> Section {
|
||||
// Fast path: Uniform section → single palette entry, no data array needed.
|
||||
// Only valid when no per-index properties exist, otherwise we must
|
||||
// fall through to the general path so every index is checked.
|
||||
if self.properties.is_empty() {
|
||||
if let BlockStorage::Uniform(block) = &self.storage {
|
||||
let palette_item = PaletteItem {
|
||||
name: format!("{}:{}", block.namespace(), block.name()),
|
||||
properties: block.properties(),
|
||||
};
|
||||
return Section {
|
||||
block_states: Blockstates {
|
||||
palette: vec![palette_item],
|
||||
data: None,
|
||||
other: FnvHashMap::default(),
|
||||
},
|
||||
y,
|
||||
other: FnvHashMap::default(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// General path: mixed blocks.
|
||||
// Create a map of unique block+properties combinations to palette indices
|
||||
let mut unique_blocks: Vec<(Block, Option<Value>)> = Vec::new();
|
||||
let mut palette_lookup: FnvHashMap<(Block, Option<String>), usize> = FnvHashMap::default();
|
||||
|
||||
// Build unique block combinations and lookup table
|
||||
for (i, &block) in self.blocks.iter().enumerate() {
|
||||
for (i, block) in self.storage.iter().enumerate() {
|
||||
let properties = self.properties.get(&i).cloned();
|
||||
|
||||
// Create a key for the lookup (block + properties hash)
|
||||
@@ -132,7 +279,7 @@ impl SectionToModify {
|
||||
let mut cur = 0;
|
||||
let mut cur_idx = 0;
|
||||
|
||||
for (i, &block) in self.blocks.iter().enumerate() {
|
||||
for (i, block) in self.storage.iter().enumerate() {
|
||||
let properties = self.properties.get(&i).cloned();
|
||||
let props_key = properties.as_ref().map(|p| format!("{p:?}"));
|
||||
let lookup_key = (block, props_key);
|
||||
@@ -175,7 +322,7 @@ impl SectionToModify {
|
||||
impl Default for SectionToModify {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blocks: [AIR; 4096],
|
||||
storage: BlockStorage::Uniform(AIR),
|
||||
properties: FnvHashMap::default(),
|
||||
}
|
||||
}
|
||||
@@ -245,7 +392,7 @@ impl RegionToModify {
|
||||
}
|
||||
}
|
||||
|
||||
/// The entire world being modified
|
||||
/// The entire world being modified.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WorldToModify {
|
||||
pub regions: FnvHashMap<(i32, i32), RegionToModify>,
|
||||
@@ -271,7 +418,6 @@ impl WorldToModify {
|
||||
|
||||
let region: &RegionToModify = self.get_region(region_x, region_z)?;
|
||||
let chunk: &ChunkToModify = region.get_chunk(chunk_x & 31, chunk_z & 31)?;
|
||||
|
||||
chunk.get_block(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
@@ -288,7 +434,6 @@ impl WorldToModify {
|
||||
|
||||
let region: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
|
||||
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
chunk.set_block(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
@@ -312,7 +457,6 @@ impl WorldToModify {
|
||||
|
||||
let region: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
|
||||
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
chunk.set_block_with_properties(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
@@ -320,4 +464,100 @@ impl WorldToModify {
|
||||
block_with_props,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a block only if the position is currently empty (AIR / absent).
|
||||
///
|
||||
/// This avoids the double HashMap traversal of `get_block()` + `set_block()`
|
||||
/// which is the hot path in ground generation and many element processors.
|
||||
#[inline]
|
||||
pub fn set_block_if_absent(&mut self, x: i32, y: i32, z: i32, block: Block) {
|
||||
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.regions.entry((region_x, region_z)).or_default();
|
||||
let chunk = region
|
||||
.chunks
|
||||
.entry((chunk_x & 31, chunk_z & 31))
|
||||
.or_default();
|
||||
|
||||
// Clamp Y
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = chunk.sections.entry(section_idx).or_default();
|
||||
|
||||
let local_x = (x & 15) as u8;
|
||||
let local_y = (y & 15) as u8;
|
||||
let local_z = (z & 15) as u8;
|
||||
let idx = SectionToModify::index(local_x, local_y, local_z);
|
||||
|
||||
// Only write if the current block is AIR
|
||||
if section.storage.get(idx) == AIR {
|
||||
section.storage.set(idx, block);
|
||||
// Clear any stale properties from a previous block at this position
|
||||
section.properties.remove(&idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill an entire column (single x, z) from y_min to y_max with the same block,
|
||||
/// resolving region/chunk only once. Used by ground generation.
|
||||
#[inline]
|
||||
pub fn fill_column(
|
||||
&mut self,
|
||||
x: i32,
|
||||
z: i32,
|
||||
y_min: i32,
|
||||
y_max: i32,
|
||||
block: Block,
|
||||
skip_existing: bool,
|
||||
) {
|
||||
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.regions.entry((region_x, region_z)).or_default();
|
||||
let chunk = region
|
||||
.chunks
|
||||
.entry((chunk_x & 31, chunk_z & 31))
|
||||
.or_default();
|
||||
|
||||
let local_x = (x & 15) as u8;
|
||||
let local_z = (z & 15) as u8;
|
||||
|
||||
let y_min = y_min.clamp(MIN_Y, MAX_Y);
|
||||
let y_max = y_max.clamp(MIN_Y, MAX_Y);
|
||||
|
||||
for y in y_min..=y_max {
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = chunk.sections.entry(section_idx).or_default();
|
||||
let local_y = (y & 15) as u8;
|
||||
let idx = SectionToModify::index(local_x, local_y, local_z);
|
||||
|
||||
if skip_existing {
|
||||
if section.storage.get(idx) == AIR {
|
||||
section.storage.set(idx, block);
|
||||
section.properties.remove(&idx);
|
||||
}
|
||||
} else {
|
||||
section.storage.set(idx, block);
|
||||
section.properties.remove(&idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan every section and collapse any that are entirely one block type
|
||||
/// from `Full(Vec)` back to `Uniform(Block)`, freeing the 4 KiB allocation.
|
||||
pub fn compact_sections(&mut self) {
|
||||
for region in self.regions.values_mut() {
|
||||
for chunk in region.chunks.values_mut() {
|
||||
for section in chunk.sections.values_mut() {
|
||||
if matches!(§ion.storage, BlockStorage::Full(_)) {
|
||||
section.compact();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,10 @@ pub struct WorldEditor<'a> {
|
||||
ground: Option<Arc<Ground>>,
|
||||
format: WorldFormat,
|
||||
/// Optional level name for Bedrock worlds (e.g., "Arnis World: New York City")
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_level_name: Option<String>,
|
||||
/// Optional spawn point for Bedrock worlds (x, z coordinates)
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_spawn_point: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
@@ -93,7 +95,9 @@ impl<'a> WorldEditor<'a> {
|
||||
llbbox,
|
||||
ground: None,
|
||||
format: WorldFormat::JavaAnvil,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_level_name: None,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_spawn_point: None,
|
||||
}
|
||||
}
|
||||
@@ -107,8 +111,12 @@ impl<'a> WorldEditor<'a> {
|
||||
xzbbox: &'a XZBBox,
|
||||
llbbox: LLBBox,
|
||||
format: WorldFormat,
|
||||
bedrock_level_name: Option<String>,
|
||||
bedrock_spawn_point: Option<(i32, i32)>,
|
||||
#[cfg_attr(not(feature = "bedrock"), allow(unused_variables))] bedrock_level_name: Option<
|
||||
String,
|
||||
>,
|
||||
#[cfg_attr(not(feature = "bedrock"), allow(unused_variables))] bedrock_spawn_point: Option<
|
||||
(i32, i32),
|
||||
>,
|
||||
) -> Self {
|
||||
Self {
|
||||
world_dir,
|
||||
@@ -117,7 +125,9 @@ impl<'a> WorldEditor<'a> {
|
||||
llbbox,
|
||||
ground: None,
|
||||
format,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_level_name,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_spawn_point,
|
||||
}
|
||||
}
|
||||
@@ -604,45 +614,6 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills a cuboid area with the specified block between two coordinates using absolute Y values.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline]
|
||||
pub fn fill_blocks_absolute(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x1: i32,
|
||||
y1_absolute: i32,
|
||||
z1: i32,
|
||||
x2: i32,
|
||||
y2_absolute: i32,
|
||||
z2: i32,
|
||||
override_whitelist: Option<&[Block]>,
|
||||
override_blacklist: Option<&[Block]>,
|
||||
) {
|
||||
let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
|
||||
let (min_y, max_y) = if y1_absolute < y2_absolute {
|
||||
(y1_absolute, y2_absolute)
|
||||
} else {
|
||||
(y2_absolute, y1_absolute)
|
||||
};
|
||||
let (min_z, max_z) = if z1 < z2 { (z1, z2) } else { (z2, z1) };
|
||||
|
||||
for x in min_x..=max_x {
|
||||
for absolute_y in min_y..=max_y {
|
||||
for z in min_z..=max_z {
|
||||
self.set_block_absolute(
|
||||
block,
|
||||
x,
|
||||
absolute_y,
|
||||
z,
|
||||
override_whitelist,
|
||||
override_blacklist,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for a block at the given coordinates.
|
||||
#[inline]
|
||||
pub fn check_for_block(&self, x: i32, y: i32, z: i32, whitelist: Option<&[Block]>) -> bool {
|
||||
@@ -706,6 +677,40 @@ impl<'a> WorldEditor<'a> {
|
||||
self.world.get_block(x, absolute_y, z).is_some()
|
||||
}
|
||||
|
||||
/// Sets a block only if no modification has been recorded yet at this
|
||||
/// position (i.e. the in-memory overlay still holds AIR).
|
||||
///
|
||||
/// This is faster than `set_block_absolute` with `None` whitelists/blacklists
|
||||
/// because it avoids the double HashMap traversal.
|
||||
#[inline]
|
||||
pub fn set_block_if_absent_absolute(&mut self, block: Block, x: i32, absolute_y: i32, z: i32) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
self.world.set_block_if_absent(x, absolute_y, z, block);
|
||||
}
|
||||
|
||||
/// Fills an entire column from y_min to y_max with one block type.
|
||||
///
|
||||
/// Resolves region/chunk once instead of per-Y-level, making underground
|
||||
/// fill (`--fillground`) dramatically faster.
|
||||
#[inline]
|
||||
pub fn fill_column_absolute(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x: i32,
|
||||
z: i32,
|
||||
y_min: i32,
|
||||
y_max: i32,
|
||||
skip_existing: bool,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
self.world
|
||||
.fill_column(x, z, y_min, y_max, block, skip_existing);
|
||||
}
|
||||
|
||||
/// Saves all changes made to the world by writing to the appropriate format.
|
||||
pub fn save(&mut self) {
|
||||
println!(
|
||||
@@ -716,6 +721,10 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
);
|
||||
|
||||
// Compact sections before saving: collapses uniform Full(Vec) sections
|
||||
// (e.g. all-STONE from --fillground) back to Uniform, freeing ~4 KiB each.
|
||||
self.world.compact_sections();
|
||||
|
||||
match self.format {
|
||||
WorldFormat::JavaAnvil => self.save_java(),
|
||||
WorldFormat::BedrockMcWorld => self.save_bedrock(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Arnis",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.0",
|
||||
"identifier": "com.louisdev.arnis",
|
||||
"build": {
|
||||
"frontendDist": "src/gui"
|
||||
|
||||
Reference in New Issue
Block a user