mirror of
https://github.com/louis-e/arnis.git
synced 2026-02-01 09:53:54 -05:00
Compare commits
3 Commits
dependabot
...
parallel-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc4d3c3e0e | ||
|
|
a46d2f93f1 | ||
|
|
2d532ab8f9 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
binary_name: arnis
|
||||
asset_name: arnis-linux
|
||||
- os: macos-15-intel # Intel runner for x86_64 builds
|
||||
- os: macos-13 # Intel runner for x86_64 builds
|
||||
target: x86_64-apple-darwin
|
||||
binary_name: arnis
|
||||
asset_name: arnis-mac-intel
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
- name: Download macOS Intel build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-15-intel-x86_64-apple-darwin-build
|
||||
name: macos-13-x86_64-apple-darwin-build
|
||||
path: ./intel
|
||||
|
||||
- name: Download macOS ARM64 build
|
||||
@@ -157,4 +157,4 @@ jobs:
|
||||
builds/linux/arnis-linux
|
||||
builds/macos/arnis-mac-universal
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
431
Cargo.lock
generated
431
Cargo.lock
generated
@@ -182,7 +182,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arnis"
|
||||
version = "2.4.1"
|
||||
version = "2.4.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bedrockrs_level",
|
||||
@@ -206,9 +206,8 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rayon",
|
||||
"reqwest 0.13.1",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
"rusty-leveldb",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -463,28 +462,6 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -828,11 +805,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
@@ -936,15 +912,6 @@ version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
@@ -1059,7 +1026,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1635,12 +1602,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
@@ -1663,6 +1624,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1670,7 +1640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1684,6 +1654,12 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1709,12 +1685,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@@ -2006,11 +1976,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2322,21 +2290,19 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
@@ -2360,29 +2326,38 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.19"
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"socket2 0.5.8",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2726,16 +2701,6 @@ version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
@@ -3058,12 +3023,6 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lz4-java-wrc"
|
||||
version = "0.2.0"
|
||||
@@ -3196,6 +3155,23 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nbtx"
|
||||
version = "0.1.0"
|
||||
@@ -3671,10 +3647,48 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
name = "openssl"
|
||||
version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
@@ -4160,62 +4174,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.9",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
"lru-slab",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.9",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
@@ -4483,44 +4441,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -4534,28 +4457,33 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4664,12 +4592,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -4707,11 +4629,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.36"
|
||||
version = "0.23.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
@@ -4720,61 +4641,26 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -4871,12 +4757,12 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.1"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -4884,9 +4770,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -5198,6 +5084,16 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
@@ -5217,7 +5113,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -5508,7 +5404,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest 0.12.28",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -5899,7 +5795,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -5915,6 +5811,16 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.1"
|
||||
@@ -6048,24 +5954,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -6328,6 +6216,12 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vek"
|
||||
version = "0.17.2"
|
||||
@@ -6621,15 +6515,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.4.1"
|
||||
version = "2.4.0"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -15,7 +15,7 @@ overflow-checks = true
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek", "rusty-leveldb"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {version = "2", optional = true}
|
||||
@@ -40,7 +40,7 @@ once_cell = "1.21.3"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.13.1", features = ["blocking", "json"] }
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.16.0", optional = true }
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -54,7 +54,6 @@ bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs",
|
||||
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
||||
vek = { version = "0.17", optional = true }
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
||||
rusty-leveldb = { version = "3", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
@@ -63,8 +63,6 @@ Arnis has been recognized in various academic and press publications after gaini
|
||||
|
||||
[XDA Developers: Hometown Minecraft Map: Arnis](https://www.xda-developers.com/hometown-minecraft-map-arnis/)
|
||||
|
||||
Free to use assets, including screenshots and logos, can be found [here](https://drive.google.com/file/d/1T1IsZSyT8oa6qAO_40hVF5KR8eEVCJjo/view?usp=sharing).
|
||||
|
||||
## :copyright: License Information
|
||||
Copyright (c) 2022-2025 Louis Erbkamm (louis-e)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -58,6 +58,10 @@ pub struct Args {
|
||||
/// Set floodfill timeout (seconds) (optional)
|
||||
#[arg(long, value_parser = parse_duration)]
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// Spawn point coordinates (lat, lng)
|
||||
#[arg(skip)]
|
||||
pub spawn_point: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||
|
||||
@@ -578,11 +578,6 @@ pub fn to_bedrock_block_with_properties(
|
||||
return convert_stairs(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle barrel facing direction
|
||||
if java_name == "barrel" {
|
||||
return convert_barrel(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle slabs with type property (top/bottom/double)
|
||||
if java_name.ends_with("_slab") {
|
||||
return convert_slab(java_name, props_map);
|
||||
@@ -655,46 +650,6 @@ fn convert_stairs(
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java barrel to Bedrock format with facing direction.
|
||||
fn convert_barrel(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let mut states = HashMap::new();
|
||||
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||
let facing_direction = match facing.as_str() {
|
||||
"down" => 0,
|
||||
"up" => 1,
|
||||
"north" => 2,
|
||||
"south" => 3,
|
||||
"west" => 4,
|
||||
"east" => 5,
|
||||
_ => 1,
|
||||
};
|
||||
states.insert(
|
||||
"facing_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(facing_direction),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !states.contains_key("facing_direction") {
|
||||
states.insert(
|
||||
"facing_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(1),
|
||||
);
|
||||
}
|
||||
|
||||
states.insert("open_bit".to_string(), BedrockBlockStateValue::Bool(false));
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{java_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java slab block to Bedrock format with proper type.
|
||||
fn convert_slab(
|
||||
java_name: &str,
|
||||
|
||||
@@ -266,17 +266,6 @@ impl Block {
|
||||
185 => "quartz_stairs",
|
||||
186 => "polished_andesite_stairs",
|
||||
187 => "nether_brick_stairs",
|
||||
188 => "barrel",
|
||||
189 => "fern",
|
||||
190 => "cobweb",
|
||||
191 => "chiseled_bookshelf",
|
||||
192 => "chiseled_bookshelf",
|
||||
193 => "chiseled_bookshelf",
|
||||
194 => "chiseled_bookshelf",
|
||||
195 => "chipped_anvil",
|
||||
196 => "damaged_anvil",
|
||||
197 => "large_fern",
|
||||
198 => "large_fern",
|
||||
_ => panic!("Invalid id"),
|
||||
}
|
||||
}
|
||||
@@ -474,37 +463,6 @@ impl Block {
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
191 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||
map
|
||||
})),
|
||||
192 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||
map
|
||||
})),
|
||||
193 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||
map
|
||||
})),
|
||||
194 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
197 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||
map
|
||||
})),
|
||||
198 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||
map
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -739,19 +697,6 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
||||
pub const BARREL: Block = Block::new(188);
|
||||
pub const FERN: Block = Block::new(189);
|
||||
pub const COBWEB: Block = Block::new(190);
|
||||
pub const CHISELLED_BOOKSHELF_NORTH: Block = Block::new(191);
|
||||
pub const CHISELLED_BOOKSHELF_EAST: Block = Block::new(192);
|
||||
pub const CHISELLED_BOOKSHELF_SOUTH: Block = Block::new(193);
|
||||
pub const CHISELLED_BOOKSHELF_WEST: Block = Block::new(194);
|
||||
// Backwards-compatible alias (defaults to north-facing)
|
||||
pub const CHISELLED_BOOKSHELF: Block = CHISELLED_BOOKSHELF_NORTH;
|
||||
pub const CHIPPED_ANVIL: Block = Block::new(195);
|
||||
pub const DAMAGED_ANVIL: Block = Block::new(196);
|
||||
pub const LARGE_FERN_LOWER: Block = Block::new(197);
|
||||
pub const LARGE_FERN_UPPER: Block = Block::new(198);
|
||||
|
||||
/// Maps a block to its corresponding stair variant
|
||||
#[inline]
|
||||
|
||||
@@ -14,9 +14,6 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Get way ID for ID generation
|
||||
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
||||
|
||||
let is_closed = is_closed_polygon(nodes);
|
||||
|
||||
if !is_closed {
|
||||
@@ -57,13 +54,12 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
|
||||
}
|
||||
|
||||
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||
|
||||
let polygon = remove_consecutive_duplicates(polygon);
|
||||
|
||||
if polygon.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
||||
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
|
||||
}
|
||||
|
||||
@@ -500,15 +496,12 @@ fn find_bbox_intersections(
|
||||
|
||||
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
|
||||
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
|
||||
// Use a slightly larger epsilon to handle floating-point errors from Sutherland-Hodgman.
|
||||
// Points should be clamped to bbox before this function is called, so any point
|
||||
// at or very near the boundary should be considered ON that edge.
|
||||
let eps = 1.0;
|
||||
let eps = 0.5;
|
||||
|
||||
let on_left = (point.0 - min_x).abs() <= eps;
|
||||
let on_right = (point.0 - max_x).abs() <= eps;
|
||||
let on_bottom = (point.1 - min_z).abs() <= eps;
|
||||
let on_top = (point.1 - max_z).abs() <= eps;
|
||||
let on_left = (point.0 - min_x).abs() < eps;
|
||||
let on_right = (point.0 - max_x).abs() < eps;
|
||||
let on_bottom = (point.1 - min_z).abs() < eps;
|
||||
let on_top = (point.1 - max_z).abs() < eps;
|
||||
|
||||
// Handle corners (assign to edge in counter-clockwise order)
|
||||
if on_bottom && on_left {
|
||||
@@ -563,21 +556,20 @@ fn get_corners_between_edges(
|
||||
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
|
||||
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
|
||||
|
||||
// For opposite edges (distance = 2), we need to pick a direction.
|
||||
// Use counter-clockwise by default to ensure corners are inserted.
|
||||
// This prevents diagonal lines when polygon spans opposite bbox edges.
|
||||
// Opposite edges: don't insert corners
|
||||
if ccw_dist == 2 && cw_dist == 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
if ccw_dist <= cw_dist {
|
||||
// Go counter-clockwise
|
||||
let mut current = edge1;
|
||||
for _ in 0..ccw_dist {
|
||||
result.push(corners[current as usize]);
|
||||
current = (current + 1) % 4;
|
||||
}
|
||||
} else {
|
||||
// Go clockwise
|
||||
let mut current = edge1;
|
||||
for _ in 0..cw_dist {
|
||||
current = (current + 4 - 1) % 4;
|
||||
@@ -588,12 +580,6 @@ fn get_corners_between_edges(
|
||||
result
|
||||
}
|
||||
|
||||
/// Checks if two points are approximately equal (within epsilon tolerance).
|
||||
fn points_approx_equal(p1: (f64, f64), p2: (f64, f64)) -> bool {
|
||||
let eps = 1.0;
|
||||
(p1.0 - p2.0).abs() <= eps && (p1.1 - p2.1).abs() <= eps
|
||||
}
|
||||
|
||||
/// Inserts bbox corners where polygon transitions between different bbox edges.
|
||||
fn insert_bbox_corners(
|
||||
polygon: Vec<(f64, f64)>,
|
||||
@@ -618,13 +604,8 @@ fn insert_bbox_corners(
|
||||
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
|
||||
|
||||
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
|
||||
let corners = get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z);
|
||||
|
||||
// Filter out corners that match the current point or the next point
|
||||
for corner in corners {
|
||||
if !points_approx_equal(corner, current) && !points_approx_equal(corner, next) {
|
||||
result.push(corner);
|
||||
}
|
||||
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
|
||||
result.push(corner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,30 +69,24 @@ pub fn generate_world_with_options(
|
||||
|
||||
println!("{} Processing data...", "[4/7]".bold());
|
||||
|
||||
// Build highway connectivity map once before processing
|
||||
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
|
||||
|
||||
// Set ground reference in the editor to enable elevation-aware block placement
|
||||
editor.set_ground(Arc::clone(&ground));
|
||||
|
||||
println!("{} Processing terrain...", "[5/7]".bold());
|
||||
emit_gui_progress_update(25.0, "Processing terrain...");
|
||||
|
||||
// Pre-compute all flood fills in parallel for better CPU utilization
|
||||
let mut flood_fill_cache = FloodFillCache::precompute(&elements, args.timeout.as_ref());
|
||||
|
||||
// Collect building footprints to prevent trees from spawning inside buildings
|
||||
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
|
||||
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
|
||||
|
||||
// Partition elements: separate boundary elements for deferred processing
|
||||
// This avoids cloning by moving elements instead of copying them
|
||||
let (boundary_elements, other_elements): (Vec<_>, Vec<_>) = elements
|
||||
.into_iter()
|
||||
.partition(|element| element.tags().contains_key("boundary"));
|
||||
// Run both precomputations concurrently using rayon::join
|
||||
// This overlaps highway connectivity map building with flood fill computation
|
||||
let timeout_ref = args.timeout.as_ref();
|
||||
let (highway_connectivity, mut flood_fill_cache) = rayon::join(
|
||||
|| highways::build_highway_connectivity_map(&elements),
|
||||
|| FloodFillCache::precompute(&elements, timeout_ref),
|
||||
);
|
||||
println!("Pre-computed {} flood fills", flood_fill_cache.way_count());
|
||||
|
||||
// Process data
|
||||
let elements_count: usize = other_elements.len() + boundary_elements.len();
|
||||
let elements_count: usize = elements.len();
|
||||
let mut elements = elements; // Take ownership for consuming
|
||||
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
||||
process_pb.set_style(ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
||||
@@ -103,8 +97,8 @@ pub fn generate_world_with_options(
|
||||
let mut current_progress_prcs: f64 = 25.0;
|
||||
let mut last_emitted_progress: f64 = current_progress_prcs;
|
||||
|
||||
// Process non-boundary elements first
|
||||
for element in other_elements.into_iter() {
|
||||
// Process elements by draining in insertion order
|
||||
for element in elements.drain(..) {
|
||||
process_pb.inc(1);
|
||||
current_progress_prcs += progress_increment_prcs;
|
||||
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
||||
@@ -135,31 +129,13 @@ pub fn generate_world_with_options(
|
||||
&flood_fill_cache,
|
||||
);
|
||||
} else if way.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
landuse::generate_landuse(&mut editor, way, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("natural") {
|
||||
natural::generate_natural(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
natural::generate_natural(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("leisure") {
|
||||
leisure::generate_leisure(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
leisure::generate_leisure(&mut editor, way, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("barrier") {
|
||||
barriers::generate_barriers(&mut editor, &element);
|
||||
} else if let Some(val) = way.tags.get("waterway") {
|
||||
@@ -192,13 +168,7 @@ pub fn generate_world_with_options(
|
||||
} else if node.tags.contains_key("natural")
|
||||
&& node.tags.get("natural") == Some(&"tree".to_string())
|
||||
{
|
||||
natural::generate_natural(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
natural::generate_natural(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if node.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if node.tags.contains_key("barrier") {
|
||||
@@ -239,7 +209,6 @@ pub fn generate_world_with_options(
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if rel.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse_from_relation(
|
||||
@@ -247,7 +216,6 @@ pub fn generate_world_with_options(
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
leisure::generate_leisure_from_relation(
|
||||
@@ -255,7 +223,6 @@ pub fn generate_world_with_options(
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if rel.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(&mut editor, &element, args);
|
||||
@@ -270,33 +237,6 @@ pub fn generate_world_with_options(
|
||||
|
||||
process_pb.finish();
|
||||
|
||||
// Process deferred boundary elements after all other elements
|
||||
// This ensures boundaries only fill empty areas, they won't overwrite
|
||||
// any ground blocks set by landuse, leisure, natural, etc.
|
||||
for element in boundary_elements.into_iter() {
|
||||
match &element {
|
||||
ProcessedElement::Way(way) => {
|
||||
boundaries::generate_boundary(&mut editor, way, args, &flood_fill_cache);
|
||||
// Clean up cache entry for consistency with other element processing
|
||||
flood_fill_cache.remove_way(way.id);
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
boundaries::generate_boundary_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&xzbbox,
|
||||
);
|
||||
// Clean up cache entries for consistency with other element processing
|
||||
let way_ids: Vec<u64> = rel.members.iter().map(|m| m.way.id).collect();
|
||||
flood_fill_cache.remove_relation_ways(&way_ids);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Element is dropped here, freeing its memory immediately
|
||||
}
|
||||
|
||||
// Drop remaining caches
|
||||
drop(highway_connectivity);
|
||||
drop(flood_fill_cache);
|
||||
@@ -324,8 +264,7 @@ pub fn generate_world_with_options(
|
||||
let total_iterations_grnd: f64 = total_blocks as f64;
|
||||
let progress_increment_grnd: f64 = 20.0 / total_iterations_grnd;
|
||||
|
||||
// Check if terrain elevation is enabled; when disabled, we can skip ground level lookups entirely
|
||||
let terrain_enabled = ground.elevation_enabled;
|
||||
let groundlayer_block = GRASS_BLOCK;
|
||||
|
||||
// Process ground generation chunk-by-chunk for better cache locality.
|
||||
// This keeps the same region/chunk HashMap entries hot in CPU cache,
|
||||
@@ -345,19 +284,11 @@ pub fn generate_world_with_options(
|
||||
|
||||
for x in chunk_min_x..=chunk_max_x {
|
||||
for z in chunk_min_z..=chunk_max_z {
|
||||
// Get ground level, when terrain is enabled, look it up once per block
|
||||
// When disabled, use constant ground_level (no function call overhead)
|
||||
let ground_y = if terrain_enabled {
|
||||
editor.get_ground_level(x, z)
|
||||
} else {
|
||||
args.ground_level
|
||||
};
|
||||
|
||||
// Add default dirt and grass layer if there isn't a stone layer already
|
||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
||||
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
|
||||
editor.set_block(groundlayer_block, x, 0, z, None, None);
|
||||
editor.set_block(DIRT, x, -1, z, None, None);
|
||||
editor.set_block(DIRT, x, -2, z, None, None);
|
||||
}
|
||||
|
||||
// Fill underground with stone
|
||||
@@ -369,7 +300,7 @@ pub fn generate_world_with_options(
|
||||
MIN_Y + 1,
|
||||
z,
|
||||
x,
|
||||
ground_y - 3,
|
||||
editor.get_absolute_y(x, -3, z),
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
@@ -417,28 +348,28 @@ pub fn generate_world_with_options(
|
||||
// Update player spawn Y coordinate based on terrain height after generation
|
||||
#[cfg(feature = "gui")]
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
use crate::gui::update_player_spawn_y_after_generation;
|
||||
// Reconstruct bbox string to match the format that GUI originally provided.
|
||||
// This ensures LLBBox::from_str() can parse it correctly.
|
||||
let bbox_string = format!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.min().lng(),
|
||||
args.bbox.max().lat(),
|
||||
args.bbox.max().lng()
|
||||
);
|
||||
if let Some(spawn_coords) = &args.spawn_point {
|
||||
use crate::gui::update_player_spawn_y_after_generation;
|
||||
let bbox_string = format!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lng(),
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.max().lng(),
|
||||
args.bbox.max().lat()
|
||||
);
|
||||
|
||||
// Always update spawn Y since we now always set a spawn point (user-selected or default)
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
bbox_string,
|
||||
args.scale,
|
||||
ground.as_ref(),
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
Some(*spawn_coords),
|
||||
bbox_string,
|
||||
args.scale,
|
||||
ground.as_ref(),
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use fastnbt::Value;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_amenities(
|
||||
editor: &mut WorldEditor,
|
||||
@@ -36,49 +34,6 @@ pub fn generate_amenities(
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
||||
.next();
|
||||
match amenity_type.as_str() {
|
||||
"recycling" => {
|
||||
let is_container = element
|
||||
.tags()
|
||||
.get("recycling_type")
|
||||
.is_some_and(|value| value == "container");
|
||||
|
||||
if !is_container {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(pt) = first_node {
|
||||
let mut rng = rand::thread_rng();
|
||||
let loot_pool = build_recycling_loot_pool(element.tags());
|
||||
let items = build_recycling_items(&loot_pool, &mut rng);
|
||||
|
||||
let properties = Value::Compound(recycling_barrel_properties());
|
||||
let barrel_block = BlockWithProperties::new(BARREL, Some(properties));
|
||||
let absolute_y = editor.get_absolute_y(pt.x, 1, pt.z);
|
||||
|
||||
editor.set_block_entity_with_items(
|
||||
barrel_block,
|
||||
pt.x,
|
||||
1,
|
||||
pt.z,
|
||||
"minecraft:barrel",
|
||||
items,
|
||||
);
|
||||
|
||||
if let Some(category) = single_loot_category(&loot_pool) {
|
||||
if let Some(display_item) =
|
||||
build_display_item_for_category(category, &mut rng)
|
||||
{
|
||||
place_item_frame_on_random_side(
|
||||
editor,
|
||||
pt.x,
|
||||
absolute_y,
|
||||
pt.z,
|
||||
display_item,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"waste_disposal" | "waste_basket" => {
|
||||
// Place a cauldron for waste disposal or waste basket
|
||||
if let Some(pt) = first_node {
|
||||
@@ -308,420 +263,3 @@ pub fn generate_amenities(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum RecyclingLootKind {
|
||||
GlassBottle,
|
||||
Paper,
|
||||
GlassBlock,
|
||||
GlassPane,
|
||||
LeatherArmor,
|
||||
EmptyBucket,
|
||||
LeatherBoots,
|
||||
ScrapMetal,
|
||||
GreenWaste,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum LeatherPiece {
|
||||
Helmet,
|
||||
Chestplate,
|
||||
Leggings,
|
||||
Boots,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum LootCategory {
|
||||
GlassBottle,
|
||||
Paper,
|
||||
Glass,
|
||||
Leather,
|
||||
EmptyBucket,
|
||||
ScrapMetal,
|
||||
GreenWaste,
|
||||
}
|
||||
|
||||
fn recycling_barrel_properties() -> HashMap<String, Value> {
|
||||
let mut props = HashMap::new();
|
||||
props.insert("facing".to_string(), Value::String("up".to_string()));
|
||||
props
|
||||
}
|
||||
|
||||
fn build_recycling_loot_pool(tags: &HashMap<String, String>) -> Vec<RecyclingLootKind> {
|
||||
let mut loot_pool: Vec<RecyclingLootKind> = Vec::new();
|
||||
|
||||
if tag_enabled(tags, "recycling:glass_bottles") {
|
||||
loot_pool.push(RecyclingLootKind::GlassBottle);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:paper") {
|
||||
loot_pool.push(RecyclingLootKind::Paper);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:glass") {
|
||||
loot_pool.push(RecyclingLootKind::GlassBlock);
|
||||
loot_pool.push(RecyclingLootKind::GlassPane);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:clothes") {
|
||||
loot_pool.push(RecyclingLootKind::LeatherArmor);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:cans") {
|
||||
loot_pool.push(RecyclingLootKind::EmptyBucket);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:shoes") {
|
||||
loot_pool.push(RecyclingLootKind::LeatherBoots);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:scrap_metal") {
|
||||
loot_pool.push(RecyclingLootKind::ScrapMetal);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:green_waste") {
|
||||
loot_pool.push(RecyclingLootKind::GreenWaste);
|
||||
}
|
||||
|
||||
loot_pool
|
||||
}
|
||||
|
||||
fn build_recycling_items(
|
||||
loot_pool: &[RecyclingLootKind],
|
||||
rng: &mut impl Rng,
|
||||
) -> Vec<HashMap<String, Value>> {
|
||||
if loot_pool.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut items = Vec::new();
|
||||
for slot in 0..27 {
|
||||
if rng.gen_bool(0.2) {
|
||||
let kind = loot_pool[rng.gen_range(0..loot_pool.len())];
|
||||
if let Some(item) = build_item_for_kind(kind, slot as i8, rng) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn kind_to_category(kind: RecyclingLootKind) -> LootCategory {
|
||||
match kind {
|
||||
RecyclingLootKind::GlassBottle => LootCategory::GlassBottle,
|
||||
RecyclingLootKind::Paper => LootCategory::Paper,
|
||||
RecyclingLootKind::GlassBlock | RecyclingLootKind::GlassPane => LootCategory::Glass,
|
||||
RecyclingLootKind::LeatherArmor | RecyclingLootKind::LeatherBoots => LootCategory::Leather,
|
||||
RecyclingLootKind::EmptyBucket => LootCategory::EmptyBucket,
|
||||
RecyclingLootKind::ScrapMetal => LootCategory::ScrapMetal,
|
||||
RecyclingLootKind::GreenWaste => LootCategory::GreenWaste,
|
||||
}
|
||||
}
|
||||
|
||||
fn single_loot_category(loot_pool: &[RecyclingLootKind]) -> Option<LootCategory> {
|
||||
let mut categories: HashSet<LootCategory> = HashSet::new();
|
||||
for kind in loot_pool {
|
||||
categories.insert(kind_to_category(*kind));
|
||||
if categories.len() > 1 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
categories.iter().next().copied()
|
||||
}
|
||||
|
||||
fn build_display_item_for_category(
|
||||
category: LootCategory,
|
||||
rng: &mut impl Rng,
|
||||
) -> Option<HashMap<String, Value>> {
|
||||
match category {
|
||||
LootCategory::GlassBottle => Some(make_display_item("minecraft:glass_bottle", 1)),
|
||||
LootCategory::Paper => Some(make_display_item("minecraft:paper", rng.gen_range(1..=4))),
|
||||
LootCategory::Glass => Some(make_display_item("minecraft:glass", 1)),
|
||||
LootCategory::Leather => Some(build_leather_display_item(rng)),
|
||||
LootCategory::EmptyBucket => Some(make_display_item("minecraft:bucket", 1)),
|
||||
LootCategory::ScrapMetal => {
|
||||
let metals = [
|
||||
"minecraft:copper_ingot",
|
||||
"minecraft:iron_ingot",
|
||||
"minecraft:gold_ingot",
|
||||
];
|
||||
let metal = metals.choose(rng)?;
|
||||
Some(make_display_item(metal, rng.gen_range(1..=2)))
|
||||
}
|
||||
LootCategory::GreenWaste => {
|
||||
let options = [
|
||||
"minecraft:oak_sapling",
|
||||
"minecraft:birch_sapling",
|
||||
"minecraft:tall_grass",
|
||||
"minecraft:sweet_berries",
|
||||
"minecraft:wheat_seeds",
|
||||
];
|
||||
let choice = options.choose(rng)?;
|
||||
Some(make_display_item(choice, rng.gen_range(1..=3)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn place_item_frame_on_random_side(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
barrel_absolute_y: i32,
|
||||
z: i32,
|
||||
item: HashMap<String, Value>,
|
||||
) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut directions = [
|
||||
((0, 0, -1), 2), // North
|
||||
((0, 0, 1), 3), // South
|
||||
((-1, 0, 0), 4), // West
|
||||
((1, 0, 0), 5), // East
|
||||
];
|
||||
directions.shuffle(&mut rng);
|
||||
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
|
||||
let ((dx, _dy, dz), facing) = directions
|
||||
.into_iter()
|
||||
.find(|((dx, _dy, dz), _)| {
|
||||
let target_x = x + dx;
|
||||
let target_z = z + dz;
|
||||
target_x >= min_x && target_x <= max_x && target_z >= min_z && target_z <= max_z
|
||||
})
|
||||
.unwrap_or(((0, 0, 1), 3)); // Fallback south if all directions are out of bounds
|
||||
|
||||
let target_x = x + dx;
|
||||
let target_y = barrel_absolute_y;
|
||||
let target_z = z + dz;
|
||||
|
||||
let ground_y = editor.get_absolute_y(target_x, 0, target_z);
|
||||
|
||||
let mut extra = HashMap::new();
|
||||
extra.insert("Facing".to_string(), Value::Byte(facing)); // 2=north, 3=south, 4=west, 5=east
|
||||
extra.insert("ItemRotation".to_string(), Value::Byte(0));
|
||||
extra.insert("Item".to_string(), Value::Compound(item));
|
||||
extra.insert("ItemDropChance".to_string(), Value::Float(1.0));
|
||||
extra.insert(
|
||||
"block_pos".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Int(target_x),
|
||||
Value::Int(target_y),
|
||||
Value::Int(target_z),
|
||||
]),
|
||||
);
|
||||
extra.insert("TileX".to_string(), Value::Int(target_x));
|
||||
extra.insert("TileY".to_string(), Value::Int(target_y));
|
||||
extra.insert("TileZ".to_string(), Value::Int(target_z));
|
||||
extra.insert("Fixed".to_string(), Value::Byte(1));
|
||||
|
||||
let relative_y = target_y - ground_y;
|
||||
editor.add_entity(
|
||||
"minecraft:item_frame",
|
||||
target_x,
|
||||
relative_y,
|
||||
target_z,
|
||||
Some(extra),
|
||||
);
|
||||
}
|
||||
|
||||
fn make_display_item(id: &str, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
fn build_leather_display_item(rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let mut item = make_display_item("minecraft:leather_chestplate", 1);
|
||||
let damage = biased_damage(80, rng);
|
||||
|
||||
let mut tag = HashMap::new();
|
||||
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||
|
||||
if let Some(color) = maybe_leather_color(rng) {
|
||||
let mut display = HashMap::new();
|
||||
display.insert("color".to_string(), Value::Int(color));
|
||||
tag.insert("display".to_string(), Value::Compound(display));
|
||||
}
|
||||
|
||||
item.insert("tag".to_string(), Value::Compound(tag));
|
||||
|
||||
let mut components = HashMap::new();
|
||||
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||
item.insert("components".to_string(), Value::Compound(components));
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
fn build_item_for_kind(
|
||||
kind: RecyclingLootKind,
|
||||
slot: i8,
|
||||
rng: &mut impl Rng,
|
||||
) -> Option<HashMap<String, Value>> {
|
||||
match kind {
|
||||
RecyclingLootKind::GlassBottle => Some(make_basic_item(
|
||||
"minecraft:glass_bottle",
|
||||
slot,
|
||||
rng.gen_range(1..=4),
|
||||
)),
|
||||
RecyclingLootKind::Paper => Some(make_basic_item(
|
||||
"minecraft:paper",
|
||||
slot,
|
||||
rng.gen_range(1..=10),
|
||||
)),
|
||||
RecyclingLootKind::GlassBlock => Some(build_glass_item(false, slot, rng)),
|
||||
RecyclingLootKind::GlassPane => Some(build_glass_item(true, slot, rng)),
|
||||
RecyclingLootKind::LeatherArmor => {
|
||||
Some(build_leather_item(random_leather_piece(rng), slot, rng))
|
||||
}
|
||||
RecyclingLootKind::EmptyBucket => Some(make_basic_item("minecraft:bucket", slot, 1)),
|
||||
RecyclingLootKind::LeatherBoots => Some(build_leather_item(LeatherPiece::Boots, slot, rng)),
|
||||
RecyclingLootKind::ScrapMetal => Some(build_scrap_metal_item(slot, rng)),
|
||||
RecyclingLootKind::GreenWaste => Some(build_green_waste_item(slot, rng)),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_scrap_metal_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let metals = ["copper_ingot", "iron_ingot", "gold_ingot"];
|
||||
let metal = metals.choose(rng).expect("scrap metal list is non-empty");
|
||||
let count = rng.gen_range(1..=3);
|
||||
make_basic_item(&format!("minecraft:{metal}"), slot, count)
|
||||
}
|
||||
|
||||
fn build_green_waste_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
#[allow(clippy::match_same_arms)]
|
||||
let (id, count) = match rng.gen_range(0..8) {
|
||||
0 => ("minecraft:tall_grass", rng.gen_range(1..=4)),
|
||||
1 => ("minecraft:sweet_berries", rng.gen_range(2..=6)),
|
||||
2 => ("minecraft:oak_sapling", rng.gen_range(1..=2)),
|
||||
3 => ("minecraft:birch_sapling", rng.gen_range(1..=2)),
|
||||
4 => ("minecraft:spruce_sapling", rng.gen_range(1..=2)),
|
||||
5 => ("minecraft:jungle_sapling", rng.gen_range(1..=2)),
|
||||
6 => ("minecraft:acacia_sapling", rng.gen_range(1..=2)),
|
||||
_ => ("minecraft:dark_oak_sapling", rng.gen_range(1..=2)),
|
||||
};
|
||||
|
||||
// 25% chance to replace with seeds instead
|
||||
let id = if rng.gen_bool(0.25) {
|
||||
match rng.gen_range(0..4) {
|
||||
0 => "minecraft:wheat_seeds",
|
||||
1 => "minecraft:pumpkin_seeds",
|
||||
2 => "minecraft:melon_seeds",
|
||||
_ => "minecraft:beetroot_seeds",
|
||||
}
|
||||
} else {
|
||||
id
|
||||
};
|
||||
|
||||
make_basic_item(id, slot, count)
|
||||
}
|
||||
|
||||
fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
const GLASS_COLORS: &[&str] = &[
|
||||
"white",
|
||||
"orange",
|
||||
"magenta",
|
||||
"light_blue",
|
||||
"yellow",
|
||||
"lime",
|
||||
"pink",
|
||||
"gray",
|
||||
"light_gray",
|
||||
"cyan",
|
||||
"purple",
|
||||
"blue",
|
||||
"brown",
|
||||
"green",
|
||||
"red",
|
||||
"black",
|
||||
];
|
||||
|
||||
let use_colorless = rng.gen_bool(0.7);
|
||||
|
||||
let id = if use_colorless {
|
||||
if is_pane {
|
||||
"minecraft:glass_pane".to_string()
|
||||
} else {
|
||||
"minecraft:glass".to_string()
|
||||
}
|
||||
} else {
|
||||
let color = GLASS_COLORS
|
||||
.choose(rng)
|
||||
.expect("glass color array is non-empty");
|
||||
if is_pane {
|
||||
format!("minecraft:{color}_stained_glass_pane")
|
||||
} else {
|
||||
format!("minecraft:{color}_stained_glass")
|
||||
}
|
||||
};
|
||||
|
||||
let count = if is_pane {
|
||||
rng.gen_range(4..=16)
|
||||
} else {
|
||||
rng.gen_range(1..=6)
|
||||
};
|
||||
|
||||
make_basic_item(&id, slot, count)
|
||||
}
|
||||
|
||||
fn build_leather_item(piece: LeatherPiece, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let (id, max_damage) = match piece {
|
||||
LeatherPiece::Helmet => ("minecraft:leather_helmet", 55),
|
||||
LeatherPiece::Chestplate => ("minecraft:leather_chestplate", 80),
|
||||
LeatherPiece::Leggings => ("minecraft:leather_leggings", 75),
|
||||
LeatherPiece::Boots => ("minecraft:leather_boots", 65),
|
||||
};
|
||||
|
||||
let mut item = make_basic_item(id, slot, 1);
|
||||
let damage = biased_damage(max_damage, rng);
|
||||
|
||||
let mut tag = HashMap::new();
|
||||
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||
|
||||
if let Some(color) = maybe_leather_color(rng) {
|
||||
let mut display = HashMap::new();
|
||||
display.insert("color".to_string(), Value::Int(color));
|
||||
tag.insert("display".to_string(), Value::Compound(display));
|
||||
}
|
||||
|
||||
item.insert("tag".to_string(), Value::Compound(tag));
|
||||
|
||||
let mut components = HashMap::new();
|
||||
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||
item.insert("components".to_string(), Value::Compound(components));
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
fn biased_damage(max_damage: i32, rng: &mut impl Rng) -> i32 {
|
||||
let safe_max = max_damage.max(1);
|
||||
let upper = safe_max.saturating_sub(1);
|
||||
let lower = (safe_max / 2).min(upper);
|
||||
|
||||
let heavy_wear = rng.gen_range(lower..=upper);
|
||||
let random_wear = rng.gen_range(0..=upper);
|
||||
heavy_wear.max(random_wear)
|
||||
}
|
||||
|
||||
fn maybe_leather_color(rng: &mut impl Rng) -> Option<i32> {
|
||||
if rng.gen_bool(0.3) {
|
||||
Some(rng.gen_range(0..=0x00FF_FFFF))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn random_leather_piece(rng: &mut impl Rng) -> LeatherPiece {
|
||||
match rng.gen_range(0..4) {
|
||||
0 => LeatherPiece::Helmet,
|
||||
1 => LeatherPiece::Chestplate,
|
||||
2 => LeatherPiece::Leggings,
|
||||
_ => LeatherPiece::Boots,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_basic_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
fn tag_enabled(tags: &HashMap<String, String>, key: &str) -> bool {
|
||||
tags.get(key).is_some_and(|value| value == "yes")
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
barrier_material = LIGHT_GRAY_CONCRETE;
|
||||
}
|
||||
if barrier_mat == "metal" {
|
||||
barrier_material = STONE_BRICK_WALL;
|
||||
barrier_material = STONE_BRICK_WALL; // IRON_BARS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,7 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
.get("height")
|
||||
.and_then(|height: &String| height.parse::<f32>().ok())
|
||||
.map(|height: f32| height.round() as i32)
|
||||
.unwrap_or(barrier_height)
|
||||
.max(2); // Minimum height of 2
|
||||
.unwrap_or(barrier_height);
|
||||
|
||||
// Process nodes to create the barrier wall
|
||||
for i in 1..way.nodes.len() {
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
//! Processing of administrative and urban boundaries.
|
||||
//!
|
||||
//! This module handles boundary elements that define urban areas (cities, boroughs, etc.)
|
||||
//! and sets appropriate ground blocks for them.
|
||||
//!
|
||||
//! Boundaries are processed last but only fill empty areas, allowing more specific
|
||||
//! landuse areas (parks, residential, etc.) to take priority over the general
|
||||
//! urban ground.
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::clipping::clip_way_to_bbox;
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
/// Checks if a boundary element represents an urban area that should have stone ground.
|
||||
///
|
||||
/// Returns true for:
|
||||
/// - `boundary=administrative` with `admin_level >= 8` (city/borough level or smaller)
|
||||
/// - `boundary=low_emission_zone` (urban traffic zones)
|
||||
/// - `boundary=limited_traffic_zone` (urban traffic zones)
|
||||
/// - `boundary=special_economic_zone` (developed industrial/commercial zones)
|
||||
/// - `boundary=political` (electoral districts, usually urban)
|
||||
fn is_urban_boundary(tags: &std::collections::HashMap<String, String>) -> bool {
|
||||
let Some(boundary_value) = tags.get("boundary") else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match boundary_value.as_str() {
|
||||
"administrative" => {
|
||||
// Only consider city-level or smaller (admin_level >= 8)
|
||||
// admin_level 2 = country, 4 = state, 6 = county, 8 = city/municipality
|
||||
if let Some(admin_level_str) = tags.get("admin_level") {
|
||||
if let Ok(admin_level) = admin_level_str.parse::<u8>() {
|
||||
return admin_level >= 8;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
// Urban zones that should have stone ground
|
||||
"low_emission_zone" | "limited_traffic_zone" | "special_economic_zone" | "political" => {
|
||||
true
|
||||
}
|
||||
// Natural/protected areas should keep grass - don't process these
|
||||
// "national_park" | "protected_area" | "forest" | "forest_compartment" | "aboriginal_lands"
|
||||
// Statistical/administrative-only boundaries - don't affect ground
|
||||
// "census" | "statistical" | "postal_code" | "timezone" | "disputed" | "maritime" | etc.
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate ground blocks for an urban boundary way.
|
||||
pub fn generate_boundary(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Check if this is an urban boundary
|
||||
if !is_urban_boundary(&element.tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the area of the boundary element using cache
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
// Fill the area with smooth stone as ground block
|
||||
// Use None, None to only set where no block exists yet - don't overwrite anything
|
||||
for (x, z) in floor_area {
|
||||
editor.set_block(SMOOTH_STONE, x, 0, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate ground blocks for an urban boundary relation.
|
||||
pub fn generate_boundary_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
xzbbox: &XZBBox,
|
||||
) {
|
||||
// Check if this is an urban boundary
|
||||
if !is_urban_boundary(&rel.tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect outer ways (unclipped) for merging
|
||||
let mut outers: Vec<Vec<ProcessedNode>> = rel
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.role == ProcessedMemberRole::Outer)
|
||||
.map(|m| m.way.nodes.clone())
|
||||
.collect();
|
||||
|
||||
if outers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge way segments into closed rings
|
||||
super::merge_way_segments(&mut outers);
|
||||
|
||||
// Clip each merged ring to bbox and process
|
||||
for ring in outers {
|
||||
if ring.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clip the merged ring to bbox
|
||||
let clipped_nodes = clip_way_to_bbox(&ring, xzbbox);
|
||||
if clipped_nodes.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a ProcessedWay for the clipped ring
|
||||
let clipped_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: clipped_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate boundary area from clipped way
|
||||
generate_boundary(editor, &clipped_way, args, flood_fill_cache);
|
||||
}
|
||||
}
|
||||
@@ -3,97 +3,37 @@ use crate::bresenham::bresenham_line;
|
||||
use crate::osm_parser::ProcessedWay;
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
// TODO FIX - This handles ways with bridge=yes tag (e.g., highway bridges)
|
||||
// TODO FIX
|
||||
#[allow(dead_code)]
|
||||
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
if let Some(_bridge_type) = element.tags.get("bridge") {
|
||||
let bridge_height = 3; // Height above the ground level
|
||||
|
||||
// Get start and end node elevations and use MAX for level bridge deck
|
||||
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
|
||||
let bridge_deck_ground_y = if element.nodes.len() >= 2 {
|
||||
let start_node = &element.nodes[0];
|
||||
let end_node = &element.nodes[element.nodes.len() - 1];
|
||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||
start_y.max(end_y)
|
||||
} else {
|
||||
return; // Need at least 2 nodes for a bridge
|
||||
};
|
||||
|
||||
// Calculate total bridge length for ramp positioning
|
||||
let total_length: f64 = element
|
||||
.nodes
|
||||
.windows(2)
|
||||
.map(|pair| {
|
||||
let dx = (pair[1].x - pair[0].x) as f64;
|
||||
let dz = (pair[1].z - pair[0].z) as f64;
|
||||
(dx * dx + dz * dz).sqrt()
|
||||
})
|
||||
.sum();
|
||||
|
||||
if total_length == 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut accumulated_length: f64 = 0.0;
|
||||
let bridge_height = 3; // Fixed height
|
||||
|
||||
for i in 1..element.nodes.len() {
|
||||
let prev = &element.nodes[i - 1];
|
||||
let cur = &element.nodes[i];
|
||||
|
||||
let segment_dx = (cur.x - prev.x) as f64;
|
||||
let segment_dz = (cur.z - prev.z) as f64;
|
||||
let segment_length = (segment_dx * segment_dx + segment_dz * segment_dz).sqrt();
|
||||
|
||||
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
|
||||
|
||||
let ramp_length = (total_length * 0.15).clamp(6.0, 20.0) as usize; // 15% of bridge, min 6, max 20 blocks
|
||||
let total_length = points.len();
|
||||
let ramp_length = 6; // Length of ramp at each end
|
||||
|
||||
for (idx, (x, _, z)) in points.iter().enumerate() {
|
||||
// Calculate progress along this segment
|
||||
let segment_progress = if points.len() > 1 {
|
||||
idx as f64 / (points.len() - 1) as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Calculate overall progress along the entire bridge
|
||||
let point_distance = accumulated_length + segment_progress * segment_length;
|
||||
let overall_progress = (point_distance / total_length).clamp(0.0, 1.0);
|
||||
let total_len_usize = total_length as usize;
|
||||
let overall_idx = (overall_progress * total_len_usize as f64) as usize;
|
||||
|
||||
// Calculate ramp height offset
|
||||
let ramp_offset = if overall_idx < ramp_length {
|
||||
let height = if idx < ramp_length {
|
||||
// Start ramp (rising)
|
||||
(overall_idx as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
||||
} else if overall_idx >= total_len_usize.saturating_sub(ramp_length) {
|
||||
(idx * bridge_height) / ramp_length
|
||||
} else if idx >= total_length - ramp_length {
|
||||
// End ramp (descending)
|
||||
let dist_from_end = total_len_usize - overall_idx;
|
||||
(dist_from_end as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
||||
((total_length - idx) * bridge_height) / ramp_length
|
||||
} else {
|
||||
// Middle section (constant height)
|
||||
bridge_height
|
||||
};
|
||||
|
||||
// Use fixed bridge deck height (max of endpoints) plus ramp offset
|
||||
let bridge_y = bridge_deck_ground_y + ramp_offset;
|
||||
|
||||
// Place bridge blocks
|
||||
for dx in -2..=2 {
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
*x + dx,
|
||||
bridge_y,
|
||||
*z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
editor.set_block(LIGHT_GRAY_CONCRETE, *x + dx, height as i32, *z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
accumulated_length += segment_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ pub fn generate_buildings(
|
||||
let lev = levels - min_level;
|
||||
|
||||
if lev >= 1 {
|
||||
building_height = multiply_scale(lev * 4 + 2, scale_factor);
|
||||
building_height = multiply_scale(levels * 4 + 2, scale_factor);
|
||||
building_height = building_height.max(3);
|
||||
|
||||
// Mark as tall building if more than 7 stories
|
||||
@@ -542,20 +542,6 @@ pub fn generate_buildings(
|
||||
}
|
||||
}
|
||||
|
||||
// Detect abandoned buildings via explicit tags
|
||||
let is_abandoned_building = element
|
||||
.tags
|
||||
.get("abandoned")
|
||||
.is_some_and(|value| value == "yes")
|
||||
|| element.tags.contains_key("abandoned:building");
|
||||
|
||||
// Use cobwebs instead of glowstone for abandoned buildings
|
||||
let ceiling_light_block = if is_abandoned_building {
|
||||
COBWEB
|
||||
} else {
|
||||
GLOWSTONE
|
||||
};
|
||||
|
||||
for (x, z) in floor_area.iter().cloned() {
|
||||
if processed_points.insert((x, z)) {
|
||||
// Create foundation columns for the floor area when using terrain
|
||||
@@ -587,7 +573,7 @@ pub fn generate_buildings(
|
||||
if x % 5 == 0 && z % 5 == 0 {
|
||||
// Light fixtures
|
||||
editor.set_block_absolute(
|
||||
ceiling_light_block,
|
||||
GLOWSTONE,
|
||||
x,
|
||||
h + abs_terrain_offset,
|
||||
z,
|
||||
@@ -607,7 +593,7 @@ pub fn generate_buildings(
|
||||
}
|
||||
} else if x % 5 == 0 && z % 5 == 0 {
|
||||
editor.set_block_absolute(
|
||||
ceiling_light_block,
|
||||
GLOWSTONE,
|
||||
x,
|
||||
start_y_offset + building_height + abs_terrain_offset,
|
||||
z,
|
||||
@@ -662,7 +648,6 @@ pub fn generate_buildings(
|
||||
args,
|
||||
element,
|
||||
abs_terrain_offset,
|
||||
is_abandoned_building,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -787,9 +772,6 @@ fn generate_roof(
|
||||
// Set base height for roof to be at least one block above building top
|
||||
let base_height = start_y_offset + building_height + 1;
|
||||
|
||||
// Optional OSM hint for ridge orientation
|
||||
let roof_orientation = element.tags.get("roof:orientation").map(|s| s.as_str());
|
||||
|
||||
match roof_type {
|
||||
RoofType::Flat => {
|
||||
// Simple flat roof
|
||||
@@ -816,13 +798,8 @@ fn generate_roof(
|
||||
let roof_peak_height = base_height + roof_height_boost;
|
||||
|
||||
// Pre-determine orientation and material
|
||||
let width_is_longer = width >= length;
|
||||
let ridge_runs_along_x = match roof_orientation {
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("along") => width_is_longer,
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("across") => !width_is_longer,
|
||||
_ => width_is_longer,
|
||||
};
|
||||
let max_distance = if ridge_runs_along_x {
|
||||
let is_wider_than_long = width > length;
|
||||
let max_distance = if is_wider_than_long {
|
||||
length >> 1
|
||||
} else {
|
||||
width >> 1
|
||||
@@ -842,15 +819,15 @@ fn generate_roof(
|
||||
|
||||
// First pass: calculate all roof heights using vectorized operations
|
||||
for &(x, z) in floor_area {
|
||||
let distance_to_ridge = if ridge_runs_along_x {
|
||||
let distance_to_ridge = if is_wider_than_long {
|
||||
(z - center_z).abs()
|
||||
} else {
|
||||
(x - center_x).abs()
|
||||
};
|
||||
|
||||
let roof_height = if distance_to_ridge == 0
|
||||
&& ((ridge_runs_along_x && z == center_z)
|
||||
|| (!ridge_runs_along_x && x == center_x))
|
||||
&& ((is_wider_than_long && z == center_z)
|
||||
|| (!is_wider_than_long && x == center_x))
|
||||
{
|
||||
roof_peak_height
|
||||
} else {
|
||||
@@ -882,7 +859,7 @@ fn generate_roof(
|
||||
for y in base_height..=roof_height {
|
||||
if y == roof_height && has_lower_neighbor {
|
||||
// Pre-compute stair direction
|
||||
let stair_block_with_props = if ridge_runs_along_x {
|
||||
let stair_block_with_props = if is_wider_than_long {
|
||||
if z < center_z {
|
||||
create_stair_with_properties(
|
||||
stair_block_material,
|
||||
@@ -942,12 +919,7 @@ fn generate_roof(
|
||||
// Determine if building is significantly rectangular or more square-shaped
|
||||
let is_rectangular =
|
||||
(width as f64 / length as f64 > 1.3) || (length as f64 / width as f64 > 1.3);
|
||||
let width_is_longer = width >= length;
|
||||
let ridge_axis_is_x = match roof_orientation {
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("along") => width_is_longer,
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("across") => !width_is_longer,
|
||||
_ => width_is_longer,
|
||||
};
|
||||
let long_axis_is_x = width > length;
|
||||
|
||||
// Make roof taller and more pointy
|
||||
let roof_peak_height = base_height + if width.max(length) > 20 { 7 } else { 5 };
|
||||
@@ -967,7 +939,7 @@ fn generate_roof(
|
||||
|
||||
for &(x, z) in floor_area {
|
||||
// Calculate distance to the ridge line
|
||||
let distance_to_ridge = if ridge_axis_is_x {
|
||||
let distance_to_ridge = if long_axis_is_x {
|
||||
// Distance in Z direction for X-axis ridge
|
||||
(z - center_z).abs()
|
||||
} else {
|
||||
@@ -976,7 +948,7 @@ fn generate_roof(
|
||||
};
|
||||
|
||||
// Calculate maximum distance from ridge to edge
|
||||
let max_distance_from_ridge = if ridge_axis_is_x {
|
||||
let max_distance_from_ridge = if long_axis_is_x {
|
||||
(max_z - min_z) / 2
|
||||
} else {
|
||||
(max_x - min_x) / 2
|
||||
@@ -1016,7 +988,7 @@ fn generate_roof(
|
||||
if has_lower_neighbor {
|
||||
// Determine stair direction based on ridge orientation and position
|
||||
let stair_block_material = get_stair_block_for_material(roof_block);
|
||||
let stair_block_with_props = if ridge_axis_is_x {
|
||||
let stair_block_with_props = if long_axis_is_x {
|
||||
// Ridge runs along X, slopes in Z direction
|
||||
if z < center_z {
|
||||
create_stair_with_properties(
|
||||
@@ -1553,8 +1525,6 @@ pub fn generate_building_from_relation(
|
||||
}
|
||||
|
||||
/// Generates a bridge structure, paying attention to the "level" tag.
|
||||
/// Bridge deck is interpolated between start and end point elevations to avoid
|
||||
/// being dragged down by valleys underneath.
|
||||
fn generate_bridge(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
@@ -1564,7 +1534,7 @@ fn generate_bridge(
|
||||
let floor_block: Block = STONE;
|
||||
let railing_block: Block = STONE_BRICKS;
|
||||
|
||||
// Calculate bridge level offset based on the "level" tag
|
||||
// Calculate bridge level based on the "level" tag (computed once, used throughout)
|
||||
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
|
||||
if let Ok(level) = level_str.parse::<i32>() {
|
||||
(level * 3) + 1
|
||||
@@ -1575,37 +1545,21 @@ fn generate_bridge(
|
||||
1 // Default elevation
|
||||
};
|
||||
|
||||
// Need at least 2 nodes to form a bridge
|
||||
if element.nodes.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get start and end node elevations and use MAX for level bridge deck
|
||||
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
|
||||
let start_node = &element.nodes[0];
|
||||
let end_node = &element.nodes[element.nodes.len() - 1];
|
||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||
let bridge_deck_ground_y = start_y.max(end_y);
|
||||
|
||||
// Process the nodes to create bridge pathways and railings
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
|
||||
for node in &element.nodes {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
// Create bridge path using Bresenham's line
|
||||
if let Some(prev) = previous_node {
|
||||
let bridge_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, 0, prev.1, x, 0, z);
|
||||
|
||||
for (bx, _, bz) in bridge_points.iter() {
|
||||
// Use fixed bridge deck height (max of endpoints)
|
||||
let bridge_y = bridge_deck_ground_y + bridge_y_offset;
|
||||
let bridge_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(prev.0, bridge_y_offset, prev.1, x, bridge_y_offset, z);
|
||||
|
||||
for (bx, by, bz) in bridge_points {
|
||||
// Place railing blocks
|
||||
editor.set_block_absolute(railing_block, *bx, bridge_y + 1, *bz, None, None);
|
||||
editor.set_block_absolute(railing_block, *bx, bridge_y, *bz, None, None);
|
||||
editor.set_block(railing_block, bx, by + 1, bz, None, None);
|
||||
editor.set_block(railing_block, bx, by, bz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1615,11 +1569,8 @@ fn generate_bridge(
|
||||
// Flood fill the area between the bridge path nodes (uses cache)
|
||||
let bridge_area: Vec<(i32, i32)> = flood_fill_cache.get_or_compute(element, floodfill_timeout);
|
||||
|
||||
// Use the same level bridge deck height for filled areas
|
||||
let floor_y = bridge_deck_ground_y + bridge_y_offset;
|
||||
|
||||
// Place floor blocks
|
||||
for (x, z) in bridge_area {
|
||||
editor.set_block_absolute(floor_block, x, floor_y, z, None, None);
|
||||
editor.set_block(floor_block, x, bridge_y_offset, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rayon::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Type alias for highway connectivity map
|
||||
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||
|
||||
/// Minimum terrain dip (in blocks) below max endpoint elevation to classify a bridge as valley-spanning
|
||||
const VALLEY_BRIDGE_THRESHOLD: i32 = 7;
|
||||
|
||||
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
||||
pub fn generate_highways(
|
||||
editor: &mut WorldEditor,
|
||||
@@ -31,39 +29,41 @@ pub fn generate_highways(
|
||||
}
|
||||
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||
/// Uses parallel processing for better performance on large element sets.
|
||||
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
|
||||
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
||||
// Parallel map phase: extract connectivity data from each highway element
|
||||
let partial_maps: Vec<Vec<((i32, i32), i32)>> = elements
|
||||
.par_iter()
|
||||
.filter_map(|element| {
|
||||
if let ProcessedElement::Way(way) = element {
|
||||
if way.tags.contains_key("highway") && !way.nodes.is_empty() {
|
||||
let layer_value = way
|
||||
.tags
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
for element in elements {
|
||||
if let ProcessedElement::Way(way) = element {
|
||||
if way.tags.contains_key("highway") {
|
||||
let layer_value = way
|
||||
.tags
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
// Treat negative layers as ground level (0) for connectivity
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
|
||||
// Treat negative layers as ground level (0) for connectivity
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
|
||||
// Add connectivity for start and end nodes
|
||||
if !way.nodes.is_empty() {
|
||||
let start_node = &way.nodes[0];
|
||||
let end_node = &way.nodes[way.nodes.len() - 1];
|
||||
|
||||
let start_coord = (start_node.x, start_node.z);
|
||||
let end_coord = (end_node.x, end_node.z);
|
||||
|
||||
connectivity_map
|
||||
.entry(start_coord)
|
||||
.or_default()
|
||||
.push(layer_value);
|
||||
connectivity_map
|
||||
.entry(end_coord)
|
||||
.or_default()
|
||||
.push(layer_value);
|
||||
return Some(vec![(start_coord, layer_value), (end_coord, layer_value)]);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sequential reduce phase: merge all partial results into final map
|
||||
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
||||
for entries in partial_maps {
|
||||
for (coord, layer) in entries {
|
||||
connectivity_map.entry(coord).or_default().push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,11 +163,6 @@ fn generate_highways_internal(
|
||||
let mut add_outline = false;
|
||||
let scale_factor = args.scale;
|
||||
|
||||
// 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");
|
||||
|
||||
// Parse the layer value for elevation calculation
|
||||
let layer_value = element
|
||||
.tags()
|
||||
@@ -257,7 +252,6 @@ fn generate_highways_internal(
|
||||
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
||||
|
||||
// Check if we need slopes at start and end
|
||||
// This is used for overpasses that need ramps to ground-level roads
|
||||
let needs_start_slope =
|
||||
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
||||
let needs_end_slope = should_add_slope_at_node(
|
||||
@@ -266,67 +260,10 @@ fn generate_highways_internal(
|
||||
highway_connectivity,
|
||||
);
|
||||
|
||||
// Calculate total way length for slope distribution (needed before valley bridge check)
|
||||
// Calculate total way length for slope distribution
|
||||
let total_way_length = calculate_way_length(way);
|
||||
|
||||
// For bridges: detect if this spans a valley by checking terrain profile
|
||||
// A valley bridge has terrain that dips significantly below the endpoints
|
||||
// Skip valley detection entirely if terrain is disabled (no valleys in flat terrain)
|
||||
// Skip very short bridges (< 25 blocks) as they're unlikely to span significant valleys
|
||||
let terrain_enabled = editor
|
||||
.get_ground()
|
||||
.map(|g| g.elevation_enabled)
|
||||
.unwrap_or(false);
|
||||
|
||||
let (is_valley_bridge, bridge_deck_y) =
|
||||
if is_bridge && terrain_enabled && way.nodes.len() >= 2 && total_way_length >= 25 {
|
||||
let start_node = &way.nodes[0];
|
||||
let end_node = &way.nodes[way.nodes.len() - 1];
|
||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||
let max_endpoint_y = start_y.max(end_y);
|
||||
|
||||
// Sample terrain at middle nodes only (excluding endpoints we already have)
|
||||
// This avoids redundant get_ground_level() calls
|
||||
let middle_nodes = &way.nodes[1..way.nodes.len().saturating_sub(1)];
|
||||
let sampled_min = if middle_nodes.is_empty() {
|
||||
// No middle nodes, just use endpoints
|
||||
start_y.min(end_y)
|
||||
} else {
|
||||
// Sample up to 3 middle points (5 total with endpoints) for performance
|
||||
// Valleys are wide terrain features, so sparse sampling is sufficient
|
||||
let sample_count = middle_nodes.len().min(3);
|
||||
let step = if sample_count > 1 {
|
||||
(middle_nodes.len() - 1) / (sample_count - 1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
middle_nodes
|
||||
.iter()
|
||||
.step_by(step.max(1))
|
||||
.map(|node| editor.get_ground_level(node.x, node.z))
|
||||
.min()
|
||||
.unwrap_or(max_endpoint_y)
|
||||
};
|
||||
|
||||
// Include endpoint elevations in the minimum calculation
|
||||
let min_terrain_y = sampled_min.min(start_y).min(end_y);
|
||||
|
||||
// If ANY sampled point along the bridge is significantly lower than the max endpoint,
|
||||
// treat as valley bridge
|
||||
let is_valley = min_terrain_y < max_endpoint_y - VALLEY_BRIDGE_THRESHOLD;
|
||||
|
||||
if is_valley {
|
||||
(true, max_endpoint_y)
|
||||
} else {
|
||||
(false, 0)
|
||||
}
|
||||
} else {
|
||||
(false, 0)
|
||||
};
|
||||
|
||||
// Check if this is a short isolated elevated segment (layer > 0), if so, treat as ground level
|
||||
// Check if this is a short isolated elevated segment - if so, treat as ground level
|
||||
let is_short_isolated_elevated =
|
||||
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
||||
|
||||
@@ -363,28 +300,17 @@ fn generate_highways_internal(
|
||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||
|
||||
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
||||
// Calculate Y elevation for this point
|
||||
// For valley bridges: use fixed deck height (max of endpoints) to stay level
|
||||
// For overpasses and regular roads: use terrain-relative elevation with slopes
|
||||
let (current_y, use_absolute_y) = if is_valley_bridge {
|
||||
// Valley bridge deck is level at the maximum endpoint elevation
|
||||
// Don't add base_elevation - the layer tag indicates it's above water/road,
|
||||
// not that it should be higher than the terrain endpoints
|
||||
(bridge_deck_y, true)
|
||||
} else {
|
||||
// Regular road or overpass: use terrain-relative calculation with ramps
|
||||
let y = calculate_point_elevation(
|
||||
segment_index,
|
||||
point_index,
|
||||
segment_length,
|
||||
total_segments,
|
||||
effective_elevation,
|
||||
effective_start_slope,
|
||||
effective_end_slope,
|
||||
slope_length,
|
||||
);
|
||||
(y, false)
|
||||
};
|
||||
// Calculate Y elevation for this point based on slopes and layer
|
||||
let current_y = calculate_point_elevation(
|
||||
segment_index,
|
||||
point_index,
|
||||
segment_length,
|
||||
total_segments,
|
||||
effective_elevation,
|
||||
effective_start_slope,
|
||||
effective_end_slope,
|
||||
slope_length,
|
||||
);
|
||||
|
||||
// Draw the road surface for the entire width
|
||||
for dx in -block_range..=block_range {
|
||||
@@ -400,32 +326,12 @@ fn generate_highways_internal(
|
||||
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
|
||||
if is_horizontal {
|
||||
if set_x % 2 < 1 {
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
BLACK_CONCRETE,
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
@@ -439,32 +345,12 @@ fn generate_highways_internal(
|
||||
);
|
||||
}
|
||||
} else if set_z % 2 < 1 {
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
BLACK_CONCRETE,
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
@@ -477,15 +363,6 @@ fn generate_highways_internal(
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
block_type,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
block_type,
|
||||
@@ -497,53 +374,30 @@ fn generate_highways_internal(
|
||||
);
|
||||
}
|
||||
|
||||
// Add stone brick foundation underneath elevated highways/bridges for thickness
|
||||
if (effective_elevation > 0 || use_absolute_y) && current_y > 0 {
|
||||
// Add stone brick foundation underneath elevated highways for thickness
|
||||
if effective_elevation > 0 && current_y > 0 {
|
||||
// Add 1 layer of stone bricks underneath the highway surface
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
editor.set_block(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Add support pillars for elevated highways/bridges
|
||||
if (effective_elevation != 0 || use_absolute_y) && current_y > 0 {
|
||||
if use_absolute_y {
|
||||
add_highway_support_pillar_absolute(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
} else {
|
||||
add_highway_support_pillar(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
}
|
||||
// Add support pillars for elevated highways
|
||||
if effective_elevation != 0 && current_y > 0 {
|
||||
add_highway_support_pillar(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,49 +408,27 @@ fn generate_highways_internal(
|
||||
for dz in -block_range..=block_range {
|
||||
let outline_x = x - block_range - 1;
|
||||
let outline_z = z + dz;
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Right outline
|
||||
for dz in -block_range..=block_range {
|
||||
let outline_x = x + block_range + 1;
|
||||
let outline_z = z + dz;
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,25 +437,14 @@ fn generate_highways_internal(
|
||||
if stripe_length < dash_length {
|
||||
let stripe_x: i32 = *x;
|
||||
let stripe_z: i32 = *z;
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Increment stripe_length and reset after completing a dash and gap
|
||||
@@ -767,46 +588,6 @@ fn add_highway_support_pillar(
|
||||
}
|
||||
}
|
||||
|
||||
/// Add support pillars for bridges using absolute Y coordinates
|
||||
/// Pillars extend from ground level up to the bridge deck
|
||||
fn add_highway_support_pillar_absolute(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
bridge_deck_y: i32,
|
||||
z: i32,
|
||||
dx: i32,
|
||||
dz: i32,
|
||||
_block_range: i32, // Keep for future use
|
||||
) {
|
||||
// Only add pillars at specific intervals and positions
|
||||
if dx == 0 && dz == 0 && (x + z) % 8 == 0 {
|
||||
// Get the actual ground level at this position
|
||||
let ground_y = editor.get_ground_level(x, z);
|
||||
|
||||
// Add pillar from ground up to bridge deck
|
||||
// Only if the bridge is actually above the ground
|
||||
if bridge_deck_y > ground_y {
|
||||
for y in (ground_y + 1)..bridge_deck_y {
|
||||
editor.set_block_absolute(STONE_BRICKS, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Add pillar base at ground level
|
||||
for base_dx in -1..=1 {
|
||||
for base_dz in -1..=1 {
|
||||
editor.set_block_absolute(
|
||||
STONE_BRICKS,
|
||||
x + base_dx,
|
||||
ground_y,
|
||||
z + base_dz,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a siding using stone brick slabs
|
||||
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
let mut previous_node: Option<XZPoint> = None;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::{Tree, TreeType};
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_landuse(
|
||||
@@ -13,15 +12,11 @@ pub fn generate_landuse(
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
// Determine block type based on landuse tag
|
||||
let binding: String = "".to_string();
|
||||
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
|
||||
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(element.id);
|
||||
|
||||
let block_type = match landuse_tag.as_str() {
|
||||
"greenfield" | "meadow" | "grass" | "orchard" | "forest" => GRASS_BLOCK,
|
||||
"farmland" => FARMLAND,
|
||||
@@ -33,13 +28,13 @@ pub fn generate_landuse(
|
||||
if residential_tag == "rural" {
|
||||
GRASS_BLOCK
|
||||
} else {
|
||||
STONE_BRICKS // Placeholder, will be randomized per-block
|
||||
STONE_BRICKS
|
||||
}
|
||||
}
|
||||
"commercial" => SMOOTH_STONE, // Placeholder, will be randomized per-block
|
||||
"commercial" => SMOOTH_STONE,
|
||||
"education" => POLISHED_ANDESITE,
|
||||
"religious" => POLISHED_ANDESITE,
|
||||
"industrial" => STONE, // Placeholder, will be randomized per-block
|
||||
"industrial" => COBBLESTONE,
|
||||
"military" => GRAY_CONCRETE,
|
||||
"railway" => GRAVEL,
|
||||
"landfill" => {
|
||||
@@ -59,75 +54,16 @@ pub fn generate_landuse(
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
let trees_ok_to_generate: Vec<TreeType> = {
|
||||
let mut trees: Vec<TreeType> = vec![];
|
||||
if let Some(leaf_type) = element.tags.get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
trees
|
||||
};
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(element.id);
|
||||
|
||||
for (x, z) in floor_area {
|
||||
// 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);
|
||||
if random_value < 72 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 87 {
|
||||
CRACKED_STONE_BRICKS
|
||||
} else if random_value < 92 {
|
||||
STONE
|
||||
} else {
|
||||
COBBLESTONE
|
||||
}
|
||||
} else if landuse_tag == "commercial" {
|
||||
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 40 {
|
||||
SMOOTH_STONE
|
||||
} else if random_value < 70 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 90 {
|
||||
STONE
|
||||
} else {
|
||||
COBBLESTONE
|
||||
}
|
||||
} else if landuse_tag == "industrial" {
|
||||
// Industrial: primarily stone, with some stone bricks and smooth stone
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 70 {
|
||||
STONE
|
||||
} else if random_value < 90 {
|
||||
STONE_BRICKS
|
||||
} else {
|
||||
SMOOTH_STONE
|
||||
}
|
||||
} else {
|
||||
block_type
|
||||
};
|
||||
|
||||
if landuse_tag == "traffic_island" {
|
||||
editor.set_block(actual_block, x, 1, z, None, None);
|
||||
editor.set_block(block_type, x, 1, z, None, None);
|
||||
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
||||
editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));
|
||||
editor.set_block(block_type, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else {
|
||||
editor.set_block(actual_block, x, 0, z, None, None);
|
||||
editor.set_block(block_type, x, 0, z, None, None);
|
||||
}
|
||||
|
||||
// Add specific features for different landuse types
|
||||
@@ -155,14 +91,9 @@ pub fn generate_landuse(
|
||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||
}
|
||||
} else if random_choice < 33 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if random_choice < 35 {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
} else if random_choice < 37 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else if random_choice < 41 {
|
||||
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
|
||||
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,31 +101,18 @@ pub fn generate_landuse(
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 20 {
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
Tree::create_of_type(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
tree_type,
|
||||
Some(building_footprints),
|
||||
);
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if random_choice == 2 {
|
||||
let flower_block: Block = match rng.gen_range(1..=6) {
|
||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
||||
1 => OAK_LEAVES,
|
||||
2 => RED_FLOWER,
|
||||
3 => BLUE_FLOWER,
|
||||
4 => YELLOW_FLOWER,
|
||||
5 => FERN,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if random_choice <= 12 {
|
||||
if rng.gen_range(0..100) < 12 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,8 +214,7 @@ pub fn generate_landuse(
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_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),
|
||||
1..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -306,8 +223,7 @@ pub fn generate_landuse(
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_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),
|
||||
1..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -316,29 +232,23 @@ pub fn generate_landuse(
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..1001);
|
||||
if random_choice < 5 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if random_choice < 6 {
|
||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||
} else if random_choice < 9 {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
} else if random_choice < 40 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else if random_choice < 65 {
|
||||
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
|
||||
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
|
||||
} else if random_choice < 825 {
|
||||
} else if random_choice < 800 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"orchard" => {
|
||||
if x % 18 == 0 && z % 10 == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_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),
|
||||
1..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -373,19 +283,12 @@ pub fn generate_landuse_from_relation(
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("landuse") {
|
||||
// Generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_landuse(
|
||||
editor,
|
||||
&member.way.clone(),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
generate_landuse(editor, &member.way.clone(), args, flood_fill_cache);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,13 +310,7 @@ pub fn generate_landuse_from_relation(
|
||||
};
|
||||
|
||||
// Generate landuse area from combined way
|
||||
generate_landuse(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
generate_landuse(editor, &combined_way, args, flood_fill_cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
@@ -13,7 +13,6 @@ pub fn generate_leisure(
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if let Some(leisure_type) = element.tags.get("leisure") {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
@@ -100,16 +99,14 @@ pub fn generate_leisure(
|
||||
|
||||
match random_choice {
|
||||
0..30 => {
|
||||
// Plants
|
||||
let plant_choice = match random_choice {
|
||||
0..5 => RED_FLOWER,
|
||||
5..10 => YELLOW_FLOWER,
|
||||
10..16 => BLUE_FLOWER,
|
||||
16..22 => WHITE_FLOWER,
|
||||
22..30 => FERN,
|
||||
_ => unreachable!(),
|
||||
// Flowers
|
||||
let flower_choice = match random_choice {
|
||||
0..10 => RED_FLOWER,
|
||||
10..20 => YELLOW_FLOWER,
|
||||
20..30 => BLUE_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(plant_choice, x, 1, z, None, None);
|
||||
editor.set_block(flower_choice, x, 1, z, None, None);
|
||||
}
|
||||
30..90 => {
|
||||
// Grass
|
||||
@@ -121,7 +118,7 @@ pub fn generate_leisure(
|
||||
}
|
||||
105..120 => {
|
||||
// Tree
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -182,19 +179,12 @@ pub fn generate_leisure_from_relation(
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
// First generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_leisure(
|
||||
editor,
|
||||
&member.way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
generate_leisure(editor, &member.way, args, flood_fill_cache);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,12 +204,6 @@ pub fn generate_leisure_from_relation(
|
||||
};
|
||||
|
||||
// Generate leisure area from combined way
|
||||
generate_leisure(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
generate_leisure(editor, &combined_way, args, flood_fill_cache);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod amenities;
|
||||
pub mod barriers;
|
||||
pub mod boundaries;
|
||||
pub mod bridges;
|
||||
pub mod buildings;
|
||||
pub mod doors;
|
||||
@@ -15,105 +14,3 @@ pub mod tourisms;
|
||||
pub mod tree;
|
||||
pub mod water_areas;
|
||||
pub mod waterways;
|
||||
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
|
||||
/// Merges way segments that share endpoints into closed rings.
|
||||
/// Used by water_areas.rs and boundaries.rs for assembling relation members.
|
||||
pub fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
||||
let mut removed: Vec<usize> = vec![];
|
||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||
|
||||
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
||||
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||
if a.id == b.id {
|
||||
return true;
|
||||
}
|
||||
let dx = (a.x - b.x).abs();
|
||||
let dz = (a.z - b.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
for i in 0..rings.len() {
|
||||
for j in 0..rings.len() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
|
||||
if removed.contains(&i) || removed.contains(&j) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x: &Vec<ProcessedNode> = &rings[i];
|
||||
let y: &Vec<ProcessedNode> = &rings[j];
|
||||
|
||||
// Skip empty rings (can happen after clipping)
|
||||
if x.is_empty() || y.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x_first = &x[0];
|
||||
let x_last = x.last().unwrap();
|
||||
let y_first = &y[0];
|
||||
let y_last = y.last().unwrap();
|
||||
|
||||
// Skip already-closed rings
|
||||
if nodes_match(x_first, x_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(y_first, y_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(x_first, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.reverse();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_last, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().rev().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_first, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut y: Vec<ProcessedNode> = y.clone();
|
||||
y.extend(x.iter().skip(1).cloned());
|
||||
|
||||
merged.push(y);
|
||||
} else if nodes_match(x_last, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removed.sort();
|
||||
|
||||
for r in removed.iter().rev() {
|
||||
rings.remove(*r);
|
||||
}
|
||||
|
||||
let merged_len: usize = merged.len();
|
||||
for m in merged {
|
||||
rings.push(m);
|
||||
}
|
||||
|
||||
if merged_len > 0 {
|
||||
merge_way_segments(rings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ 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::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_natural(
|
||||
@@ -14,7 +13,6 @@ pub fn generate_natural(
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if let Some(natural_type) = element.tags().get("natural") {
|
||||
if natural_type == "tree" {
|
||||
@@ -22,66 +20,7 @@ pub fn generate_natural(
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
let mut trees_ok_to_generate: Vec<TreeType> = vec![];
|
||||
if let Some(species) = element.tags().get("species") {
|
||||
if species.contains("Betula") {
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
if species.contains("Quercus") {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
}
|
||||
if species.contains("Picea") {
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
}
|
||||
} else if let Some(genus_wikidata) = element.tags().get("genus:wikidata") {
|
||||
match genus_wikidata.as_str() {
|
||||
"Q12004" => trees_ok_to_generate.push(TreeType::Birch),
|
||||
"Q26782" => trees_ok_to_generate.push(TreeType::Oak),
|
||||
"Q25243" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else if let Some(genus) = element.tags().get("genus") {
|
||||
match genus.as_str() {
|
||||
"Betula" => trees_ok_to_generate.push(TreeType::Birch),
|
||||
"Quercus" => trees_ok_to_generate.push(TreeType::Oak),
|
||||
"Picea" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => trees_ok_to_generate.push(TreeType::Oak),
|
||||
}
|
||||
} else if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
|
||||
if trees_ok_to_generate.is_empty() {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
|
||||
let mut rng = element_rng(element.id());
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
|
||||
Tree::create_of_type(editor, (x, 1, z), tree_type, Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z));
|
||||
}
|
||||
} else {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
@@ -141,29 +80,6 @@ pub fn generate_natural(
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
||||
|
||||
let trees_ok_to_generate: Vec<TreeType> = {
|
||||
let mut trees: Vec<TreeType> = vec![];
|
||||
if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
trees
|
||||
};
|
||||
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(way.id);
|
||||
|
||||
@@ -218,7 +134,7 @@ pub fn generate_natural(
|
||||
}
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
@@ -247,15 +163,7 @@ pub fn generate_natural(
|
||||
}
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 0 {
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
Tree::create_of_type(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
tree_type,
|
||||
Some(building_footprints),
|
||||
);
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
@@ -314,11 +222,7 @@ pub fn generate_natural(
|
||||
// TODO implement mangrove
|
||||
let random_choice: i32 = rng.gen_range(0..40);
|
||||
if random_choice == 0 {
|
||||
Tree::create(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
Some(building_footprints),
|
||||
);
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if random_choice < 35 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
@@ -402,7 +306,6 @@ pub fn generate_natural(
|
||||
Tree::create(
|
||||
editor,
|
||||
(cluster_x, 1, cluster_z),
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if vegetation_chance < 15 {
|
||||
// 15% chance for grass
|
||||
@@ -515,7 +418,7 @@ pub fn generate_natural(
|
||||
let hill_chance = rng.gen_range(0..1000);
|
||||
if hill_chance == 0 {
|
||||
// 0.1% chance for rare trees
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z));
|
||||
} else if hill_chance < 50 {
|
||||
// 5% chance for flowers
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
@@ -548,7 +451,6 @@ pub fn generate_natural_from_relation(
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("natural") {
|
||||
// Generate individual ways with their original tags
|
||||
@@ -559,7 +461,6 @@ pub fn generate_natural_from_relation(
|
||||
&ProcessedElement::Way((*member.way).clone()),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -587,7 +488,6 @@ pub fn generate_natural_from_relation(
|
||||
&ProcessedElement::Way(combined_way),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (1st layer above floor)
|
||||
/// Interior layout for building level floors (1nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
@@ -114,119 +114,6 @@ const INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
|
||||
];
|
||||
|
||||
// Generic Abandoned Building Interiors
|
||||
/// Interior layout for building ground floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR1_LAYER1: [[char; 23]; 23] = [
|
||||
['1', 'U', ' ', 'W', 'C', ' ', ' ', ' ', 'S', 'S', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', ' ', 'W',],
|
||||
['2', ' ', ' ', 'W', 'F', ' ', ' ', ' ', 'U', 'U', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', 'F', ' ', ' ', ' ', ' ', ' ', 'W', 'b', 'T', 'T', 'd', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'M', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'c', 'c', 'c', ' ', ' ', 'J', 'W', ' ', ' ', ' ', 'd', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', 'T', 'S', 'S', 'T', ' ', ' ', 'W', 'S', 'S', ' ', 'd', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'T', 'T', 'T', 'T', ' ', ' ', 'W', 'U', 'U', ' ', 'd', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', 'T', 'T', 'T', 'T', ' ', 'B', 'W', ' ', ' ', ' ', 'd', 'W', ' ', ' ',],
|
||||
['L', ' ', 'M', 'L', 'W', 'W', ' ', ' ', 'W', 'J', 'U', 'U', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['c', 'c', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', '6', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['U', '5', ' ', 'W', ' ', ' ', 'W', 'C', 'F', 'F', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', 'W', 'M', ' ', 'b', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'b', 'W', 'J', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'U', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
['J', ' ', ' ', 'C', 'a', 'a', 'W', 'L', 'F', ' ', 'W', 'F', ' ', 'W', 'L', 'W', '7', '8', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'd', 'W', 'W', 'W', 'W', 'W', ' ', 'W', 'M', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'C', ' ', 'W',],
|
||||
['B', ' ', ' ', 'd', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'C', ' ', ' ', 'W', 'W', 'c', 'c', 'c', 'c', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', 'C', ' ', ' ', ' ', 'W', 'W', 'W', 'b', 'T', 'T', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building ground floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||
[' ', 'P', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'P', 'P', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', 'P', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', ' ', 'P', 'P', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['P', ' ', ' ', 'W', ' ', ' ', 'W', 'N', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'C', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'P', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'B', 'B', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'P', 'W', ' ', ' ', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'N', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['U', ' ', ' ', ' ', ' ', ' ', 'C', 'W', 'L', ' ', ' ', 'L', 'W', 'M', 'M', 'W', ' ', ' ', ' ', ' ', ' ', 'L', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'S', 'S', 'S', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', 'C', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'J', ' ', 'U', 'U', 'U', ' ', 'D',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', 'T', 'T', 'W', ' ', ' ', ' ', ' ', ' ', 'U', 'W', ' ', 'L', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'T', 'J', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', 'L', ' ', 'W',],
|
||||
['J', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', 'M', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'U', ' ', ' ', ' ', 'D', ' ', ' ', 'F', 'F', 'W', 'M', 'M', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'U', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', 'C', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['C', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'L', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'U', 'U', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'U', 'U', ' ', 'Q', 'b', ' ', 'U', 'U', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['S', 'S', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||
['U', 'U', ' ', ' ', ' ', 'L', 'a', 'a', 'a', ' ', ' ', 'Q', 'B', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', 'D', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['P', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'O', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'O', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', 'P', 'P', ' ', 'D',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'P', 'W', ' ', 'P', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'P', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'O', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'P', 'P', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'P', 'P', ' ', 'Q', 'b', ' ', 'P', 'P', 'c', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||
['P', 'P', ' ', ' ', ' ', 'O', 'a', 'a', 'a', ' ', ' ', 'Q', 'b', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', ' ', 'D',],
|
||||
];
|
||||
|
||||
/// Maps interior layout characters to actual block types for different floor layers
|
||||
#[inline(always)]
|
||||
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
|
||||
@@ -258,19 +145,12 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
||||
Some(DARK_OAK_DOOR_LOWER)
|
||||
}
|
||||
}
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||
'O' => Some(COBWEB), // Cobweb
|
||||
'a' => Some(CHISELLED_BOOKSHELF_NORTH), // Chiseled Bookshelf
|
||||
'b' => Some(CHISELLED_BOOKSHELF_EAST), // Chiseled Bookshelf East
|
||||
'c' => Some(CHISELLED_BOOKSHELF_SOUTH), // Chiseled Bookshelf South
|
||||
'd' => Some(CHISELLED_BOOKSHELF_WEST), // Chiseled Bookshelf West
|
||||
'M' => Some(DAMAGED_ANVIL), // Damaged Anvil
|
||||
'Q' => Some(SCAFFOLDING), // Scaffolding
|
||||
_ => None, // Default case for unknown characters
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||
_ => None, // Default case for unknown characters
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +170,6 @@ pub fn generate_building_interior(
|
||||
args: &crate::args::Args,
|
||||
element: &crate::osm_parser::ProcessedWay,
|
||||
abs_terrain_offset: i32,
|
||||
is_abandoned_building: bool,
|
||||
) {
|
||||
// Skip interior generation for very small buildings
|
||||
let width = max_x - min_x + 1;
|
||||
@@ -335,13 +214,7 @@ pub fn generate_building_interior(
|
||||
};
|
||||
|
||||
// Choose the appropriate interior pattern based on floor number
|
||||
let (layer1, layer2) = if is_abandoned_building {
|
||||
if floor_index == 0 {
|
||||
(&ABANDONED_INTERIOR1_LAYER1, &ABANDONED_INTERIOR1_LAYER2)
|
||||
} else {
|
||||
(&ABANDONED_INTERIOR2_LAYER1, &ABANDONED_INTERIOR2_LAYER2)
|
||||
}
|
||||
} else if floor_index == 0 {
|
||||
let (layer1, layer2) = if floor_index == 0 {
|
||||
// Ground floor uses INTERIOR1 patterns
|
||||
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::coord_rng;
|
||||
use crate::floodfill_cache::BuildingFootprintBitmap;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
@@ -92,7 +91,6 @@ fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_patt
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TreeType {
|
||||
Oak,
|
||||
Spruce,
|
||||
@@ -110,46 +108,7 @@ pub struct Tree<'a> {
|
||||
}
|
||||
|
||||
impl Tree<'_> {
|
||||
/// Creates a tree at the specified coordinates.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `editor` - The world editor to place blocks
|
||||
/// * `(x, y, z)` - The base coordinates for the tree
|
||||
/// * `building_footprints` - Optional bitmap of (x, z) coordinates that are inside buildings.
|
||||
/// If provided, trees will not be placed at coordinates within this bitmap.
|
||||
pub fn create(
|
||||
editor: &mut WorldEditor,
|
||||
(x, y, z): Coord,
|
||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||
) {
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree_type = match rng.gen_range(1..=3) {
|
||||
1 => TreeType::Oak,
|
||||
2 => TreeType::Spruce,
|
||||
3 => TreeType::Birch,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Self::create_of_type(editor, (x, y, z), tree_type, building_footprints);
|
||||
}
|
||||
|
||||
/// Creates a tree of a specific type at the specified coordinates.
|
||||
pub fn create_of_type(
|
||||
editor: &mut WorldEditor,
|
||||
(x, y, z): Coord,
|
||||
tree_type: TreeType,
|
||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||
) {
|
||||
// Skip if this coordinate is inside a building
|
||||
if let Some(footprints) = building_footprints {
|
||||
if footprints.contains(x, z) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(editor: &mut WorldEditor, (x, y, z): Coord) {
|
||||
let mut blacklist: Vec<Block> = Vec::new();
|
||||
blacklist.extend(Self::get_building_wall_blocks());
|
||||
blacklist.extend(Self::get_building_floor_blocks());
|
||||
@@ -157,7 +116,16 @@ impl Tree<'_> {
|
||||
blacklist.extend(Self::get_functional_blocks());
|
||||
blacklist.push(WATER);
|
||||
|
||||
let tree = Self::get_tree(tree_type);
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
||||
1 => TreeType::Oak,
|
||||
2 => TreeType::Spruce,
|
||||
3 => TreeType::Birch,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
|
||||
// Build the logs
|
||||
editor.fill_blocks(
|
||||
|
||||
@@ -58,14 +58,14 @@ pub fn generate_water_areas_from_relation(
|
||||
}
|
||||
|
||||
// Preserve OSM-defined outer/inner roles without modification
|
||||
super::merge_way_segments(&mut outers);
|
||||
merge_way_segments(&mut outers);
|
||||
|
||||
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
|
||||
outers = outers
|
||||
.into_iter()
|
||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||
.collect();
|
||||
super::merge_way_segments(&mut inners);
|
||||
merge_way_segments(&mut inners);
|
||||
inners = inners
|
||||
.into_iter()
|
||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||
@@ -112,7 +112,7 @@ pub fn generate_water_areas_from_relation(
|
||||
}
|
||||
}
|
||||
|
||||
super::merge_way_segments(&mut inners);
|
||||
merge_way_segments(&mut inners);
|
||||
if !verify_closed_rings(&inners) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
@@ -166,6 +166,105 @@ fn generate_water_areas(
|
||||
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
|
||||
}
|
||||
|
||||
/// Merges way segments that share endpoints into closed rings.
|
||||
fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
||||
let mut removed: Vec<usize> = vec![];
|
||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||
|
||||
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
||||
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||
if a.id == b.id {
|
||||
return true;
|
||||
}
|
||||
let dx = (a.x - b.x).abs();
|
||||
let dz = (a.z - b.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
for i in 0..rings.len() {
|
||||
for j in 0..rings.len() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
|
||||
if removed.contains(&i) || removed.contains(&j) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x: &Vec<ProcessedNode> = &rings[i];
|
||||
let y: &Vec<ProcessedNode> = &rings[j];
|
||||
|
||||
// Skip empty rings (can happen after clipping)
|
||||
if x.is_empty() || y.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x_first = &x[0];
|
||||
let x_last = x.last().unwrap();
|
||||
let y_first = &y[0];
|
||||
let y_last = y.last().unwrap();
|
||||
|
||||
// Skip already-closed rings
|
||||
if nodes_match(x_first, x_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(y_first, y_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(x_first, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.reverse();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_last, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().rev().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_first, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut y: Vec<ProcessedNode> = y.clone();
|
||||
y.extend(x.iter().skip(1).cloned());
|
||||
|
||||
merged.push(y);
|
||||
} else if nodes_match(x_last, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removed.sort();
|
||||
|
||||
for r in removed.iter().rev() {
|
||||
rings.remove(*r);
|
||||
}
|
||||
|
||||
let merged_len: usize = merged.len();
|
||||
for m in merged {
|
||||
rings.push(m);
|
||||
}
|
||||
|
||||
if merged_len > 0 {
|
||||
merge_way_segments(rings);
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies all rings are properly closed (first node matches last).
|
||||
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
||||
let mut valid = true;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use crate::{
|
||||
coordinate_system::{geographic::LLBBox, transformation::geo_distance},
|
||||
progress::emit_gui_progress_update,
|
||||
};
|
||||
use image::Rgb;
|
||||
use rayon::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Maximum Y coordinate in Minecraft (build height limit)
|
||||
const MAX_Y: i32 = 319;
|
||||
/// Scale factor for converting real elevation to Minecraft heights
|
||||
const BASE_HEIGHT_SCALE: f64 = 0.7;
|
||||
/// AWS S3 Terrarium tiles endpoint (no API key required)
|
||||
const AWS_TERRARIUM_URL: &str =
|
||||
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
|
||||
@@ -21,8 +20,6 @@ const MIN_ZOOM: u8 = 10;
|
||||
const MAX_ZOOM: u8 = 15;
|
||||
/// Maximum concurrent tile downloads to be respectful to AWS
|
||||
const MAX_CONCURRENT_DOWNLOADS: usize = 8;
|
||||
/// Maximum age for cached tiles in days before they are cleaned up
|
||||
const TILE_CACHE_MAX_AGE_DAYS: u64 = 7;
|
||||
|
||||
/// Holds processed elevation data and metadata
|
||||
#[derive(Clone)]
|
||||
@@ -40,88 +37,6 @@ type TileImage = image::ImageBuffer<Rgb<u8>, Vec<u8>>;
|
||||
/// Result type for tile download operations: ((tile_x, tile_y), image) or error
|
||||
type TileDownloadResult = Result<((u32, u32), TileImage), String>;
|
||||
|
||||
/// Cleans up old cached tiles from the tile cache directory.
|
||||
/// Only deletes .png files within the arnis-tile-cache directory that are older than TILE_CACHE_MAX_AGE_DAYS.
|
||||
/// This function is safe and will not delete files outside the cache directory or fail on errors.
|
||||
pub fn cleanup_old_cached_tiles() {
|
||||
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
|
||||
|
||||
if !tile_cache_dir.exists() || !tile_cache_dir.is_dir() {
|
||||
return; // Nothing to clean up
|
||||
}
|
||||
|
||||
let max_age = std::time::Duration::from_secs(TILE_CACHE_MAX_AGE_DAYS * 24 * 60 * 60);
|
||||
let now = std::time::SystemTime::now();
|
||||
let mut deleted_count = 0;
|
||||
let mut error_count = 0;
|
||||
|
||||
// Read directory entries
|
||||
let entries = match std::fs::read_dir(&tile_cache_dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Safety check: only process .png files within the cache directory
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify the file is a .png and follows our naming pattern (z{zoom}_x{x}_y{y}.png)
|
||||
let file_name = match path.file_name().and_then(|n| n.to_str()) {
|
||||
Some(name) => name,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if !file_name.ends_with(".png") || !file_name.starts_with('z') {
|
||||
continue; // Skip files that don't match our tile naming pattern
|
||||
}
|
||||
|
||||
// Check file age
|
||||
let metadata = match std::fs::metadata(&path) {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let modified = match metadata.modified() {
|
||||
Ok(time) => time,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let age = match now.duration_since(modified) {
|
||||
Ok(duration) => duration,
|
||||
Err(_) => continue, // File modified in the future? Skip it.
|
||||
};
|
||||
|
||||
if age > max_age {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => deleted_count += 1,
|
||||
Err(e) => {
|
||||
// Log but don't fail, this is a best-effort cleanup
|
||||
if error_count == 0 {
|
||||
eprintln!(
|
||||
"Warning: Failed to delete old cached tile {}: {e}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if deleted_count > 0 {
|
||||
println!("Cleaned up {deleted_count} old cached elevation tiles (older than {TILE_CACHE_MAX_AGE_DAYS} days)");
|
||||
}
|
||||
if error_count > 1 {
|
||||
eprintln!("Warning: Failed to delete {error_count} old cached tiles");
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates appropriate zoom level for the given bounding box
|
||||
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
|
||||
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
|
||||
@@ -139,13 +54,7 @@ fn lat_lng_to_tile(lat: f64, lng: f64, zoom: u8) -> (u32, u32) {
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// Maximum number of retry attempts for tile downloads
|
||||
const TILE_DOWNLOAD_MAX_RETRIES: u32 = 3;
|
||||
|
||||
/// Base delay in milliseconds for exponential backoff between retries
|
||||
const TILE_DOWNLOAD_RETRY_BASE_DELAY_MS: u64 = 500;
|
||||
|
||||
/// Downloads a tile from AWS Terrain Tiles service with retry logic
|
||||
/// Downloads a tile from AWS Terrain Tiles service
|
||||
fn download_tile(
|
||||
client: &reqwest::blocking::Client,
|
||||
tile_x: u32,
|
||||
@@ -159,51 +68,7 @@ fn download_tile(
|
||||
.replace("{x}", &tile_x.to_string())
|
||||
.replace("{y}", &tile_y.to_string());
|
||||
|
||||
let mut last_error: String = String::new();
|
||||
|
||||
for attempt in 0..TILE_DOWNLOAD_MAX_RETRIES {
|
||||
if attempt > 0 {
|
||||
// Exponential backoff: 500ms, 1000ms, 2000ms...
|
||||
let delay_ms = TILE_DOWNLOAD_RETRY_BASE_DELAY_MS * (1 << (attempt - 1));
|
||||
eprintln!(
|
||||
"Retry attempt {}/{} for tile x={},y={},z={} after {}ms delay",
|
||||
attempt,
|
||||
TILE_DOWNLOAD_MAX_RETRIES - 1,
|
||||
tile_x,
|
||||
tile_y,
|
||||
zoom,
|
||||
delay_ms
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
|
||||
}
|
||||
|
||||
match download_tile_once(client, &url, tile_path) {
|
||||
Ok(img) => return Ok(img),
|
||||
Err(e) => {
|
||||
last_error = e;
|
||||
if attempt < TILE_DOWNLOAD_MAX_RETRIES - 1 {
|
||||
eprintln!(
|
||||
"Tile download failed for x={},y={},z={}: {}",
|
||||
tile_x, tile_y, zoom, last_error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Failed to download tile x={},y={},z={} after {} attempts: {}",
|
||||
tile_x, tile_y, zoom, TILE_DOWNLOAD_MAX_RETRIES, last_error
|
||||
))
|
||||
}
|
||||
|
||||
/// Single download attempt for a tile (no retries)
|
||||
fn download_tile_once(
|
||||
client: &reqwest::blocking::Client,
|
||||
url: &str,
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||
let response = client.get(url).send().map_err(|e| e.to_string())?;
|
||||
let response = client.get(&url).send().map_err(|e| e.to_string())?;
|
||||
response.error_for_status_ref().map_err(|e| e.to_string())?;
|
||||
let bytes = response.bytes().map_err(|e| e.to_string())?;
|
||||
std::fs::write(tile_path, &bytes).map_err(|e| e.to_string())?;
|
||||
@@ -223,6 +88,35 @@ fn fetch_or_load_tile(
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||
if tile_path.exists() {
|
||||
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||
let file_size = std::fs::metadata(tile_path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
if file_size < 1000 {
|
||||
eprintln!(
|
||||
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(),
|
||||
file_size
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile appears too small, refetching.",
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(e) = std::fs::remove_file(tile_path) {
|
||||
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during refetching.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
return download_tile(client, tile_x, tile_y, zoom, tile_path);
|
||||
}
|
||||
|
||||
// Try to load cached tile, but handle corruption gracefully
|
||||
match image::open(tile_path) {
|
||||
Ok(img) => {
|
||||
@@ -336,7 +230,6 @@ pub fn fetch_elevation_data(
|
||||
}
|
||||
|
||||
println!("Processing {} elevation tiles...", successful_tiles.len());
|
||||
emit_gui_progress_update(15.0, "Processing elevation...");
|
||||
|
||||
// Process tiles sequentially (writes to shared height_grid)
|
||||
for ((tile_x, tile_y), rgb_img) in successful_tiles {
|
||||
@@ -425,11 +318,16 @@ pub fn fetch_elevation_data(
|
||||
// This smooths terrain proportionally while preserving more detail.
|
||||
let sigma: f64 = BASE_SIGMA_REF * (grid_size / BASE_GRID_REF).sqrt();
|
||||
|
||||
//let blur_percentage: f64 = (sigma / grid_size) * 100.0;
|
||||
/*eprintln!(
|
||||
let blur_percentage: f64 = (sigma / grid_size) * 100.0;
|
||||
eprintln!(
|
||||
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
|
||||
grid_width, grid_height, sigma, blur_percentage
|
||||
);*/
|
||||
);
|
||||
|
||||
/* eprintln!(
|
||||
"Grid: {}x{}, Blur sigma: {:.2}",
|
||||
grid_width, grid_height, sigma
|
||||
); */
|
||||
|
||||
// Continue with the existing blur and conversion to Minecraft heights...
|
||||
let blurred_heights: Vec<Vec<f64>> = apply_gaussian_blur(&height_grid, sigma);
|
||||
@@ -437,34 +335,30 @@ pub fn fetch_elevation_data(
|
||||
// Release raw height grid
|
||||
drop(height_grid);
|
||||
|
||||
// Find min/max in raw data using parallel reduction
|
||||
let (min_height, max_height, extreme_low_count, extreme_high_count) = blurred_heights
|
||||
.par_iter()
|
||||
.map(|row| {
|
||||
let mut local_min = f64::MAX;
|
||||
let mut local_max = f64::MIN;
|
||||
let mut local_low = 0usize;
|
||||
let mut local_high = 0usize;
|
||||
for &height in row {
|
||||
local_min = local_min.min(height);
|
||||
local_max = local_max.max(height);
|
||||
if height < -1000.0 {
|
||||
local_low += 1;
|
||||
}
|
||||
if height > 10000.0 {
|
||||
local_high += 1;
|
||||
}
|
||||
}
|
||||
(local_min, local_max, local_low, local_high)
|
||||
})
|
||||
.reduce(
|
||||
|| (f64::MAX, f64::MIN, 0usize, 0usize),
|
||||
|(min1, max1, low1, high1), (min2, max2, low2, high2)| {
|
||||
(min1.min(min2), max1.max(max2), low1 + low2, high1 + high2)
|
||||
},
|
||||
);
|
||||
let mut mc_heights: Vec<Vec<i32>> = Vec::with_capacity(blurred_heights.len());
|
||||
|
||||
//eprintln!("Height data range: {min_height} to {max_height} m");
|
||||
// Find min/max in raw data
|
||||
let mut min_height: f64 = f64::MAX;
|
||||
let mut max_height: f64 = f64::MIN;
|
||||
let mut extreme_low_count = 0;
|
||||
let mut extreme_high_count = 0;
|
||||
|
||||
for row in &blurred_heights {
|
||||
for &height in row {
|
||||
min_height = min_height.min(height);
|
||||
max_height = max_height.max(height);
|
||||
|
||||
// Count extreme values that might indicate data issues
|
||||
if height < -1000.0 {
|
||||
extreme_low_count += 1;
|
||||
}
|
||||
if height > 10000.0 {
|
||||
extreme_high_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("Height data range: {min_height} to {max_height} m");
|
||||
if extreme_low_count > 0 {
|
||||
eprintln!(
|
||||
"WARNING: Found {extreme_low_count} pixels with extremely low elevations (< -1000m)"
|
||||
@@ -477,63 +371,39 @@ pub fn fetch_elevation_data(
|
||||
}
|
||||
|
||||
let height_range: f64 = max_height - min_height;
|
||||
// Apply scale factor to height scaling
|
||||
let mut height_scale: f64 = BASE_HEIGHT_SCALE * scale.sqrt(); // sqrt to make height scaling less extreme
|
||||
let mut scaled_range: f64 = height_range * height_scale;
|
||||
|
||||
// Realistic height scaling: 1 meter of real elevation = scale blocks in Minecraft
|
||||
// At scale=1.0, 1 meter = 1 block (realistic 1:1 mapping)
|
||||
// At scale=2.0, 1 meter = 2 blocks (exaggerated for larger worlds)
|
||||
let ideal_scaled_range: f64 = height_range * scale;
|
||||
// Adaptive scaling: ensure we don't exceed reasonable Y range
|
||||
let available_y_range = (MAX_Y - ground_level) as f64;
|
||||
let safety_margin = 0.9; // Use 90% of available range
|
||||
let max_allowed_range = available_y_range * safety_margin;
|
||||
|
||||
// Calculate available Y range in Minecraft (from ground_level to MAX_Y)
|
||||
// Leave a buffer at the top for buildings, trees, and other structures
|
||||
const TERRAIN_HEIGHT_BUFFER: i32 = 15;
|
||||
let available_y_range: f64 = (MAX_Y - TERRAIN_HEIGHT_BUFFER - ground_level) as f64;
|
||||
|
||||
// Determine final height scale:
|
||||
// - Use realistic 1:1 (times scale) if terrain fits within Minecraft limits
|
||||
// - Only compress if the terrain would exceed the build height
|
||||
let scaled_range: f64 = if ideal_scaled_range <= available_y_range {
|
||||
// Terrain fits! Use realistic scaling
|
||||
if scaled_range > max_allowed_range {
|
||||
let adjustment_factor = max_allowed_range / scaled_range;
|
||||
height_scale *= adjustment_factor;
|
||||
scaled_range = height_range * height_scale;
|
||||
eprintln!(
|
||||
"Realistic elevation: {:.1}m range fits in {} available blocks",
|
||||
height_range, available_y_range as i32
|
||||
"Height range too large, applying scaling adjustment factor: {adjustment_factor:.3}"
|
||||
);
|
||||
ideal_scaled_range
|
||||
} else {
|
||||
// Terrain too tall, compress to fit within Minecraft limits
|
||||
let compression_factor: f64 = available_y_range / height_range;
|
||||
let compressed_range: f64 = height_range * compression_factor;
|
||||
eprintln!(
|
||||
"Elevation compressed: {:.1}m range -> {:.0} blocks ({:.2}:1 ratio, 1 block = {:.2}m)",
|
||||
height_range,
|
||||
compressed_range,
|
||||
height_range / compressed_range,
|
||||
compressed_range / height_range
|
||||
);
|
||||
compressed_range
|
||||
};
|
||||
eprintln!("Adjusted scaled range: {scaled_range:.1} blocks");
|
||||
}
|
||||
|
||||
// Convert to scaled Minecraft Y coordinates (parallelized across rows)
|
||||
// Lowest real elevation maps to ground_level, highest maps to ground_level + scaled_range
|
||||
let mc_heights: Vec<Vec<i32>> = blurred_heights
|
||||
.par_iter()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|&h| {
|
||||
// Calculate relative position within the elevation range (0.0 to 1.0)
|
||||
let relative_height: f64 = if height_range > 0.0 {
|
||||
(h - min_height) / height_range
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Scale to Minecraft blocks and add to ground level
|
||||
let scaled_height: f64 = relative_height * scaled_range;
|
||||
// Clamp to valid Minecraft Y range (leave buffer at top for structures)
|
||||
((ground_level as f64 + scaled_height).round() as i32)
|
||||
.clamp(ground_level, MAX_Y - TERRAIN_HEIGHT_BUFFER)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
// Convert to scaled Minecraft Y coordinates
|
||||
for row in blurred_heights {
|
||||
let mc_row: Vec<i32> = row
|
||||
.iter()
|
||||
.map(|&h| {
|
||||
// Scale the height differences
|
||||
let relative_height: f64 = (h - min_height) / height_range;
|
||||
let scaled_height: f64 = relative_height * scaled_range;
|
||||
// With terrain enabled, ground_level is used as the MIN_Y for terrain
|
||||
((ground_level as f64 + scaled_height).round() as i32).clamp(ground_level, MAX_Y)
|
||||
})
|
||||
.collect();
|
||||
mc_heights.push(mc_row);
|
||||
}
|
||||
|
||||
let mut min_block_height: i32 = i32::MAX;
|
||||
let mut max_block_height: i32 = i32::MIN;
|
||||
@@ -543,7 +413,7 @@ pub fn fetch_elevation_data(
|
||||
max_block_height = max_block_height.max(height);
|
||||
}
|
||||
}
|
||||
//eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
|
||||
eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
|
||||
|
||||
Ok(ElevationData {
|
||||
heights: mc_heights,
|
||||
@@ -570,61 +440,48 @@ fn apply_gaussian_blur(heights: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
|
||||
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
|
||||
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
|
||||
|
||||
let height_len = heights.len();
|
||||
let width = heights[0].len();
|
||||
// Apply blur
|
||||
let mut blurred: Vec<Vec<f64>> = heights.to_owned();
|
||||
|
||||
// Horizontal pass - parallelize across rows (each row is independent)
|
||||
let after_horizontal: Vec<Vec<f64>> = heights
|
||||
.par_iter()
|
||||
.map(|row| {
|
||||
let mut temp: Vec<f64> = vec![0.0; row.len()];
|
||||
for (i, val) in temp.iter_mut().enumerate() {
|
||||
let mut sum: f64 = 0.0;
|
||||
let mut weight_sum: f64 = 0.0;
|
||||
for (j, k) in kernel.iter().enumerate() {
|
||||
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
|
||||
if idx >= 0 && idx < row.len() as i32 {
|
||||
sum += row[idx as usize] * k;
|
||||
weight_sum += k;
|
||||
}
|
||||
// Horizontal pass
|
||||
for row in blurred.iter_mut() {
|
||||
let mut temp: Vec<f64> = row.clone();
|
||||
for (i, val) in temp.iter_mut().enumerate() {
|
||||
let mut sum: f64 = 0.0;
|
||||
let mut weight_sum: f64 = 0.0;
|
||||
for (j, k) in kernel.iter().enumerate() {
|
||||
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
|
||||
if idx >= 0 && idx < row.len() as i32 {
|
||||
sum += row[idx as usize] * k;
|
||||
weight_sum += k;
|
||||
}
|
||||
*val = sum / weight_sum;
|
||||
}
|
||||
temp
|
||||
})
|
||||
.collect();
|
||||
*val = sum / weight_sum;
|
||||
}
|
||||
*row = temp;
|
||||
}
|
||||
|
||||
// Vertical pass - parallelize across columns (each column is independent)
|
||||
// Process each column in parallel and collect results as column vectors
|
||||
let blurred_columns: Vec<Vec<f64>> = (0..width)
|
||||
.into_par_iter()
|
||||
.map(|x| {
|
||||
// Extract column from after_horizontal
|
||||
let column: Vec<f64> = after_horizontal.iter().map(|row| row[x]).collect();
|
||||
// Vertical pass
|
||||
let height: usize = blurred.len();
|
||||
let width: usize = blurred[0].len();
|
||||
for x in 0..width {
|
||||
let temp: Vec<_> = blurred
|
||||
.iter()
|
||||
.take(height)
|
||||
.map(|row: &Vec<f64>| row[x])
|
||||
.collect();
|
||||
|
||||
// Apply vertical blur to this column
|
||||
let mut blurred_column: Vec<f64> = vec![0.0; height_len];
|
||||
for (y, val) in blurred_column.iter_mut().enumerate() {
|
||||
let mut sum: f64 = 0.0;
|
||||
let mut weight_sum: f64 = 0.0;
|
||||
for (j, k) in kernel.iter().enumerate() {
|
||||
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
|
||||
if idx >= 0 && idx < height_len as i32 {
|
||||
sum += column[idx as usize] * k;
|
||||
weight_sum += k;
|
||||
}
|
||||
for (y, row) in blurred.iter_mut().enumerate().take(height) {
|
||||
let mut sum: f64 = 0.0;
|
||||
let mut weight_sum: f64 = 0.0;
|
||||
for (j, k) in kernel.iter().enumerate() {
|
||||
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
|
||||
if idx >= 0 && idx < height as i32 {
|
||||
sum += temp[idx as usize] * k;
|
||||
weight_sum += k;
|
||||
}
|
||||
*val = sum / weight_sum;
|
||||
}
|
||||
blurred_column
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Transpose columns back to row-major format
|
||||
let mut blurred: Vec<Vec<f64>> = vec![vec![0.0; width]; height_len];
|
||||
for (x, column) in blurred_columns.into_iter().enumerate() {
|
||||
for (y, val) in column.into_iter().enumerate() {
|
||||
blurred[y][x] = val;
|
||||
row[x] = sum / weight_sum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,24 +563,17 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort to find percentiles
|
||||
all_heights.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let len = all_heights.len();
|
||||
|
||||
// Use 1st and 99th percentiles to define reasonable bounds
|
||||
// Using quickselect (select_nth_unstable) instead of full sort: O(n) vs O(n log n)
|
||||
let p1_idx = (len as f64 * 0.01) as usize;
|
||||
let p99_idx = ((len as f64 * 0.99) as usize).min(len - 1);
|
||||
let p99_idx = (len as f64 * 0.99) as usize;
|
||||
let min_reasonable = all_heights[p1_idx];
|
||||
let max_reasonable = all_heights[p99_idx];
|
||||
|
||||
// Find p1 (1st percentile) - all elements before p1_idx will be <= p1
|
||||
let (_, p1_val, _) =
|
||||
all_heights.select_nth_unstable_by(p1_idx, |a, b| a.partial_cmp(b).unwrap());
|
||||
let min_reasonable = *p1_val;
|
||||
|
||||
// Find p99 (99th percentile) - need to search in remaining slice or use separate call
|
||||
let (_, p99_val, _) =
|
||||
all_heights.select_nth_unstable_by(p99_idx, |a, b| a.partial_cmp(b).unwrap());
|
||||
let max_reasonable = *p99_val;
|
||||
|
||||
//eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
||||
eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
||||
|
||||
let mut outliers_filtered = 0;
|
||||
|
||||
@@ -738,7 +588,7 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
|
||||
}
|
||||
|
||||
if outliers_filtered > 0 {
|
||||
//eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
|
||||
eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
|
||||
// Re-run the NaN filling to interpolate the filtered values
|
||||
fill_nan_values(height_grid);
|
||||
}
|
||||
|
||||
@@ -4,119 +4,12 @@
|
||||
//! before the main element processing loop, then retrieve cached results during
|
||||
//! sequential processing.
|
||||
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedWay};
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use fnv::FnvHashMap;
|
||||
use rayon::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A memory-efficient bitmap for storing building footprint coordinates.
|
||||
///
|
||||
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
|
||||
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
|
||||
///
|
||||
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
|
||||
pub struct BuildingFootprintBitmap {
|
||||
/// The bitmap data, where each bit represents one (x, z) coordinate
|
||||
bits: Vec<u8>,
|
||||
/// Minimum x coordinate (offset for indexing)
|
||||
min_x: i32,
|
||||
/// Minimum z coordinate (offset for indexing)
|
||||
min_z: i32,
|
||||
/// Width of the world (max_x - min_x + 1)
|
||||
width: usize,
|
||||
/// Height of the world (max_z - min_z + 1)
|
||||
height: usize,
|
||||
/// Number of coordinates marked as building footprints
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl BuildingFootprintBitmap {
|
||||
/// Creates a new empty bitmap covering the given world bounds.
|
||||
pub fn new(xzbbox: &XZBBox) -> Self {
|
||||
let min_x = xzbbox.min_x();
|
||||
let min_z = xzbbox.min_z();
|
||||
// Use i64 to avoid overflow when world spans more than i32::MAX in either dimension
|
||||
let width = (i64::from(xzbbox.max_x()) - i64::from(min_x) + 1) as usize;
|
||||
let height = (i64::from(xzbbox.max_z()) - i64::from(min_z) + 1) as usize;
|
||||
|
||||
// Calculate number of bytes needed (round up to nearest byte)
|
||||
let total_bits = width
|
||||
.checked_mul(height)
|
||||
.expect("BuildingFootprintBitmap: world size too large (width * height overflowed)");
|
||||
let num_bytes = total_bits.div_ceil(8);
|
||||
|
||||
Self {
|
||||
bits: vec![0u8; num_bytes],
|
||||
min_x,
|
||||
min_z,
|
||||
width,
|
||||
height,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts (x, z) coordinate to bit index, returning None if out of bounds.
|
||||
#[inline]
|
||||
fn coord_to_index(&self, x: i32, z: i32) -> Option<usize> {
|
||||
// Use i64 arithmetic to avoid overflow when coordinates span large ranges
|
||||
let local_x = i64::from(x) - i64::from(self.min_x);
|
||||
let local_z = i64::from(z) - i64::from(self.min_z);
|
||||
|
||||
if local_x < 0 || local_z < 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let local_x = local_x as usize;
|
||||
let local_z = local_z as usize;
|
||||
|
||||
if local_x >= self.width || local_z >= self.height {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Safe: bounds checks above ensure this won't overflow (max = total_bits - 1)
|
||||
Some(local_z * self.width + local_x)
|
||||
}
|
||||
|
||||
/// Sets a coordinate as part of a building footprint.
|
||||
#[inline]
|
||||
pub fn set(&mut self, x: i32, z: i32) {
|
||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||
let byte_index = bit_index / 8;
|
||||
let bit_offset = bit_index % 8;
|
||||
|
||||
// Safety: coord_to_index already validates bounds, so byte_index is always valid
|
||||
let mask = 1u8 << bit_offset;
|
||||
// Only increment count if bit wasn't already set
|
||||
if self.bits[byte_index] & mask == 0 {
|
||||
self.bits[byte_index] |= mask;
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a coordinate is part of a building footprint.
|
||||
#[inline]
|
||||
pub fn contains(&self, x: i32, z: i32) -> bool {
|
||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||
let byte_index = bit_index / 8;
|
||||
let bit_offset = bit_index % 8;
|
||||
|
||||
// Safety: coord_to_index already validates bounds, so byte_index is always valid
|
||||
return (self.bits[byte_index] >> bit_offset) & 1 == 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if no coordinates are marked.
|
||||
#[must_use]
|
||||
#[allow(dead_code)] // Standard API method for collection-like types
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.count == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache of pre-computed flood fill results, keyed by element ID.
|
||||
pub struct FloodFillCache {
|
||||
/// Cached results: element_id -> filled coordinates
|
||||
@@ -222,7 +115,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")
|
||||
@@ -236,51 +128,9 @@ impl FloodFillCache {
|
||||
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Collects all building footprint coordinates from the pre-computed cache.
|
||||
///
|
||||
/// This should be called after precompute() and before elements are processed.
|
||||
/// Returns a memory-efficient bitmap of all (x, z) coordinates that are part of buildings.
|
||||
///
|
||||
/// The bitmap uses only 1 bit per coordinate in the world bounds, compared to ~24 bytes
|
||||
/// per entry in a HashSet, reducing memory usage by ~200x for large worlds.
|
||||
pub fn collect_building_footprints(
|
||||
&self,
|
||||
elements: &[ProcessedElement],
|
||||
xzbbox: &XZBBox,
|
||||
) -> BuildingFootprintBitmap {
|
||||
let mut footprints = BuildingFootprintBitmap::new(xzbbox);
|
||||
|
||||
for element in elements {
|
||||
match element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
if let Some(cached) = self.way_cache.get(&way.id) {
|
||||
for &(x, z) in cached {
|
||||
footprints.set(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
for member in &rel.members {
|
||||
// Only treat outer members as building footprints.
|
||||
// Inner members represent courtyards/holes where trees can spawn.
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
if let Some(cached) = self.way_cache.get(&member.way.id) {
|
||||
for &(x, z) in cached {
|
||||
footprints.set(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
footprints
|
||||
/// Returns the number of cached way entries.
|
||||
pub fn way_count(&self) -> usize {
|
||||
self.way_cache.len()
|
||||
}
|
||||
|
||||
/// Removes a way's cached flood fill result, freeing memory.
|
||||
|
||||
@@ -2,8 +2,6 @@ use crate::args::Args;
|
||||
use crate::coordinate_system::{cartesian::XZPoint, geographic::LLBBox};
|
||||
use crate::elevation_data::{fetch_elevation_data, ElevationData};
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use colored::Colorize;
|
||||
use image::{Rgb, RgbImage};
|
||||
|
||||
@@ -33,11 +31,7 @@ impl Ground {
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch elevation data: {}", e);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Elevation unavailable, using flat ground",
|
||||
);
|
||||
emit_gui_progress_update(15.0, "Elevation unavailable, using flat ground");
|
||||
// Graceful fallback: disable elevation and keep provided ground_level
|
||||
Self {
|
||||
elevation_enabled: false,
|
||||
@@ -147,7 +141,7 @@ impl Ground {
|
||||
pub fn generate_ground_data(args: &Args) -> Ground {
|
||||
if args.terrain {
|
||||
println!("{} Fetching elevation...", "[3/7]".bold());
|
||||
emit_gui_progress_update(14.0, "Fetching elevation...");
|
||||
emit_gui_progress_update(15.0, "Fetching elevation...");
|
||||
let ground = Ground::new_enabled(&args.bbox, args.scale, args.ground_level);
|
||||
if args.debug {
|
||||
ground.save_debug_image("elevation_debug");
|
||||
|
||||
201
src/gui.rs
201
src/gui.rs
@@ -1,5 +1,5 @@
|
||||
use crate::args::Args;
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
use crate::data_processing::{self, GenerationOptions};
|
||||
@@ -62,13 +62,6 @@ impl Drop for SessionLock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Desktop directory for Bedrock .mcworld file output.
|
||||
fn get_bedrock_output_directory() -> PathBuf {
|
||||
dirs::desktop_dir()
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
/// Gets the area name for a given bounding box using the center point
|
||||
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
||||
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
||||
@@ -84,9 +77,6 @@ pub fn run_gui() {
|
||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
||||
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
|
||||
|
||||
// Clean up old cached elevation tiles on startup
|
||||
crate::elevation_data::cleanup_old_cached_tiles();
|
||||
|
||||
// Launch the UI
|
||||
println!("Launching UI...");
|
||||
|
||||
@@ -112,7 +102,7 @@ pub fn run_gui() {
|
||||
tauri::Builder::default()
|
||||
.plugin(
|
||||
LogBuilder::default()
|
||||
.level(LevelFilter::Info)
|
||||
.level(LevelFilter::Warn)
|
||||
.targets([
|
||||
Target::new(TargetKind::LogDir {
|
||||
file_name: Some("arnis".into()),
|
||||
@@ -168,20 +158,16 @@ fn gui_select_world(generate_new: bool) -> Result<String, i32> {
|
||||
|
||||
if generate_new {
|
||||
// Handle new world generation
|
||||
// Try Minecraft saves directory first, fall back to current directory
|
||||
let target_path = if let Some(default_path) = &default_dir {
|
||||
if let Some(default_path) = &default_dir {
|
||||
if default_path.exists() {
|
||||
default_path.clone()
|
||||
// Call create_new_world and return the result
|
||||
create_new_world(default_path).map_err(|_| 1) // Error code 1: Minecraft directory not found
|
||||
} else {
|
||||
// Minecraft directory doesn't exist, use current directory
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
Err(1) // Error code 1: Minecraft directory not found
|
||||
}
|
||||
} else {
|
||||
// No default directory configured, use current directory
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
};
|
||||
|
||||
create_new_world(&target_path).map_err(|_| 3) // Error code 3: Failed to create new world
|
||||
Err(1) // Error code 1: Minecraft directory not found
|
||||
}
|
||||
} else {
|
||||
// Handle existing world selection
|
||||
// Open the directory picker dialog
|
||||
@@ -429,7 +415,6 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||
if let Ok(compressed_data) = encoder.finish() {
|
||||
if let Err(e) = std::fs::write(&level_path, compressed_data) {
|
||||
eprintln!("Failed to update level.dat with area name: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to update level.dat with area name",
|
||||
@@ -448,20 +433,36 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||
world_path
|
||||
}
|
||||
|
||||
/// Calculates the default spawn point at X=1, Z=1 relative to the world origin.
|
||||
/// This is used when no spawn point is explicitly selected by the user.
|
||||
fn calculate_default_spawn(xzbbox: &XZBBox) -> (i32, i32) {
|
||||
(xzbbox.min_x() + 1, xzbbox.min_z() + 1)
|
||||
}
|
||||
|
||||
/// Sets the player spawn point in level.dat using Minecraft XZ coordinates.
|
||||
/// The Y coordinate is set to a temporary value (150) and will be updated
|
||||
/// after terrain generation by `update_player_spawn_y_after_generation`.
|
||||
fn set_player_spawn_in_level_dat(
|
||||
// Function to update player position in level.dat based on spawn point coordinates
|
||||
fn update_player_position(
|
||||
world_path: &str,
|
||||
spawn_x: i32,
|
||||
spawn_z: i32,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
bbox_text: String,
|
||||
scale: f64,
|
||||
) -> Result<(), String> {
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
|
||||
let Some((lat, lng)) = spawn_point else {
|
||||
return Ok(()); // No spawn point selected, exit early
|
||||
};
|
||||
|
||||
// Parse geometrical point and bounding box
|
||||
let llpoint =
|
||||
LLPoint::new(lat, lng).map_err(|e| format!("Failed to parse spawn point:\n{e}"))?;
|
||||
let llbbox = LLBBox::from_str(&bbox_text)
|
||||
.map_err(|e| format!("Failed to parse bounding box for spawn point:\n{e}"))?;
|
||||
|
||||
// Check if spawn point is within the bbox
|
||||
if !llbbox.contains(&llpoint) {
|
||||
return Err("Spawn point is outside the selected area".to_string());
|
||||
}
|
||||
|
||||
// Convert lat/lng to Minecraft coordinates
|
||||
let (transformer, _) = CoordTransformer::llbbox_to_xzbbox(&llbbox, scale)
|
||||
.map_err(|e| format!("Failed to build transformation on coordinate systems:\n{e}"))?;
|
||||
|
||||
let xzpoint = transformer.transform_point(llpoint);
|
||||
|
||||
// Default y spawn position since terrain elevation cannot be determined yet
|
||||
let y = 150.0;
|
||||
|
||||
@@ -493,24 +494,21 @@ fn set_player_spawn_in_level_dat(
|
||||
if let Value::Compound(ref mut root) = nbt_data {
|
||||
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
||||
// Set world spawn point
|
||||
data.insert("SpawnX".to_string(), Value::Int(spawn_x));
|
||||
data.insert("SpawnX".to_string(), Value::Int(xzpoint.x));
|
||||
data.insert("SpawnY".to_string(), Value::Int(y as i32));
|
||||
data.insert("SpawnZ".to_string(), Value::Int(spawn_z));
|
||||
data.insert("SpawnZ".to_string(), Value::Int(xzpoint.z));
|
||||
|
||||
// Update player position if Player compound exists
|
||||
// Update player position
|
||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||
// Safely update position values with bounds checking
|
||||
if pos.len() >= 3 {
|
||||
if let Some(Value::Double(ref mut pos_x)) = pos.get_mut(0) {
|
||||
*pos_x = spawn_x as f64;
|
||||
}
|
||||
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
||||
*pos_y = y;
|
||||
}
|
||||
if let Some(Value::Double(ref mut pos_z)) = pos.get_mut(2) {
|
||||
*pos_z = spawn_z as f64;
|
||||
}
|
||||
if let Value::Double(ref mut pos_x) = pos.get_mut(0).unwrap() {
|
||||
*pos_x = xzpoint.x as f64;
|
||||
}
|
||||
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
||||
*pos_y = y;
|
||||
}
|
||||
if let Value::Double(ref mut pos_z) = pos.get_mut(2).unwrap() {
|
||||
*pos_z = xzpoint.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -542,15 +540,19 @@ fn set_player_spawn_in_level_dat(
|
||||
}
|
||||
|
||||
// Function to update player spawn Y coordinate based on terrain height after generation
|
||||
// This updates the spawn Y coordinate to be at terrain height + 3 blocks
|
||||
pub fn update_player_spawn_y_after_generation(
|
||||
world_path: &Path,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
bbox_text: String,
|
||||
scale: f64,
|
||||
ground: &Ground,
|
||||
) -> Result<(), String> {
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
|
||||
let Some((_lat, _lng)) = spawn_point else {
|
||||
return Ok(()); // No spawn point selected, exit early
|
||||
};
|
||||
|
||||
// Read the current level.dat file to get existing spawn coordinates
|
||||
let level_path = PathBuf::from(world_path).join("level.dat");
|
||||
if !level_path.exists() {
|
||||
@@ -619,7 +621,7 @@ pub fn update_player_spawn_y_after_generation(
|
||||
let relative_z = existing_spawn_z - xzbbox.min_z();
|
||||
let terrain_point = XZPoint::new(relative_x, relative_z);
|
||||
|
||||
ground.level(terrain_point) + 3 // Add 3 blocks above terrain for safety
|
||||
ground.level(terrain_point) + 2
|
||||
} else {
|
||||
-61 // Default Y if no terrain
|
||||
};
|
||||
@@ -633,8 +635,8 @@ pub fn update_player_spawn_y_after_generation(
|
||||
// Update player position - only Y coordinate
|
||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||
// Safely update Y position with bounds checking
|
||||
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
||||
// Keep existing X and Z, only update Y
|
||||
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
||||
*pos_y = spawn_y as f64;
|
||||
}
|
||||
}
|
||||
@@ -717,22 +719,12 @@ fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, St
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLon in metadata")?;
|
||||
|
||||
// Extract Minecraft coordinate bounds
|
||||
let min_mc_x = metadata["minMcX"].as_i64().unwrap_or(0) as i32;
|
||||
let max_mc_x = metadata["maxMcX"].as_i64().unwrap_or(0) as i32;
|
||||
let min_mc_z = metadata["minMcZ"].as_i64().unwrap_or(0) as i32;
|
||||
let max_mc_z = metadata["maxMcZ"].as_i64().unwrap_or(0) as i32;
|
||||
|
||||
Ok(Some(WorldMapData {
|
||||
image_base64: format!("data:image/png;base64,{}", base64_image),
|
||||
min_lat,
|
||||
max_lat,
|
||||
min_lon,
|
||||
max_lon,
|
||||
min_mc_x,
|
||||
max_mc_x,
|
||||
min_mc_z,
|
||||
max_mc_z,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -744,11 +736,6 @@ struct WorldMapData {
|
||||
max_lat: f64,
|
||||
min_lon: f64,
|
||||
max_lon: f64,
|
||||
// Minecraft coordinate bounds for coordinate copying
|
||||
min_mc_x: i32,
|
||||
max_mc_x: i32,
|
||||
min_mc_z: i32,
|
||||
max_mc_z: i32,
|
||||
}
|
||||
|
||||
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
||||
@@ -829,49 +816,36 @@ fn gui_start_generation(
|
||||
// Send generation click telemetry
|
||||
telemetry::send_generation_click();
|
||||
|
||||
// For new Java worlds, set the spawn point in level.dat
|
||||
// If spawn point was chosen and the world is new, check and set the spawn point
|
||||
// Only update player position for Java worlds - Bedrock worlds don't have a pre-existing
|
||||
// level.dat to modify (the spawn point will be set when the .mcworld is created)
|
||||
if is_new_world && world_format != "bedrock" {
|
||||
let llbbox = match LLBBox::from_str(&bbox_text) {
|
||||
Ok(bbox) => bbox,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to parse bounding box: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
};
|
||||
if is_new_world && spawn_point.is_some() && world_format != "bedrock" {
|
||||
// Verify the spawn point is within bounds
|
||||
if let Some(coords) = spawn_point {
|
||||
let llbbox = match LLBBox::from_str(&bbox_text) {
|
||||
Ok(bbox) => bbox,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to parse bounding box: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
};
|
||||
|
||||
let (transformer, xzbbox) = match CoordTransformer::llbbox_to_xzbbox(&llbbox, world_scale) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to create coordinate transformer: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
};
|
||||
|
||||
let (spawn_x, spawn_z) = if let Some(coords) = spawn_point {
|
||||
// User selected a spawn point - verify it's within bounds and convert to XZ
|
||||
let llpoint = LLPoint::new(coords.0, coords.1)
|
||||
.map_err(|e| format!("Failed to parse spawn point: {e}"))?;
|
||||
|
||||
if llbbox.contains(&llpoint) {
|
||||
let xzpoint = transformer.transform_point(llpoint);
|
||||
(xzpoint.x, xzpoint.z)
|
||||
} else {
|
||||
// Spawn point outside bounds, use default
|
||||
calculate_default_spawn(&xzbbox)
|
||||
// Spawn point is valid, update the player position
|
||||
update_player_position(
|
||||
&selected_world,
|
||||
spawn_point,
|
||||
bbox_text.clone(),
|
||||
world_scale,
|
||||
)
|
||||
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
||||
}
|
||||
} else {
|
||||
// No user-selected spawn point - use default at X=1, Z=1 relative to world origin
|
||||
calculate_default_spawn(&xzbbox)
|
||||
};
|
||||
|
||||
set_player_spawn_in_level_dat(&selected_world, spawn_x, spawn_z)
|
||||
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -947,18 +921,18 @@ fn gui_start_generation(
|
||||
(updated_path, None)
|
||||
}
|
||||
WorldFormat::BedrockMcWorld => {
|
||||
// Bedrock: generate .mcworld on Desktop with location-based name
|
||||
// Bedrock: generate .mcworld in current directory with location-based name
|
||||
let area_name = get_area_name_for_bedrock(&bbox);
|
||||
let filename = format!("Arnis {}.mcworld", area_name);
|
||||
let lvl_name = format!("Arnis World: {}", area_name);
|
||||
|
||||
let output_path = get_bedrock_output_directory().join(&filename);
|
||||
let output_path = std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join(filename);
|
||||
(output_path, Some(lvl_name))
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate MC spawn coordinates from lat/lng if spawn point was provided
|
||||
// Otherwise, default to X=1, Z=1 (relative to xzbbox min coordinates)
|
||||
let mc_spawn_point: Option<(i32, i32)> = if let Some((lat, lng)) = spawn_point {
|
||||
if let Ok(llpoint) = LLPoint::new(lat, lng) {
|
||||
if let Ok((transformer, _)) =
|
||||
@@ -973,12 +947,7 @@ fn gui_start_generation(
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// Default spawn point: X=1, Z=1 relative to world origin
|
||||
if let Ok((_, xzbbox)) = CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale) {
|
||||
Some(calculate_default_spawn(&xzbbox))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
// Create generation options
|
||||
@@ -1009,6 +978,7 @@ fn gui_start_generation(
|
||||
fillground: fillground_enabled,
|
||||
debug: false,
|
||||
timeout: Some(std::time::Duration::from_secs(40)),
|
||||
spawn_point,
|
||||
};
|
||||
|
||||
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
||||
@@ -1101,9 +1071,10 @@ fn gui_start_generation(
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
emit_gui_error(&e.to_string());
|
||||
let error_msg = format!("Failed to fetch data: {e}");
|
||||
emit_gui_error(&error_msg);
|
||||
// Session lock will be automatically released when _session_lock goes out of scope
|
||||
Err(e.to_string())
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
51
src/gui/css/bbox.css
vendored
51
src/gui/css/bbox.css
vendored
@@ -8,9 +8,13 @@ body,
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
/* Hide the BBOX coordinates display at bottom of map */
|
||||
#info-box {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
bottom: 0;
|
||||
border: 0 0 7px 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#coord-format {
|
||||
@@ -347,8 +351,7 @@ body,
|
||||
background-position: -31px -2px;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled,
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.editing-mode {
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
@@ -375,42 +378,4 @@ body,
|
||||
accent-color: #3887BE;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Context menu for coordinate copying */
|
||||
.coordinate-context-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10000;
|
||||
min-width: 160px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-separator {
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
188
src/gui/css/styles.css
vendored
188
src/gui/css/styles.css
vendored
@@ -32,12 +32,9 @@ p {
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding-top: 0.4em;
|
||||
padding-bottom: 0.5em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
max-width: 950px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.logo.arnis:hover {
|
||||
@@ -62,11 +59,11 @@ a:hover {
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-top: 5px;
|
||||
min-height: 70vh;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -78,70 +75,34 @@ a:hover {
|
||||
|
||||
.map-box,
|
||||
.controls-box {
|
||||
width: 45%;
|
||||
background: #575757;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-box {
|
||||
width: 63%;
|
||||
min-height: 420px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #575757;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.controls-box {
|
||||
width: 32%;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.controls-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.controls-box .progress-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.controls-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bbox-selection-text {
|
||||
font-size: 0.9em;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
min-height: 2.5em;
|
||||
line-height: 1.25em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.progress-info-text {
|
||||
font-size: 0.9em;
|
||||
color: #ececec;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
min-height: 1.5em;
|
||||
line-height: 1.25em;
|
||||
margin-bottom: 5px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
border: none;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
@@ -181,25 +142,18 @@ button:hover {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.progress-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.progress-section h2 {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#progress-detail {
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@@ -209,6 +163,15 @@ button:hover {
|
||||
transition: width 0.4s;
|
||||
}
|
||||
|
||||
/* Left and right alignment for "Saving world..." text */
|
||||
.progress-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9em;
|
||||
margin-top: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
@@ -227,7 +190,7 @@ button:hover {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #333333;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -270,7 +233,6 @@ button:hover {
|
||||
width: 100%;
|
||||
border-radius: 8px 8px 0 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@@ -325,7 +287,7 @@ button:hover {
|
||||
/* Customization Settings */
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 20001;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
@@ -338,7 +300,7 @@ button:hover {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #717171;
|
||||
background-color: #797979;
|
||||
padding: 20px;
|
||||
border: 1px solid #797979;
|
||||
border-radius: 10px;
|
||||
@@ -458,20 +420,6 @@ button:hover {
|
||||
box-shadow: 0 0 5px #fecc44;
|
||||
}
|
||||
|
||||
#save-path {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
border: 1px solid #fecc44;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#save-path:focus {
|
||||
outline: none;
|
||||
border-color: #fecc44;
|
||||
box-shadow: 0 0 5px #fecc44;
|
||||
}
|
||||
|
||||
/* Settings Modal Layout */
|
||||
.settings-row {
|
||||
display: flex;
|
||||
@@ -483,75 +431,6 @@ button:hover {
|
||||
.settings-row label {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Tooltip icon (question mark in circle) */
|
||||
.tooltip-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(254, 204, 68, 0.3);
|
||||
color: #fecc44;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
cursor: help;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tooltip-icon:hover {
|
||||
background-color: rgba(254, 204, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Arnis-styled tooltip box */
|
||||
.tooltip-icon::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 8px);
|
||||
transform: translateX(-50%);
|
||||
background-color: #2a2a2a;
|
||||
color: #fecc44;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #fecc44;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Tooltip arrow */
|
||||
.tooltip-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 2px);
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #fecc44;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
z-index: 1001;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-icon:hover::after,
|
||||
.tooltip-icon:hover::before {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.settings-control {
|
||||
@@ -727,12 +606,9 @@ button:hover {
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.settings-button svg {
|
||||
stroke: white;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
.settings-button .gear-icon::before {
|
||||
content: "⚙️";
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Logo Animation */
|
||||
|
||||
139
src/gui/index.html
vendored
139
src/gui/index.html
vendored
@@ -20,52 +20,60 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<!-- Left Box: Map -->
|
||||
<section class="section map-box">
|
||||
<iframe src="maps.html" width="100%" height="100%" class="map-container" title="Map Picker"></iframe>
|
||||
<!-- Left Box: Map and BBox Input -->
|
||||
<section class="section map-box" style="margin-bottom: 0; padding-bottom: 0;">
|
||||
<h2 data-localize="select_location">Select Location</h2>
|
||||
<span id="bbox-text" style="font-size: 1.0em; display: block; margin-top: -8px; margin-bottom: 3px;" data-localize="zoom_in_and_choose">
|
||||
Zoom in and choose your area using the rectangle tool
|
||||
</span>
|
||||
<iframe src="maps.html" width="100%" height="300" class="map-container" title="Map Picker"></iframe>
|
||||
|
||||
<span id="bbox-info"
|
||||
style="font-size: 0.75em; color: #7bd864; display: block; margin-bottom: 4px; font-weight: bold; min-height: 2em;"></span>
|
||||
</section>
|
||||
|
||||
<!-- Right Box: Directory Selection, Start Button, and Progress Bar -->
|
||||
<section class="section controls-box">
|
||||
<div class="controls-content">
|
||||
<div class="controls-top">
|
||||
<!-- World Selection Container -->
|
||||
<div class="world-selection-container">
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
||||
<span id="choose_world">Choose World</span>
|
||||
<br>
|
||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||
No world selected
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- World Format Toggle -->
|
||||
<div class="format-toggle-container">
|
||||
<button type="button" id="format-java" class="format-toggle-btn format-active" onclick="setWorldFormat('java')">
|
||||
Java
|
||||
</button>
|
||||
<button type="button" id="format-bedrock" class="format-toggle-btn" onclick="setWorldFormat('bedrock')">
|
||||
Bedrock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
|
||||
<button type="button" class="settings-button" onclick="openSettings()" aria-label="Settings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
<h2 data-localize="select_world">Select World</h2>
|
||||
|
||||
<!-- World Selection Container -->
|
||||
<div class="world-selection-container">
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
||||
<span id="choose_world">Choose World</span>
|
||||
<br>
|
||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||
No world selected
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- World Format Toggle -->
|
||||
<div class="format-toggle-container">
|
||||
<button type="button" id="format-java" class="format-toggle-btn format-active" onclick="setWorldFormat('java')">
|
||||
Java
|
||||
</button>
|
||||
<button type="button" id="format-bedrock" class="format-toggle-btn" onclick="setWorldFormat('bedrock')">
|
||||
Bedrock
|
||||
</button>
|
||||
</div>
|
||||
<span id="bbox-selection-info" class="bbox-selection-text" data-localize="select_area_prompt">Select an area on the map using the tools.</span>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
|
||||
<button type="button" class="settings-button" onclick="openSettings()">
|
||||
<i class="gear-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<div class="progress-section">
|
||||
<span id="progress-info" class="progress-info-text"></span>
|
||||
<div class="progress-row">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<h2 data-localize="progress">Progress</h2>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div class="progress-status">
|
||||
<span id="progress-message"></span>
|
||||
<span id="progress-detail">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,10 +100,7 @@
|
||||
|
||||
<!-- Generation Mode Dropdown -->
|
||||
<div class="settings-row">
|
||||
<label for="generation-mode-select">
|
||||
<span data-localize="generation_mode">Generation Mode</span>
|
||||
<span class="tooltip-icon" data-tooltip="Choose what to generate: buildings/roads with terrain, just objects, or terrain only">?</span>
|
||||
</label>
|
||||
<label for="generation-mode-select" data-localize="generation_mode">Generation Mode</label>
|
||||
<div class="settings-control">
|
||||
<select id="generation-mode-select" name="generation-mode-select" class="generation-mode-dropdown">
|
||||
<option value="geo-terrain" data-localize="mode_geo_terrain">Objects + Terrain</option>
|
||||
@@ -105,34 +110,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interior Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="interior-toggle" data-localize="interior">Interior Generation</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="interior-toggle" name="interior-toggle" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roof Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="roof-toggle">
|
||||
<span data-localize="roof">Roof Generation</span>
|
||||
<span class="tooltip-icon" data-tooltip="Generate roofs on buildings">?</span>
|
||||
</label>
|
||||
<label for="roof-toggle" data-localize="roof">Roof Generation</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="roof-toggle" name="roof-toggle" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interior Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="interior-toggle">
|
||||
<span data-localize="interior">Interior Generation</span>
|
||||
<span class="tooltip-icon" data-tooltip="Generate interior details inside buildings">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="interior-toggle" name="interior-toggle">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fill ground Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="fillground-toggle">
|
||||
<span data-localize="fillground">Fill Ground</span>
|
||||
<span class="tooltip-icon" data-tooltip="Fill the ground below the surface">?</span>
|
||||
</label>
|
||||
<label for="fillground-toggle" data-localize="fillground">Fill Ground</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="fillground-toggle" name="fillground-toggle">
|
||||
</div>
|
||||
@@ -140,10 +136,7 @@
|
||||
|
||||
<!-- World Scale Slider -->
|
||||
<div class="settings-row">
|
||||
<label for="scale-value-slider">
|
||||
<span data-localize="world_scale">World Scale</span>
|
||||
<span class="tooltip-icon" data-tooltip="Scale factor for the generated world (1.0 = real-world scale)">?</span>
|
||||
</label>
|
||||
<label for="scale-value-slider" data-localize="world_scale">World Scale</label>
|
||||
<div class="settings-control">
|
||||
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.30" max="2.5" step="0.1" value="1">
|
||||
<span id="slider-value">1.00</span>
|
||||
@@ -152,10 +145,7 @@
|
||||
|
||||
<!-- Bounding Box Input -->
|
||||
<div class="settings-row">
|
||||
<label for="bbox-coords">
|
||||
<span data-localize="custom_bounding_box">Custom Bounding Box</span>
|
||||
<span class="tooltip-icon" data-tooltip="Manually enter coordinates (lat,lng,lat,lng) or use map selection">?</span>
|
||||
</label>
|
||||
<label for="bbox-coords" data-localize="custom_bounding_box">Custom Bounding Box</label>
|
||||
<div class="settings-control">
|
||||
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" placeholder="Format: lat,lng,lat,lng">
|
||||
</div>
|
||||
@@ -163,10 +153,7 @@
|
||||
|
||||
<!-- Map Theme Selector -->
|
||||
<div class="settings-row">
|
||||
<label for="tile-theme-select">
|
||||
<span data-localize="map_theme">Map Theme</span>
|
||||
<span class="tooltip-icon" data-tooltip="Visual style of the map picker">?</span>
|
||||
</label>
|
||||
<label for="tile-theme-select" data-localize="map_theme">Map Theme</label>
|
||||
<div class="settings-control">
|
||||
<select id="tile-theme-select" name="tile-theme-select" class="theme-dropdown">
|
||||
<option value="osm">Standard</option>
|
||||
@@ -180,10 +167,7 @@
|
||||
|
||||
<!-- Language Selector -->
|
||||
<div class="settings-row">
|
||||
<label for="language-select">
|
||||
<span data-localize="language">Language</span>
|
||||
<span class="tooltip-icon" data-tooltip="Interface language">?</span>
|
||||
</label>
|
||||
<label for="language-select" data-localize="language">Language</label>
|
||||
<div class="settings-control">
|
||||
<select id="language-select" name="language-select" class="language-dropdown">
|
||||
<option value="en">English</option>
|
||||
@@ -207,10 +191,7 @@
|
||||
|
||||
<!-- Telemetry Consent Toggle -->
|
||||
<div class="settings-row">
|
||||
<label for="telemetry-toggle" style="white-space: nowrap;">
|
||||
<span>Anonymous Crash Reports</span>
|
||||
<span class="tooltip-icon" data-tooltip="Send anonymous crash data to help improve Arnis">?</span>
|
||||
</label>
|
||||
<label for="telemetry-toggle">Anonymous Crash Reports</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
|
||||
</div>
|
||||
|
||||
226
src/gui/js/bbox.js
vendored
226
src/gui/js/bbox.js
vendored
@@ -564,7 +564,6 @@ $(document).ready(function () {
|
||||
var worldOverlayEnabled = false;
|
||||
var worldPreviewAvailable = false;
|
||||
var sliderControl = null;
|
||||
var worldOverlayHiddenForEdit = false; // Track if we hid the overlay for edit/delete mode
|
||||
|
||||
// Create the opacity slider as a proper Leaflet control
|
||||
var SliderControl = L.Control.extend({
|
||||
@@ -723,214 +722,6 @@ $(document).ready(function () {
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily hide the overlay (for edit/delete mode)
|
||||
function hideWorldOverlayTemporarily() {
|
||||
if (worldOverlay && worldOverlayEnabled) {
|
||||
worldOverlayHiddenForEdit = true;
|
||||
map.removeLayer(worldOverlay);
|
||||
}
|
||||
// Also visually disable the preview button during edit/delete mode
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
if (btn) {
|
||||
btn.classList.add('editing-mode');
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the overlay after edit/delete mode ends
|
||||
function restoreWorldOverlay() {
|
||||
if (worldOverlayHiddenForEdit && worldOverlay && worldOverlayEnabled) {
|
||||
worldOverlay.addTo(map);
|
||||
worldOverlayHiddenForEdit = false;
|
||||
}
|
||||
// Re-enable the preview button
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
if (btn) {
|
||||
btn.classList.remove('editing-mode');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Context Menu for Coordinate Copying ==========
|
||||
var contextMenuElement = null;
|
||||
|
||||
// Create the context menu element
|
||||
function createContextMenu() {
|
||||
if (contextMenuElement) return contextMenuElement;
|
||||
|
||||
contextMenuElement = document.createElement('div');
|
||||
contextMenuElement.className = 'coordinate-context-menu';
|
||||
contextMenuElement.style.display = 'none';
|
||||
contextMenuElement.innerHTML = `
|
||||
<div class="coordinate-context-menu-item" id="copy-coords-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<span id="copy-coords-text">Copy coordinates</span>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(contextMenuElement);
|
||||
|
||||
// Handle click on the copy coordinates item
|
||||
var copyItem = contextMenuElement.querySelector('#copy-coords-item');
|
||||
copyItem.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
copyMinecraftCoordinates();
|
||||
hideContextMenu();
|
||||
});
|
||||
|
||||
return contextMenuElement;
|
||||
}
|
||||
|
||||
// Show context menu at position
|
||||
function showContextMenu(x, y, latLng) {
|
||||
if (!worldPreviewAvailable || !worldOverlayData) return;
|
||||
|
||||
var menu = createContextMenu();
|
||||
|
||||
// Position the menu, ensuring it stays within viewport
|
||||
var menuWidth = 180;
|
||||
var menuHeight = 40;
|
||||
var viewportWidth = window.innerWidth;
|
||||
var viewportHeight = window.innerHeight;
|
||||
|
||||
var posX = x;
|
||||
var posY = y;
|
||||
|
||||
// Adjust if menu would go off-screen
|
||||
if (x + menuWidth > viewportWidth) {
|
||||
posX = viewportWidth - menuWidth - 10;
|
||||
}
|
||||
if (y + menuHeight > viewportHeight) {
|
||||
posY = viewportHeight - menuHeight - 10;
|
||||
}
|
||||
|
||||
menu.style.left = posX + 'px';
|
||||
menu.style.top = posY + 'px';
|
||||
menu.style.display = 'block';
|
||||
|
||||
// Store the latLng for copying
|
||||
menu.dataset.lat = latLng.lat;
|
||||
menu.dataset.lng = latLng.lng;
|
||||
}
|
||||
|
||||
// Hide context menu
|
||||
function hideContextMenu() {
|
||||
if (contextMenuElement) {
|
||||
contextMenuElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Minecraft coordinates from lat/lng
|
||||
function calculateMinecraftCoords(lat, lng) {
|
||||
if (!worldOverlayData) return null;
|
||||
|
||||
var data = worldOverlayData;
|
||||
|
||||
// Check if Minecraft coordinate bounds are available (not all zeros)
|
||||
if (data.min_mc_x === 0 && data.max_mc_x === 0 &&
|
||||
data.min_mc_z === 0 && data.max_mc_z === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate the relative position within the geo bounds (0 to 1)
|
||||
// Note: Latitude increases northward, but Minecraft Z increases southward
|
||||
var relX = (lng - data.min_lon) / (data.max_lon - data.min_lon);
|
||||
var relZ = (data.max_lat - lat) / (data.max_lat - data.min_lat);
|
||||
|
||||
// Clamp to 0-1 range
|
||||
relX = Math.max(0, Math.min(1, relX));
|
||||
relZ = Math.max(0, Math.min(1, relZ));
|
||||
|
||||
// Calculate Minecraft X and Z coordinates
|
||||
var mcX = Math.round(data.min_mc_x + relX * (data.max_mc_x - data.min_mc_x));
|
||||
var mcZ = Math.round(data.min_mc_z + relZ * (data.max_mc_z - data.min_mc_z));
|
||||
|
||||
// Default Y coordinate (ground level, typically around 64-70)
|
||||
var mcY = 100;
|
||||
|
||||
return { x: mcX, y: mcY, z: mcZ };
|
||||
}
|
||||
|
||||
// Copy Minecraft coordinates to clipboard
|
||||
function copyMinecraftCoordinates() {
|
||||
if (!contextMenuElement) return;
|
||||
|
||||
var lat = parseFloat(contextMenuElement.dataset.lat);
|
||||
var lng = parseFloat(contextMenuElement.dataset.lng);
|
||||
|
||||
var coords = calculateMinecraftCoords(lat, lng);
|
||||
if (!coords) return;
|
||||
|
||||
var tpCommand = '/tp ' + coords.x + ' ' + coords.y + ' ' + coords.z;
|
||||
|
||||
// Copy to clipboard using modern API with fallback
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(tpCommand).catch(function(err) {
|
||||
// Fallback for clipboard API failure
|
||||
fallbackCopyToClipboard(tpCommand);
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
fallbackCopyToClipboard(tpCommand);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback clipboard copy method for older browsers
|
||||
function fallbackCopyToClipboard(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
textArea.style.top = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy coordinates:', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Check if Minecraft coordinate bounds are available
|
||||
function hasMinecraftCoords() {
|
||||
if (!worldOverlayData) return false;
|
||||
var data = worldOverlayData;
|
||||
return !(data.min_mc_x === 0 && data.max_mc_x === 0 &&
|
||||
data.min_mc_z === 0 && data.max_mc_z === 0);
|
||||
}
|
||||
|
||||
// Handle right-click on the map
|
||||
map.on('contextmenu', function(e) {
|
||||
// Only show context menu if world preview is available and has Minecraft coords
|
||||
if (worldPreviewAvailable && worldOverlayData && hasMinecraftCoords()) {
|
||||
// Check if the click is within the world bounds
|
||||
var data = worldOverlayData;
|
||||
var lat = e.latlng.lat;
|
||||
var lng = e.latlng.lng;
|
||||
|
||||
if (lat >= data.min_lat && lat <= data.max_lat &&
|
||||
lng >= data.min_lon && lng <= data.max_lon) {
|
||||
showContextMenu(e.originalEvent.clientX, e.originalEvent.clientY, e.latlng);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hide context menu on any click or map interaction
|
||||
document.addEventListener('click', function(e) {
|
||||
if (contextMenuElement && !contextMenuElement.contains(e.target)) {
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
map.on('movestart', hideContextMenu);
|
||||
map.on('zoomstart', hideContextMenu);
|
||||
// ========== End Context Menu ==========
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'changeTileTheme') {
|
||||
@@ -1208,23 +999,6 @@ $(document).ready(function () {
|
||||
map.fitBounds(bounds.getBounds());
|
||||
});
|
||||
|
||||
// Hide world preview overlay when entering edit or delete mode
|
||||
map.on('draw:editstart', function() {
|
||||
hideWorldOverlayTemporarily();
|
||||
});
|
||||
|
||||
map.on('draw:deletestart', function() {
|
||||
hideWorldOverlayTemporarily();
|
||||
});
|
||||
|
||||
// Restore world preview overlay when exiting edit or delete mode
|
||||
map.on('draw:editstop', function() {
|
||||
restoreWorldOverlay();
|
||||
});
|
||||
|
||||
map.on('draw:deletestop', function() {
|
||||
restoreWorldOverlay();
|
||||
});
|
||||
function display() {
|
||||
$('#boxbounds').text(formatBounds(bounds.getBounds(), '4326'));
|
||||
$('#boxboundsmerc').text(formatBounds(bounds.getBounds(), currentproj));
|
||||
|
||||
118
src/gui/js/main.js
vendored
118
src/gui/js/main.js
vendored
@@ -12,25 +12,6 @@ if (window.__TAURI__) {
|
||||
|
||||
const DEFAULT_LOCALE_PATH = `./locales/en.json`;
|
||||
|
||||
// Track current bbox selection info localization key for language changes
|
||||
let currentBboxSelectionKey = "select_area_prompt";
|
||||
let currentBboxSelectionColor = "#ffffff";
|
||||
|
||||
// Helper function to set bbox selection info text and track it for language changes
|
||||
async function setBboxSelectionInfo(bboxSelectionElement, localizationKey, color) {
|
||||
currentBboxSelectionKey = localizationKey;
|
||||
currentBboxSelectionColor = color;
|
||||
|
||||
// Ensure localization is available
|
||||
let localization = window.localization;
|
||||
if (!localization) {
|
||||
localization = await getLocalization();
|
||||
}
|
||||
|
||||
localizeElement(localization, { element: bboxSelectionElement }, localizationKey);
|
||||
bboxSelectionElement.style.color = color;
|
||||
}
|
||||
|
||||
// Initialize elements and start the demo progress
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
registerMessageEvent();
|
||||
@@ -85,7 +66,7 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
||||
const attribute = localizedStringKey.startsWith("placeholder_") ? "placeholder" : "textContent";
|
||||
|
||||
if (element) {
|
||||
if (json && localizedStringKey in json) {
|
||||
if (localizedStringKey in json) {
|
||||
element[attribute] = json[localizedStringKey];
|
||||
} else {
|
||||
// Fallback to default (English) string
|
||||
@@ -97,9 +78,13 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
||||
|
||||
async function applyLocalization(localization) {
|
||||
const localizationElements = {
|
||||
"h2[data-localize='select_location']": "select_location",
|
||||
"#bbox-text": "zoom_in_and_choose",
|
||||
"h2[data-localize='select_world']": "select_world",
|
||||
"span[id='choose_world']": "choose_world",
|
||||
"#selected-world": "no_world_selected",
|
||||
"#start-button": "start_generation",
|
||||
"h2[data-localize='progress']": "progress",
|
||||
"h2[data-localize='choose_world_modal_title']": "choose_world_modal_title",
|
||||
"button[data-localize='select_existing_world']": "select_existing_world",
|
||||
"button[data-localize='generate_new_world']": "generate_new_world",
|
||||
@@ -132,13 +117,6 @@ async function applyLocalization(localization) {
|
||||
localizeElement(localization, { selector: selector }, localizationElements[selector]);
|
||||
}
|
||||
|
||||
// Re-apply current bbox selection info text with new language
|
||||
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
|
||||
if (bboxSelectionInfo && currentBboxSelectionKey) {
|
||||
localizeElement(localization, { element: bboxSelectionInfo }, currentBboxSelectionKey);
|
||||
bboxSelectionInfo.style.color = currentBboxSelectionColor;
|
||||
}
|
||||
|
||||
// Update error messages
|
||||
window.localization = localization;
|
||||
}
|
||||
@@ -187,7 +165,7 @@ async function checkForUpdates() {
|
||||
updateMessage.style.textDecoration = "none";
|
||||
|
||||
localizeElement(window.localization, { element: updateMessage }, "new_version_available");
|
||||
footer.style.marginTop = "10px";
|
||||
footer.style.marginTop = "15px";
|
||||
footer.appendChild(updateMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -210,7 +188,7 @@ function registerMessageEvent() {
|
||||
// Function to set up the progress bar listener
|
||||
function setupProgressListener() {
|
||||
const progressBar = document.getElementById("progress-bar");
|
||||
const progressInfo = document.getElementById("progress-info");
|
||||
const progressMessage = document.getElementById("progress-message");
|
||||
const progressDetail = document.getElementById("progress-detail");
|
||||
|
||||
window.__TAURI__.event.listen("progress-update", (event) => {
|
||||
@@ -222,16 +200,16 @@ function setupProgressListener() {
|
||||
}
|
||||
|
||||
if (message != "") {
|
||||
progressInfo.textContent = message;
|
||||
progressMessage.textContent = message;
|
||||
|
||||
if (message.startsWith("Error!")) {
|
||||
progressInfo.style.color = "#fa7878";
|
||||
progressMessage.style.color = "#fa7878";
|
||||
generationButtonEnabled = true;
|
||||
} else if (message.startsWith("Done!")) {
|
||||
progressInfo.style.color = "#7bd864";
|
||||
progressMessage.style.color = "#7bd864";
|
||||
generationButtonEnabled = true;
|
||||
} else {
|
||||
progressInfo.style.color = "#ececec";
|
||||
progressMessage.style.color = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -272,20 +250,6 @@ function initSettings() {
|
||||
settingsModal.style.display = "none";
|
||||
}
|
||||
|
||||
// Close settings and license modals on escape key
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
if (settingsModal.style.display === "flex") {
|
||||
closeSettings();
|
||||
}
|
||||
|
||||
const licenseModal = document.getElementById("license-modal");
|
||||
if (licenseModal && licenseModal.style.display === "flex") {
|
||||
closeLicense();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.openSettings = openSettings;
|
||||
window.closeSettings = closeSettings;
|
||||
|
||||
@@ -535,7 +499,7 @@ function initWorldPicker() {
|
||||
*/
|
||||
function handleBboxInput() {
|
||||
const inputBox = document.getElementById("bbox-coords");
|
||||
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
|
||||
const bboxInfo = document.getElementById("bbox-info");
|
||||
|
||||
inputBox.addEventListener("input", function () {
|
||||
const input = inputBox.value.trim();
|
||||
@@ -547,12 +511,11 @@ function handleBboxInput() {
|
||||
|
||||
// Clear the info text only if no map selection exists
|
||||
if (!mapSelectedBBox) {
|
||||
setBboxSelectionInfo(bboxSelectionInfo, "select_area_prompt", "#ffffff");
|
||||
bboxInfo.textContent = "";
|
||||
bboxInfo.style.color = "";
|
||||
} else {
|
||||
// Restore map selection info display but don't update input field
|
||||
const [lng1, lat1, lng2, lat2] = mapSelectedBBox.split(" ").map(Number);
|
||||
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
||||
displayBboxSizeStatus(bboxSelectionInfo, selectedSize);
|
||||
// Restore map selection display
|
||||
displayBboxInfoText(mapSelectedBBox);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -588,7 +551,8 @@ function handleBboxInput() {
|
||||
// Update the info text and mark custom input as valid
|
||||
customBBoxValid = true;
|
||||
selectedBBox = bboxText.replace(/,/g, ' '); // Convert to space format for consistency
|
||||
setBboxSelectionInfo(bboxSelectionInfo, "custom_selection_confirmed", "#7bd864");
|
||||
localizeElement(window.localization, { element: bboxInfo }, "custom_selection_confirmed");
|
||||
bboxInfo.style.color = "#7bd864";
|
||||
} else {
|
||||
// Valid numbers but invalid order or range
|
||||
customBBoxValid = false;
|
||||
@@ -598,7 +562,8 @@ function handleBboxInput() {
|
||||
} else {
|
||||
selectedBBox = mapSelectedBBox;
|
||||
}
|
||||
setBboxSelectionInfo(bboxSelectionInfo, "error_coordinates_out_of_range", "#fecc44");
|
||||
localizeElement(window.localization, { element: bboxInfo }, "error_coordinates_out_of_range");
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
}
|
||||
} else {
|
||||
// Input doesn't match the required format
|
||||
@@ -609,7 +574,8 @@ function handleBboxInput() {
|
||||
} else {
|
||||
selectedBBox = mapSelectedBBox;
|
||||
}
|
||||
setBboxSelectionInfo(bboxSelectionInfo, "invalid_format", "#fecc44");
|
||||
localizeElement(window.localization, { element: bboxInfo }, "invalid_format");
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -658,21 +624,6 @@ let selectedBBox = "";
|
||||
let mapSelectedBBox = ""; // Tracks bbox from map selection
|
||||
let customBBoxValid = false; // Tracks if custom input is valid
|
||||
|
||||
/**
|
||||
* Displays the appropriate bbox size status message based on area thresholds
|
||||
* @param {HTMLElement} bboxSelectionElement - The element to display the message in
|
||||
* @param {number} selectedSize - The calculated bbox area in square meters
|
||||
*/
|
||||
function displayBboxSizeStatus(bboxSelectionElement, selectedSize) {
|
||||
if (selectedSize > threshold2) {
|
||||
setBboxSelectionInfo(bboxSelectionElement, "area_too_large", "#fa7878");
|
||||
} else if (selectedSize > threshold1) {
|
||||
setBboxSelectionInfo(bboxSelectionElement, "area_extensive", "#fecc44");
|
||||
} else {
|
||||
setBboxSelectionInfo(bboxSelectionElement, "selection_confirmed", "#7bd864");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle incoming bbox data
|
||||
function displayBboxInfoText(bboxText) {
|
||||
let [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
|
||||
@@ -686,13 +637,11 @@ function displayBboxInfoText(bboxText) {
|
||||
selectedBBox = mapSelectedBBox;
|
||||
customBBoxValid = false;
|
||||
|
||||
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
|
||||
const bboxCoordsInput = document.getElementById("bbox-coords");
|
||||
const bboxInfo = document.getElementById("bbox-info");
|
||||
|
||||
// Reset the info text if the bbox is 0,0,0,0
|
||||
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
|
||||
setBboxSelectionInfo(bboxSelectionInfo, "select_area_prompt", "#ffffff");
|
||||
bboxCoordsInput.value = "";
|
||||
bboxInfo.textContent = "";
|
||||
mapSelectedBBox = "";
|
||||
if (!customBBoxValid) {
|
||||
selectedBBox = "";
|
||||
@@ -700,13 +649,19 @@ function displayBboxInfoText(bboxText) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the custom bbox input with the map selection (comma-separated format)
|
||||
bboxCoordsInput.value = `${lng1},${lat1},${lng2},${lat2}`;
|
||||
|
||||
// Calculate the size of the selected bbox
|
||||
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
||||
|
||||
displayBboxSizeStatus(bboxSelectionInfo, selectedSize);
|
||||
if (selectedSize > threshold2) {
|
||||
localizeElement(window.localization, { element: bboxInfo }, "area_too_large");
|
||||
bboxInfo.style.color = "#fa7878";
|
||||
} else if (selectedSize > threshold1) {
|
||||
localizeElement(window.localization, { element: bboxInfo }, "area_extensive");
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
} else {
|
||||
localizeElement(window.localization, { element: bboxInfo }, "selection_confirmed");
|
||||
bboxInfo.style.color = "#7bd864";
|
||||
}
|
||||
}
|
||||
|
||||
let worldPath = "";
|
||||
@@ -796,8 +751,9 @@ async function startGeneration() {
|
||||
}
|
||||
|
||||
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
|
||||
const bboxSelectionInfo = document.getElementById('bbox-selection-info');
|
||||
setBboxSelectionInfo(bboxSelectionInfo, "select_location_first", "#fa7878");
|
||||
const bboxInfo = document.getElementById('bbox-info');
|
||||
localizeElement(window.localization, { element: bboxInfo }, "select_location_first");
|
||||
bboxInfo.style.color = "#fa7878";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
4
src/gui/js/maps/wkt.parser.js
vendored
4
src/gui/js/maps/wkt.parser.js
vendored
@@ -195,7 +195,11 @@ Wkt.Wkt.prototype.toObject = function (config) {
|
||||
* Absorbs the geometry of another Wkt.Wkt instance, merging it with its own,
|
||||
* creating a collection (MULTI-geometry) based on their types, which must agree.
|
||||
* For example, creates a MULTIPOLYGON from a POLYGON type merged with another
|
||||
<<<<<<< HEAD
|
||||
* POLYGON type.
|
||||
=======
|
||||
* POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
|
||||
>>>>>>> dev
|
||||
* @memberof Wkt.Wkt
|
||||
* @method
|
||||
*/
|
||||
|
||||
5
src/gui/locales/ar.json
vendored
5
src/gui/locales/ar.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "اختيار موقع",
|
||||
"zoom_in_and_choose": "قم بالتكبير واختر منطقتك باستخدام أداة المستطيل",
|
||||
"select_world": "تحديد عالم",
|
||||
"choose_world": "اختيار عالم",
|
||||
"no_world_selected": "لم يتم تحديد عالم",
|
||||
"start_generation": "بدء البناء",
|
||||
"progress": "التقدم",
|
||||
"custom_selection_confirmed": "تم تأكيد التحديد المخصص!",
|
||||
"error_coordinates_out_of_range": "خطأ: الإحداثيات خارج النطاق أو مرتبة بشكل غير صحيح (مطلوب خط العرض قبل خط الطول).",
|
||||
"invalid_format": "تنسيق غير صالح. استخدم 'lat,lng,lat,lng' أو 'lat lng lat lng'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "تُعتبر هذه المنطقة كبيرة جدًا وقد تتجاوز حدود الحوسبة النموذجية.",
|
||||
"area_extensive": "المنطقة واسعة جدًا وقد تتطلب الكثير من الوقت والموارد.",
|
||||
"selection_confirmed": "تم تأكيد التحديد!",
|
||||
"select_area_prompt": "حدد منطقة على الخريطة باستخدام الأدوات.",
|
||||
"unknown_error": "خطأ غير معروف",
|
||||
"license_and_credits": "الرخصة والمساهمون",
|
||||
"placeholder_bbox": "الصيغة: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/de.json
vendored
5
src/gui/locales/de.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Standort auswählen",
|
||||
"zoom_in_and_choose": "Zoome hinein und wähle dein Gebiet aus",
|
||||
"select_world": "Welt auswählen",
|
||||
"choose_world": "Welt wählen",
|
||||
"no_world_selected": "Keine Welt ausgewählt",
|
||||
"start_generation": "Generierung starten",
|
||||
"progress": "Fortschritt",
|
||||
"custom_selection_confirmed": "Benutzerdefinierte Auswahl bestätigt!",
|
||||
"error_coordinates_out_of_range": "Fehler: Koordinaten sind außerhalb des Bereichs oder falsch geordnet (Lat vor Lng erforderlich).",
|
||||
"invalid_format": "Ungültiges Format. Bitte verwende 'lat,lng,lat,lng' oder 'lat lng lat lng'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Dieses Gebiet ist sehr groß und könnte das Berechnungslimit überschreiten.",
|
||||
"area_extensive": "Diese Gebietsgröße könnte längere Zeit für die Generierung benötigen.",
|
||||
"selection_confirmed": "Auswahl bestätigt!",
|
||||
"select_area_prompt": "Wähle einen Bereich auf der Karte aus.",
|
||||
"unknown_error": "Unbekannter Fehler",
|
||||
"license_and_credits": "Lizenz und Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/en-US.json
vendored
5
src/gui/locales/en-US.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Select Location",
|
||||
"zoom_in_and_choose": "Zoom in and choose your area using the rectangle tool",
|
||||
"select_world": "Select World",
|
||||
"choose_world": "Choose World",
|
||||
"no_world_selected": "No world selected",
|
||||
"start_generation": "Start Generation",
|
||||
"progress": "Progress",
|
||||
"custom_selection_confirmed": "Custom selection confirmed!",
|
||||
"error_coordinates_out_of_range": "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).",
|
||||
"invalid_format": "Invalid format. Please use 'lat,lng,lat,lng' or 'lat lng lat lng'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "This area is very large and could exceed typical computing limits.",
|
||||
"area_extensive": "The area is quite extensive and may take significant time and resources.",
|
||||
"selection_confirmed": "Selection confirmed!",
|
||||
"select_area_prompt": "Select an area on the map using the tools.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/es.json
vendored
5
src/gui/locales/es.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Seleccionar ubicación",
|
||||
"zoom_in_and_choose": "Acércate y elige tu área usando la herramienta de rectángulo",
|
||||
"select_world": "Seleccionar mundo",
|
||||
"choose_world": "Elegir mundo",
|
||||
"no_world_selected": "Ningún mundo seleccionado",
|
||||
"start_generation": "Iniciar generación",
|
||||
"progress": "Progreso",
|
||||
"custom_selection_confirmed": "¡Selección personalizada confirmada!",
|
||||
"error_coordinates_out_of_range": "Error: Las coordenadas están fuera de rango o están ordenadas incorrectamente (Lat antes de Lng requerido).",
|
||||
"invalid_format": "Formato inválido. Por favor, use 'lat,lng,lat,lng' o 'lat lng lat lng'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Esta área es muy grande y podría exceder los límites típicos de computación.",
|
||||
"area_extensive": "El área es bastante extensa y puede requerir mucho tiempo y recursos.",
|
||||
"selection_confirmed": "¡Selección confirmada!",
|
||||
"select_area_prompt": "Selecciona un área en el mapa usando las herramientas.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/fi.json
vendored
5
src/gui/locales/fi.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Valitse paikka",
|
||||
"zoom_in_and_choose": "Zoomaa ja valitse paikka käyttämällä suorakulmatyökalua.",
|
||||
"select_world": "Valitse maailma",
|
||||
"choose_world": "Valitse maailma",
|
||||
"no_world_selected": "Maailmaa ei valittu",
|
||||
"start_generation": "Aloita generointi",
|
||||
"progress": "Edistys",
|
||||
"custom_selection_confirmed": "Mukautettu valinta vahvistettu!",
|
||||
"error_coordinates_out_of_range": "Virhe: Koordinaatit ovat kantaman ulkopuolella tai vääriin aseteltu (Lat ennen Lng vaadittu).",
|
||||
"invalid_format": "Väärä formaatti. Käytä 'lat,lng,lat,lng' tai 'lat lng lat lng'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Tämä alue on todella iso ja voi ylittää tyypilliset laskentarajat.",
|
||||
"area_extensive": "Alue on aika laaja ja voi viedä pitkän ajan ja resursseja.",
|
||||
"selection_confirmed": "Valinta vahvistettu!",
|
||||
"select_area_prompt": "Valitse alue kartalta työkaluilla.",
|
||||
"unknown_error": "Tuntematon virhe",
|
||||
"license_and_credits": "Lisenssi ja krediitit",
|
||||
"placeholder_bbox": "Formaatti: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/fr-FR.json
vendored
5
src/gui/locales/fr-FR.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Sélectionner une localisation",
|
||||
"zoom_in_and_choose": "Zoomez et choisissez votre zone avec l'outil rectangle",
|
||||
"select_world": "Sélectionner un monde",
|
||||
"choose_world": "Choisir un monde",
|
||||
"no_world_selected": "Aucun monde sélectionné",
|
||||
"start_generation": "Commencer la génération",
|
||||
"progress": "Progrès",
|
||||
"custom_selection_confirmed": "Sélection personnalisée confirmée !",
|
||||
"error_coordinates_out_of_range": "Erreur: Coordonnées hors de portée ou dans un ordre incorrect (besoin de la latitude avant la longitude).",
|
||||
"invalid_format": "Format invalide. Utilisez 'lat,lng,lat,lng' ou 'lat lng lat lng'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Cette zone est très grande et pourrait dépasser les limites de calcul courantes.",
|
||||
"area_extensive": "Cette zone est très étendue et pourrait nécessiter beaucoup de ressources et de temps.",
|
||||
"selection_confirmed": "Sélection confirmée !",
|
||||
"select_area_prompt": "Sélectionnez une zone sur la carte avec les outils.",
|
||||
"unknown_error": "Erreur inconnue",
|
||||
"license_and_credits": "Licence et crédits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/hu.json
vendored
5
src/gui/locales/hu.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Hely kiválasztása",
|
||||
"zoom_in_and_choose": "Nagyíts és jelöld ki a területet a kijelölő eszközzel",
|
||||
"select_world": "Világ kijelölése",
|
||||
"choose_world": "Világ kiválasztása",
|
||||
"no_world_selected": "Nincs világ kiválasztva",
|
||||
"start_generation": "Generálás indítása",
|
||||
"progress": "Haladás",
|
||||
"custom_selection_confirmed": "Egyéni kiválasztás megerősítve",
|
||||
"error_coordinates_out_of_range": "Hiba: A koordináták tartományon kívül vannak vagy hibásan rendezettek (a szélességi foknak a hosszúsági fok előtt kell lennie)",
|
||||
"invalid_format": "Érvénytelen formátum. Kérjük, használja a 'lat,lng,lat,lng' vagy a 'lat lng lat lng' formátumot.'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Ez a terület nagyon nagy, és meghaladhatja a szokásos számítási korlátokat.",
|
||||
"area_extensive": "A terület meglehetősen kiterjedt, és jelentős időt és erőforrásokat igényelhet.",
|
||||
"selection_confirmed": "Kiválasztás megerősítve",
|
||||
"select_area_prompt": "Jelölj ki egy területet a térképen az eszközökkel.",
|
||||
"unknown_error": "Ismeretlen hiba",
|
||||
"license_and_credits": "Licenc és elismerés.",
|
||||
"placeholder_bbox": "Formátum: lat,lng,lat,lng",
|
||||
|
||||
7
src/gui/locales/ko.json
vendored
7
src/gui/locales/ko.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "장소 선택",
|
||||
"zoom_in_and_choose": "줌 인하고 직사각형 도구를 사용하여 영역을 선택하세요.",
|
||||
"select_world": "세계 선택",
|
||||
"choose_world": "세계 선택",
|
||||
"no_world_selected": "선택된 세계 없음",
|
||||
"start_generation": "생성 시작",
|
||||
"progress": "진행",
|
||||
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
||||
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
||||
"invalid_format": "잘못된 형식입니다. 'lat,lng,lat,lng' 또는 'lat lng lat lng' 형식을 사용하세요.",
|
||||
@@ -24,9 +28,8 @@
|
||||
"select_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
||||
"select_location_first": "먼저 위치를 선택하세요!",
|
||||
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
||||
"area_extensive": "이 지역은 꽤 광범위하여 상당한 시간과 자원이 필요할 수 있습니다.",
|
||||
"area_extensive": "이 지역은 꽤 광범위하여 значитель한 시간과 자원이 필요할 수 있습니다.",
|
||||
"selection_confirmed": "선택이 확인되었습니다!",
|
||||
"select_area_prompt": "도구를 사용하여 지도에서 영역을 선택하세요.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/lt.json
vendored
5
src/gui/locales/lt.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Vietos pasirinkimas",
|
||||
"zoom_in_and_choose": "Pasididinkite žemėlapį ir pasirinkite plotą su kvadrato įrankiu",
|
||||
"select_world": "Pasaulio pasirinkimas",
|
||||
"choose_world": "Pasirinkti pasaulį",
|
||||
"no_world_selected": "Pasaulis nepasirinktas",
|
||||
"start_generation": "Pradėti generaciją",
|
||||
"progress": "Progresas",
|
||||
"custom_selection_confirmed": "Rėmo pasirinkimas patvirtintas!",
|
||||
"error_coordinates_out_of_range": "Klaida: Koordinatės yra už ribų arba neteisingai išdėstytos (plat turi būti prieš ilg).",
|
||||
"invalid_format": "Neteisingas formatas. Prašome naudoti 'plat,ilg,plat,ilg' arba 'plat ilg plat ilg'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Šis plotas yra labai didelis ir gali viršyti tipinius resursų limitus.",
|
||||
"area_extensive": "Šis plotas yra pakankamai didelis kuriam reikėtų daug laiko ir resursų.",
|
||||
"selection_confirmed": "Pasirinkimas patvirtintas!",
|
||||
"select_area_prompt": "Pasirinkite plotą žemėlapyje naudodami įrankius.",
|
||||
"unknown_error": "Nežinoma klaida",
|
||||
"license_and_credits": "Licencija ir padėkos",
|
||||
"placeholder_bbox": "Formatas: plat,lyg,plat,lyg",
|
||||
|
||||
5
src/gui/locales/lv.json
vendored
5
src/gui/locales/lv.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Izvēlēties atrašanās vietu",
|
||||
"zoom_in_and_choose": "Pietuviniet un izvēlieties apgabalu",
|
||||
"select_world": "Izvēlēties pasauli",
|
||||
"choose_world": "Izvēlēties pasauli",
|
||||
"no_world_selected": "Pasaulē nav izvēlēta",
|
||||
"start_generation": "Sākt ģenerēšanu",
|
||||
"progress": "Progress",
|
||||
"custom_selection_confirmed": "Pielāgota izvēle apstiprināta!",
|
||||
"error_coordinates_out_of_range": "Kļūda: koordinātas ir ārpus darbības zonas vai norādītas nepareizā secībā (vispirms platums, tad garums)",
|
||||
"invalid_format": "Nederīgs formāts. Izmantojiet 'platums,garums,platums,garums' vai 'platums garums platums garums'",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Šis apgabals ir pārāk liels un var pārsniegt tipiskos aprēķina ierobežojumus",
|
||||
"area_extensive": "Apgabals ir diezgan plašs un var prasīt ievērojamu laiku un resursus",
|
||||
"selection_confirmed": "Izvēle apstiprināta!",
|
||||
"select_area_prompt": "Izvēlieties apgabalu kartē, izmantojot rīkus.",
|
||||
"unknown_error": "Nezināma kļūda",
|
||||
"license_and_credits": "Licence un autori",
|
||||
"placeholder_bbox": "Formāts: platums,garums,platums,garums",
|
||||
|
||||
5
src/gui/locales/pl.json
vendored
5
src/gui/locales/pl.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Wybierz lokalizację",
|
||||
"zoom_in_and_choose": "Przybliż i zaznacz obszar za pomocą prostokąta",
|
||||
"select_world": "Wybierz świat",
|
||||
"choose_world": "Wybierz świat",
|
||||
"no_world_selected": "Nie wybrano świata",
|
||||
"start_generation": "Rozpocznij generowanie",
|
||||
"progress": "Postęp",
|
||||
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
||||
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub w złej kolejności (wymagana szerokość przed długością).",
|
||||
"invalid_format": "Nieprawidłowy format. Użyj 'szer.,dł.,szer.,dł.' lub 'szer. dł. szer. dł.'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć limity obliczeniowe.",
|
||||
"area_extensive": "Ten obszar jest rozległy i może pochłonąć dużo czasu oraz zasobów.",
|
||||
"selection_confirmed": "Wybór potwierdzony!",
|
||||
"select_area_prompt": "Zaznacz obszar na mapie za pomocą narzędzi.",
|
||||
"unknown_error": "Nieznany błąd",
|
||||
"license_and_credits": "Licencja i autorzy",
|
||||
"placeholder_bbox": "Format: szer,dł,szer,dł",
|
||||
|
||||
5
src/gui/locales/ru.json
vendored
5
src/gui/locales/ru.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Выбрать местоположение",
|
||||
"zoom_in_and_choose": "Приблизьте и выберите область",
|
||||
"select_world": "Выбрать мир",
|
||||
"choose_world": "Выбрать мир",
|
||||
"no_world_selected": "Мир не выбран",
|
||||
"start_generation": "Начать генерацию",
|
||||
"progress": "Прогресс",
|
||||
"custom_selection_confirmed": "Пользовательский выбор подтвержден!",
|
||||
"error_coordinates_out_of_range": "Ошибка: Координаты находятся вне зоны действия или указаны в неправильном порядке (сначала широта, затем долгота)",
|
||||
"invalid_format": "Неверный формат. Используйте 'широта,долгота,широта,долгота' или 'широта долгота широта долгота'",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Эта область слишком велика и может превысить типичные вычислительные ограничения",
|
||||
"area_extensive": "Область довольно обширна и может потребовать значительного времени и ресурсов",
|
||||
"selection_confirmed": "Выбор подтвержден!",
|
||||
"select_area_prompt": "Выберите область на карте с помощью инструментов.",
|
||||
"unknown_error": "Неизвестная ошибка",
|
||||
"license_and_credits": "Лицензия и авторы",
|
||||
"placeholder_bbox": "Формат: широта,долгота,широта,долгота",
|
||||
|
||||
5
src/gui/locales/sv.json
vendored
5
src/gui/locales/sv.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Välj plats",
|
||||
"zoom_in_and_choose": "Zooma in och välj ditt område med rektangulärt verktyg",
|
||||
"select_world": "Välj värld",
|
||||
"choose_world": "Välj värld",
|
||||
"no_world_selected": "Ingen värld vald",
|
||||
"start_generation": "Starta generering",
|
||||
"progress": "Framsteg",
|
||||
"custom_selection_confirmed": "Anpassad markering bekräftad!",
|
||||
"error_coordinates_out_of_range": "Fel: Koordinater är utanför området eller felaktigt ordnade (Lat före Lng krävs).",
|
||||
"invalid_format": "Ogiltigt format. Använd 'lat,lng,lat,lng' eller 'lat lng lat lng'.",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Detta område är mycket stort och kan överskrida vanliga beräkningsgränser.",
|
||||
"area_extensive": "Området är ganska extensivt och kan ta betydande tid och resurser.",
|
||||
"selection_confirmed": "Val bekräftat!",
|
||||
"select_area_prompt": "Välj ett område på kartan med verktygen.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/ua.json
vendored
5
src/gui/locales/ua.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "Обрати локацію",
|
||||
"zoom_in_and_choose": "Збільште і оберіть область за допомогою прямокутника",
|
||||
"select_world": "Обрати світ",
|
||||
"choose_world": "Обрати світ",
|
||||
"no_world_selected": "Світ не обрано",
|
||||
"start_generation": "Почати генерацію",
|
||||
"progress": "Прогрес",
|
||||
"custom_selection_confirmed": "Користувацький вибір підтверджено!",
|
||||
"error_coordinates_out_of_range": "Помилка: Координати поза діапазоном або неправильно впорядковані (потрібно широта перед довгота)",
|
||||
"invalid_format": "Неправильний формат. Будь ласка, використовуйте 'широта,довгота,широта,довгота' або 'широта довгота широта довгота'",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "Ця область дуже велика і може перевищити типові обчислювальні межі",
|
||||
"area_extensive": "Область досить велика і може вимагати значного часу та ресурсів",
|
||||
"selection_confirmed": "Вибір підтверджено!",
|
||||
"select_area_prompt": "Виберіть область на карті за допомогою інструментів.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/zh-CN.json
vendored
5
src/gui/locales/zh-CN.json
vendored
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"select_location": "选择位置",
|
||||
"zoom_in_and_choose": "放大并使用矩形工具选择您的区域",
|
||||
"select_world": "选择世界",
|
||||
"choose_world": "选择世界",
|
||||
"no_world_selected": "未选择世界",
|
||||
"start_generation": "开始生成",
|
||||
"progress": "进度",
|
||||
"custom_selection_confirmed": "自定义选择已确认!",
|
||||
"error_coordinates_out_of_range": "错误:坐标超出范围或顺序不正确(需要先纬度后经度)。",
|
||||
"invalid_format": "格式无效。请使用 'lat,lng,lat,lng' 或 'lat lng lat lng'。",
|
||||
@@ -26,7 +30,6 @@
|
||||
"area_too_large": "该区域非常大,可能会超出典型的计算限制。",
|
||||
"area_extensive": "该区域相当广泛,可能需要大量时间和资源。",
|
||||
"selection_confirmed": "选择已确认!",
|
||||
"select_area_prompt": "使用工具在地图上选择一个区域。",
|
||||
"unknown_error": "未知错误",
|
||||
"license_and_credits": "许可证和致谢",
|
||||
"placeholder_bbox": "格式: lat,lng,lat,lng",
|
||||
|
||||
2
src/gui/maps.html
vendored
2
src/gui/maps.html
vendored
@@ -26,7 +26,7 @@
|
||||
<div id="search-container">
|
||||
<div id="search-box">
|
||||
<input type="text" id="city-search" placeholder="Search for a city..." autocomplete="off" />
|
||||
<button id="search-btn" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg></button>
|
||||
<button id="search-btn">🔍</button>
|
||||
</div>
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
|
||||
@@ -54,9 +54,6 @@ fn run_cli() {
|
||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
||||
floodfill_cache::configure_rayon_thread_pool(0.9);
|
||||
|
||||
// Clean up old cached elevation tiles on startup
|
||||
elevation_data::cleanup_old_cached_tiles();
|
||||
|
||||
let version: &str = env!("CARGO_PKG_VERSION");
|
||||
let repository: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
println!(
|
||||
|
||||
@@ -272,17 +272,13 @@ pub fn parse_osm_data(
|
||||
continue;
|
||||
};
|
||||
|
||||
// Process multipolygons and boundary relations
|
||||
let relation_type = tags.get("type").map(|x: &String| x.as_str());
|
||||
if relation_type != Some("multipolygon") && relation_type != Some("boundary") {
|
||||
// Only process multipolygons for now
|
||||
if tags.get("type").map(|x: &String| x.as_str()) != Some("multipolygon") {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Water relations require unclipped ways for ring merging in water_areas.rs
|
||||
// Boundary relations also require unclipped ways for proper ring assembly
|
||||
let is_water_relation = is_water_element(tags);
|
||||
let is_boundary_relation = tags.contains_key("boundary");
|
||||
let keep_unclipped = is_water_relation || is_boundary_relation;
|
||||
|
||||
let members: Vec<ProcessedMember> = element
|
||||
.members
|
||||
@@ -308,9 +304,9 @@ pub fn parse_osm_data(
|
||||
}
|
||||
};
|
||||
|
||||
// Water and boundary relations: keep unclipped for ring merging
|
||||
// Other relations: clip member ways now
|
||||
let final_way = if keep_unclipped {
|
||||
// Water relations: keep unclipped for ring merging
|
||||
// Non-water relations: clip member ways now
|
||||
let final_way = if is_water_relation {
|
||||
way
|
||||
} else {
|
||||
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
|
||||
@@ -340,7 +336,7 @@ pub fn parse_osm_data(
|
||||
}
|
||||
}
|
||||
|
||||
emit_gui_progress_update(14.0, "");
|
||||
emit_gui_progress_update(15.0, "");
|
||||
|
||||
drop(nodes_map);
|
||||
drop(ways_map);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::osm_parser::OsmData;
|
||||
use crate::progress::{emit_gui_error, emit_gui_progress_update, is_running_with_gui};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use colored::Colorize;
|
||||
use rand::seq::SliceRandom;
|
||||
use reqwest::blocking::Client;
|
||||
@@ -38,22 +36,19 @@ fn download_with_reqwest(url: &str, query: &str) -> Result<String, Box<dyn std::
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_timeout() {
|
||||
let msg = "Request timed out. Try selecting a smaller area.";
|
||||
eprintln!("{}", format!("Error! {msg}").red().bold());
|
||||
Err(msg.into())
|
||||
} else if e.is_connect() {
|
||||
let msg = "No internet connection.";
|
||||
eprintln!("{}", format!("Error! {msg}").red().bold());
|
||||
Err(msg.into())
|
||||
} else {
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Error,
|
||||
&format!("Request error in download_with_reqwest: {e}"),
|
||||
eprintln!(
|
||||
"{}",
|
||||
"Error! Request timed out. Try selecting a smaller area."
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
emit_gui_error("Request timed out. Try selecting a smaller area.");
|
||||
} else {
|
||||
eprintln!("{}", format!("Error! {e:.52}").red().bold());
|
||||
Err(format!("{e:.52}").into())
|
||||
emit_gui_error(&format!("{:.52}", e.to_string()));
|
||||
}
|
||||
// Always propagate errors
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +132,6 @@ pub fn fetch_data_from_overpass(
|
||||
nwr["barrier"];
|
||||
nwr["entrance"];
|
||||
nwr["door"];
|
||||
nwr["boundary"];
|
||||
way;
|
||||
)->.relsinbbox;
|
||||
(
|
||||
|
||||
@@ -14,14 +14,11 @@ use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
|
||||
use bedrockrs_level::level::db_interface::bedrock_key::ChunkKey;
|
||||
use bedrockrs_level::level::db_interface::key_level::KeyTypeTag;
|
||||
use bedrockrs_level::level::db_interface::rusty::{mcpe_options, RustyDBInterface};
|
||||
use bedrockrs_level::level::db_interface::rusty::RustyDBInterface;
|
||||
use bedrockrs_level::level::file_interface::RawWorldTrait;
|
||||
use bedrockrs_shared::world::dimension::Dimension;
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use fastnbt::Value;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use rusty_leveldb::DB;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
use std::fs::{self, File};
|
||||
@@ -85,8 +82,6 @@ impl From<serde_json::Error> for BedrockSaveError {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_BEDROCK_COMPRESSION_LEVEL: u8 = 6;
|
||||
|
||||
/// Metadata for Bedrock worlds
|
||||
#[derive(Serialize)]
|
||||
struct BedrockMetadata {
|
||||
@@ -220,11 +215,8 @@ impl BedrockWriter {
|
||||
.ground
|
||||
.as_ref()
|
||||
.map(|ground| {
|
||||
// Ground elevation data expects coordinates relative to the XZ bbox origin
|
||||
let rel_x = spawn_x - xzbbox.min_x();
|
||||
let rel_z = spawn_z - xzbbox.min_z();
|
||||
let coord = crate::coordinate_system::cartesian::XZPoint::new(rel_x, rel_z);
|
||||
ground.level(coord) + 3 // Add 3 blocks above ground for safety
|
||||
let coord = crate::coordinate_system::cartesian::XZPoint::new(spawn_x, spawn_z);
|
||||
ground.level(coord) + 2 // Add 2 blocks above ground for safety
|
||||
})
|
||||
.unwrap_or(64);
|
||||
|
||||
@@ -407,7 +399,7 @@ impl BedrockWriter {
|
||||
// Open LevelDB with Bedrock-compatible options
|
||||
let mut state = ();
|
||||
let mut db: RustyDBInterface<()> =
|
||||
RustyDBInterface::new(db_path.clone().into_boxed_path(), true, &mut state)
|
||||
RustyDBInterface::new(db_path.into_boxed_path(), true, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Count total chunks for progress
|
||||
@@ -421,128 +413,63 @@ impl BedrockWriter {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
{
|
||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||
progress_bar.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||
progress_bar.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
|
||||
let mut chunks_processed: usize = 0;
|
||||
|
||||
// Process each region and chunk
|
||||
for ((region_x, region_z), region) in &world.regions {
|
||||
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
||||
// Calculate absolute chunk coordinates
|
||||
let abs_chunk_x = region_x * 32 + local_chunk_x;
|
||||
let abs_chunk_z = region_z * 32 + local_chunk_z;
|
||||
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
|
||||
|
||||
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
|
||||
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
||||
db.set_subchunk_raw(version_key, &[42], &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
||||
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
||||
let data3d = self.create_data3d(chunk);
|
||||
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Process each section (subchunk)
|
||||
for (§ion_y, section) in &chunk.sections {
|
||||
// Encode the subchunk
|
||||
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
|
||||
|
||||
// Write to database
|
||||
let subchunk_key =
|
||||
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
|
||||
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
chunks_processed += 1;
|
||||
progress_bar.inc(1);
|
||||
|
||||
// Update GUI progress (92% to 97% range for chunk writing)
|
||||
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
|
||||
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
|
||||
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
|
||||
emit_gui_progress_update(gui_progress, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress_bar.finish_with_message("Chunks written to LevelDB");
|
||||
}
|
||||
|
||||
// Ensure the RustyDBInterface handle is dropped before opening another DB for the same path.
|
||||
drop(db);
|
||||
|
||||
self.write_chunk_entities(world, &db_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_chunk_entities(
|
||||
&self,
|
||||
world: &WorldToModify,
|
||||
db_path: &std::path::Path,
|
||||
) -> Result<(), BedrockSaveError> {
|
||||
let mut opts = mcpe_options(DEFAULT_BEDROCK_COMPRESSION_LEVEL);
|
||||
opts.create_if_missing = true;
|
||||
let mut db = DB::open(db_path.to_path_buf().into_boxed_path(), opts)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
let mut chunks_processed: usize = 0;
|
||||
|
||||
// Process each region and chunk
|
||||
for ((region_x, region_z), region) in &world.regions {
|
||||
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
||||
let chunk_pos =
|
||||
Vec2::new(region_x * 32 + local_chunk_x, region_z * 32 + local_chunk_z);
|
||||
// Calculate absolute chunk coordinates
|
||||
let abs_chunk_x = region_x * 32 + local_chunk_x;
|
||||
let abs_chunk_z = region_z * 32 + local_chunk_z;
|
||||
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
|
||||
|
||||
self.write_compound_list_record(
|
||||
&mut db,
|
||||
chunk_pos,
|
||||
KeyTypeTag::BlockEntity,
|
||||
chunk.other.get("block_entities"),
|
||||
)?;
|
||||
self.write_compound_list_record(
|
||||
&mut db,
|
||||
chunk_pos,
|
||||
KeyTypeTag::Entity,
|
||||
chunk.other.get("entities"),
|
||||
)?;
|
||||
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
|
||||
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
||||
db.set_subchunk_raw(version_key, &[42], &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
||||
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
||||
let data3d = self.create_data3d(chunk);
|
||||
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Process each section (subchunk)
|
||||
for (§ion_y, section) in &chunk.sections {
|
||||
// Encode the subchunk
|
||||
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
|
||||
|
||||
// Write to database
|
||||
let subchunk_key =
|
||||
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
|
||||
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
chunks_processed += 1;
|
||||
progress_bar.inc(1);
|
||||
|
||||
// Update GUI progress (92% to 97% range for chunk writing)
|
||||
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
|
||||
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
|
||||
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
|
||||
emit_gui_progress_update(gui_progress, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
progress_bar.finish_with_message("Chunks written to LevelDB");
|
||||
|
||||
fn write_compound_list_record(
|
||||
&self,
|
||||
db: &mut DB,
|
||||
chunk_pos: Vec2<i32>,
|
||||
key_type: KeyTypeTag,
|
||||
value: Option<&Value>,
|
||||
) -> Result<(), BedrockSaveError> {
|
||||
let Some(Value::List(values)) = value else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let deduped = dedup_compound_list(values);
|
||||
if deduped.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = nbtx::to_le_bytes(&deduped).map_err(|e| BedrockSaveError::Nbt(e.to_string()))?;
|
||||
let key = build_chunk_key_bytes(chunk_pos, Dimension::Overworld, key_type, None);
|
||||
db.put(&key, &data)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
// LevelDB writes are flushed when the database is dropped
|
||||
drop(db);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -807,91 +734,6 @@ fn bedrock_bits_per_block(palette_count: u32) -> u8 {
|
||||
16 // Maximum
|
||||
}
|
||||
|
||||
fn build_chunk_key_bytes(
|
||||
chunk_pos: Vec2<i32>,
|
||||
dimension: Dimension,
|
||||
key_type: KeyTypeTag,
|
||||
y_index: Option<i8>,
|
||||
) -> Vec<u8> {
|
||||
let mut buffer = Vec::with_capacity(
|
||||
9 + if dimension != Dimension::Overworld {
|
||||
4
|
||||
} else {
|
||||
0
|
||||
} + 1,
|
||||
);
|
||||
buffer.extend_from_slice(&chunk_pos.x.to_le_bytes());
|
||||
buffer.extend_from_slice(&chunk_pos.y.to_le_bytes());
|
||||
|
||||
if dimension != Dimension::Overworld {
|
||||
buffer.extend_from_slice(&i32::from(dimension).to_le_bytes());
|
||||
}
|
||||
|
||||
buffer.push(key_type.to_byte());
|
||||
if let Some(y) = y_index {
|
||||
buffer.push(y as u8);
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
fn dedup_compound_list(values: &[Value]) -> Vec<Value> {
|
||||
let mut coord_index: StdHashMap<(i32, i32, i32), usize> = StdHashMap::new();
|
||||
let mut deduped: Vec<Value> = Vec::with_capacity(values.len());
|
||||
|
||||
for value in values {
|
||||
if let Value::Compound(map) = value {
|
||||
if let Some(coords) = get_entity_coords(map) {
|
||||
if let Some(idx) = coord_index.get(&coords).copied() {
|
||||
deduped[idx] = value.clone();
|
||||
continue;
|
||||
} else {
|
||||
coord_index.insert(coords, deduped.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
deduped.push(value.clone());
|
||||
}
|
||||
|
||||
deduped
|
||||
}
|
||||
|
||||
fn get_entity_coords(entity: &StdHashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||
if pos.len() == 3 {
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
value_to_i32(&pos[0]),
|
||||
value_to_i32(&pos[1]),
|
||||
value_to_i32(&pos[2]),
|
||||
) {
|
||||
return Some((x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(x), Some(y), Some(z)) = (
|
||||
entity.get("x").and_then(value_to_i32),
|
||||
entity.get("y").and_then(value_to_i32),
|
||||
entity.get("z").and_then(value_to_i32),
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((x, y, z))
|
||||
}
|
||||
|
||||
fn value_to_i32(value: &Value) -> Option<i32> {
|
||||
match value {
|
||||
Value::Byte(v) => Some(i32::from(*v)),
|
||||
Value::Short(v) => Some(i32::from(*v)),
|
||||
Value::Int(v) => Some(*v),
|
||||
Value::Long(v) => i32::try_from(*v).ok(),
|
||||
Value::Float(v) => Some(*v as i32),
|
||||
Value::Double(v) => Some(*v as i32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Level.dat structure for Bedrock Edition
|
||||
/// This struct contains all required fields for a valid Bedrock world
|
||||
#[derive(serde::Serialize)]
|
||||
|
||||
@@ -27,7 +27,7 @@ pub(crate) struct Chunk {
|
||||
}
|
||||
|
||||
/// Section within a chunk (16x16x16 blocks)
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct Section {
|
||||
pub block_states: Blockstates,
|
||||
#[serde(rename = "Y")]
|
||||
@@ -37,7 +37,7 @@ pub(crate) struct Section {
|
||||
}
|
||||
|
||||
/// Block states within a section
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct Blockstates {
|
||||
pub palette: Vec<PaletteItem>,
|
||||
pub data: Option<LongArray>,
|
||||
@@ -46,7 +46,7 @@ pub(crate) struct Blockstates {
|
||||
}
|
||||
|
||||
/// Palette item for block state encoding
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct PaletteItem {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
@@ -155,7 +155,7 @@ impl SectionToModify {
|
||||
let palette = unique_blocks
|
||||
.iter()
|
||||
.map(|(block, stored_props)| PaletteItem {
|
||||
name: format!("{}:{}", block.namespace(), block.name()),
|
||||
name: block.name().to_string(),
|
||||
properties: stored_props.clone().or_else(|| block.properties()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Java Edition Anvil format world saving.
|
||||
//!
|
||||
//! This module handles saving worlds in the Java Edition Anvil (.mca) format.
|
||||
//! Supports streaming mode for memory-efficient saving of large worlds.
|
||||
|
||||
use super::common::{Chunk, ChunkToModify, Section};
|
||||
use super::common::{Chunk, ChunkToModify, RegionToModify, Section};
|
||||
use super::WorldEditor;
|
||||
use crate::block_definitions::GRASS_BLOCK;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
@@ -15,83 +16,17 @@ use rayon::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Cached base chunk sections (grass at Y=-62)
|
||||
/// Computed once on first use and reused for all empty chunks
|
||||
static BASE_CHUNK_SECTIONS: OnceLock<Vec<Section>> = OnceLock::new();
|
||||
|
||||
/// Get or create the cached base chunk sections
|
||||
fn get_base_chunk_sections() -> &'static [Section] {
|
||||
BASE_CHUNK_SECTIONS.get_or_init(|| {
|
||||
let mut chunk = ChunkToModify::default();
|
||||
for x in 0..16 {
|
||||
for z in 0..16 {
|
||||
chunk.set_block(x, -62, z, GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
chunk.sections().collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
|
||||
impl<'a> WorldEditor<'a> {
|
||||
/// Creates a region file for the given region coordinates.
|
||||
pub(super) fn create_region(&self, region_x: i32, region_z: i32) -> Region<File> {
|
||||
let region_dir = self.world_dir.join("region");
|
||||
let out_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
||||
|
||||
// Ensure region directory exists before creating region files
|
||||
std::fs::create_dir_all(®ion_dir).expect("Failed to create region directory");
|
||||
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
|
||||
|
||||
let mut region_file: File = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&out_path)
|
||||
.expect("Failed to open region file");
|
||||
|
||||
region_file
|
||||
.write_all(REGION_TEMPLATE)
|
||||
.expect("Could not write region template");
|
||||
|
||||
Region::from_stream(region_file).expect("Failed to load region")
|
||||
}
|
||||
|
||||
/// Helper function to create a base chunk with grass blocks at Y -62
|
||||
/// Uses cached sections for efficiency - only serialization happens per chunk
|
||||
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
|
||||
// Use cached sections (computed once on first call)
|
||||
let sections = get_base_chunk_sections();
|
||||
|
||||
// Prepare chunk data with cloned sections
|
||||
let chunk_data = Chunk {
|
||||
sections: sections.to_vec(),
|
||||
x_pos: abs_chunk_x,
|
||||
z_pos: abs_chunk_z,
|
||||
is_light_on: 0,
|
||||
other: FnvHashMap::default(),
|
||||
};
|
||||
|
||||
// Create the Level wrapper
|
||||
let level_data = create_level_wrapper(&chunk_data);
|
||||
|
||||
// Serialize the chunk with Level wrapper
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
||||
|
||||
(ser_buffer, true)
|
||||
}
|
||||
|
||||
/// Saves the world in Java Edition Anvil format.
|
||||
///
|
||||
/// Uses parallel processing with rayon for fast region saving.
|
||||
/// Uses parallel processing: saves multiple regions concurrently for faster I/O,
|
||||
/// while still releasing memory after each region is processed.
|
||||
pub(super) fn save_java(&mut self) {
|
||||
println!("{} Saving world...", "[7/7]".bold());
|
||||
emit_gui_progress_update(90.0, "Saving world...");
|
||||
@@ -105,6 +40,12 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
|
||||
let total_regions = self.world.regions.len() as u64;
|
||||
|
||||
// Early return if no regions to save (prevents division by zero)
|
||||
if total_regions == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let save_pb = ProgressBar::new(total_regions);
|
||||
save_pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
@@ -115,220 +56,293 @@ impl<'a> WorldEditor<'a> {
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
|
||||
// Ensure region directory exists before parallel processing
|
||||
let region_dir = self.world_dir.join("region");
|
||||
std::fs::create_dir_all(®ion_dir).expect("Failed to create region directory");
|
||||
|
||||
// Drain all regions from memory into a Vec for parallel processing
|
||||
let regions_to_save: Vec<((i32, i32), super::common::RegionToModify)> =
|
||||
self.world.regions.drain().collect();
|
||||
|
||||
// Track progress atomically across threads
|
||||
let regions_processed = AtomicU64::new(0);
|
||||
let world_dir = self.world_dir.clone();
|
||||
|
||||
self.world
|
||||
.regions
|
||||
.par_iter()
|
||||
// Process regions in parallel, each region file is independent
|
||||
regions_to_save
|
||||
.into_par_iter()
|
||||
.for_each(|((region_x, region_z), region_to_modify)| {
|
||||
self.save_single_region(*region_x, *region_z, region_to_modify);
|
||||
// Save this region (creates its own file handle)
|
||||
save_region_to_file(&world_dir, region_x, region_z, ®ion_to_modify);
|
||||
|
||||
// Update progress
|
||||
let regions_done = regions_processed.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
// Update progress atomically
|
||||
let processed = regions_processed.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
save_pb.inc(1);
|
||||
|
||||
// Update progress at regular intervals (every ~10% or at least every 10 regions)
|
||||
// Emit GUI progress update periodically
|
||||
let update_interval = (total_regions / 10).max(1);
|
||||
if regions_done.is_multiple_of(update_interval) || regions_done == total_regions {
|
||||
let progress = 90.0 + (regions_done as f64 / total_regions as f64) * 9.0;
|
||||
if processed.is_multiple_of(update_interval) || processed == total_regions {
|
||||
let progress = 90.0 + (processed as f64 / total_regions as f64) * 9.0;
|
||||
emit_gui_progress_update(progress, "Saving world...");
|
||||
}
|
||||
|
||||
save_pb.inc(1);
|
||||
// Region memory is freed when region_to_modify goes out of scope here
|
||||
});
|
||||
|
||||
save_pb.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves a single region to disk.
|
||||
///
|
||||
/// Optimized for new world creation, writes chunks directly without reading existing data.
|
||||
/// This assumes we're creating a fresh world, not modifying an existing one.
|
||||
fn save_single_region(
|
||||
&self,
|
||||
region_x: i32,
|
||||
region_z: i32,
|
||||
region_to_modify: &super::common::RegionToModify,
|
||||
) {
|
||||
let mut region = self.create_region(region_x, region_z);
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
/// Saves a single region to a file (thread-safe, for parallel processing).
|
||||
///
|
||||
/// This is a standalone function that can be called from parallel threads
|
||||
/// since it only needs the world directory path, not a reference to WorldEditor.
|
||||
fn save_region_to_file(
|
||||
world_dir: &Path,
|
||||
region_x: i32,
|
||||
region_z: i32,
|
||||
region_to_modify: &RegionToModify,
|
||||
) {
|
||||
// Create region file
|
||||
let region_dir = world_dir.join("region");
|
||||
let out_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
||||
|
||||
// First pass: write all chunks that have content
|
||||
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
||||
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
||||
// Create chunk directly, we're writing to a fresh region file
|
||||
// so there's no existing data to preserve
|
||||
let chunk = Chunk {
|
||||
sections: chunk_to_modify.sections().collect(),
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
|
||||
|
||||
let mut region_file: File = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&out_path)
|
||||
.expect("Failed to open region file");
|
||||
|
||||
region_file
|
||||
.write_all(REGION_TEMPLATE)
|
||||
.expect("Could not write region template");
|
||||
|
||||
let mut region = Region::from_stream(region_file).expect("Failed to load region");
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
|
||||
// First pass: write modified chunks
|
||||
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
||||
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
||||
// Read existing chunk data if it exists
|
||||
let existing_data = region
|
||||
.read_chunk(chunk_x as usize, chunk_z as usize)
|
||||
.unwrap()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Parse existing chunk or create new one
|
||||
let mut chunk: Chunk = if !existing_data.is_empty() {
|
||||
fastnbt::from_bytes(&existing_data).unwrap()
|
||||
} else {
|
||||
Chunk {
|
||||
sections: Vec::new(),
|
||||
x_pos: chunk_x + (region_x * 32),
|
||||
z_pos: chunk_z + (region_z * 32),
|
||||
is_light_on: 0,
|
||||
other: chunk_to_modify.other.clone(),
|
||||
};
|
||||
other: FnvHashMap::default(),
|
||||
}
|
||||
};
|
||||
|
||||
// Create Level wrapper and save
|
||||
let level_data = create_level_wrapper(&chunk);
|
||||
ser_buffer.clear();
|
||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
||||
// Update sections while preserving existing data
|
||||
let new_sections: Vec<Section> = chunk_to_modify.sections().collect();
|
||||
for new_section in new_sections {
|
||||
if let Some(existing_section) =
|
||||
chunk.sections.iter_mut().find(|s| s.y == new_section.y)
|
||||
{
|
||||
// Merge block states
|
||||
existing_section.block_states.palette = new_section.block_states.palette;
|
||||
existing_section.block_states.data = new_section.block_states.data;
|
||||
} else {
|
||||
// Add new section if it doesn't exist
|
||||
chunk.sections.push(new_section);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve existing block entities and merge with new ones
|
||||
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
||||
if let (Value::List(existing), Value::List(new)) =
|
||||
(existing_entities, new_entities)
|
||||
{
|
||||
// Remove old entities that are replaced by new ones
|
||||
existing.retain(|e| {
|
||||
if let Value::Compound(map) = e {
|
||||
let (x, y, z) = get_entity_coords(map);
|
||||
!new.iter().any(|new_e| {
|
||||
if let Value::Compound(new_map) = new_e {
|
||||
let (nx, ny, nz) = get_entity_coords(new_map);
|
||||
x == nx && y == ny && z == nz
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Add new entities
|
||||
existing.extend(new.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no existing entities, just add the new ones
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
||||
chunk
|
||||
.other
|
||||
.insert("block_entities".to_string(), new_entities.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Update chunk coordinates and flags
|
||||
chunk.x_pos = chunk_x + (region_x * 32);
|
||||
chunk.z_pos = chunk_z + (region_z * 32);
|
||||
|
||||
// Create Level wrapper and save
|
||||
let level_data = create_level_wrapper(&chunk);
|
||||
ser_buffer.clear();
|
||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
||||
region
|
||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: ensure all chunks exist (create base chunks for empty slots)
|
||||
for chunk_x in 0..32 {
|
||||
for chunk_z in 0..32 {
|
||||
let abs_chunk_x = chunk_x + (region_x * 32);
|
||||
let abs_chunk_z = chunk_z + (region_z * 32);
|
||||
|
||||
// Check if chunk exists in our modifications
|
||||
let chunk_exists = region_to_modify.chunks.contains_key(&(chunk_x, chunk_z));
|
||||
|
||||
// If chunk doesn't exist, create it with base layer
|
||||
if !chunk_exists {
|
||||
let (ser_buffer, _) = create_base_chunk(abs_chunk_x, abs_chunk_z);
|
||||
region
|
||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: ensure all chunks exist (fill with base layer if not)
|
||||
for chunk_x in 0..32 {
|
||||
for chunk_z in 0..32 {
|
||||
let abs_chunk_x = chunk_x + (region_x * 32);
|
||||
let abs_chunk_z = chunk_z + (region_z * 32);
|
||||
|
||||
// Check if chunk exists in our modifications
|
||||
let chunk_exists = region_to_modify.chunks.contains_key(&(chunk_x, chunk_z));
|
||||
|
||||
// If chunk doesn't exist, create it with base layer
|
||||
if !chunk_exists {
|
||||
let (ser_buffer, _) = Self::create_base_chunk(abs_chunk_x, abs_chunk_z);
|
||||
region
|
||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to get entity coordinates
|
||||
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||
if pos.len() == 3 {
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
value_to_i32(&pos[0]),
|
||||
value_to_i32(&pos[1]),
|
||||
value_to_i32(&pos[2]),
|
||||
) {
|
||||
return Some((x, y, z));
|
||||
}
|
||||
/// Helper function to create a base chunk with grass blocks at Y -62 (standalone version)
|
||||
fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
|
||||
let mut chunk = ChunkToModify::default();
|
||||
|
||||
// Fill the bottom layer with grass blocks at Y -62
|
||||
for x in 0..16 {
|
||||
for z in 0..16 {
|
||||
chunk.set_block(x, -62, z, GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(x), Some(y), Some(z)) = (
|
||||
entity.get("x").and_then(value_to_i32),
|
||||
entity.get("y").and_then(value_to_i32),
|
||||
entity.get("z").and_then(value_to_i32),
|
||||
) else {
|
||||
return None;
|
||||
// Prepare chunk data
|
||||
let chunk_data = Chunk {
|
||||
sections: chunk.sections().collect(),
|
||||
x_pos: abs_chunk_x,
|
||||
z_pos: abs_chunk_z,
|
||||
is_light_on: 0,
|
||||
other: chunk.other,
|
||||
};
|
||||
|
||||
Some((x, y, z))
|
||||
// Create the Level wrapper
|
||||
let level_data = create_level_wrapper(&chunk_data);
|
||||
|
||||
// Serialize the chunk with Level wrapper
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
||||
|
||||
(ser_buffer, true)
|
||||
}
|
||||
|
||||
/// Helper function to get entity coordinates
|
||||
#[inline]
|
||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
|
||||
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
|
||||
*x
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let y = if let Value::Int(y) = entity.get("y").unwrap_or(&Value::Int(0)) {
|
||||
*y
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
|
||||
*z
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(x, y, z)
|
||||
}
|
||||
|
||||
/// Creates a Level wrapper for chunk data (Java Edition format)
|
||||
#[inline]
|
||||
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
||||
let mut level_map = HashMap::from([
|
||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||
(
|
||||
"isLightOn".to_string(),
|
||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
||||
),
|
||||
(
|
||||
"sections".to_string(),
|
||||
Value::List(
|
||||
chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let mut block_states = HashMap::from([(
|
||||
"palette".to_string(),
|
||||
Value::List(
|
||||
section
|
||||
.block_states
|
||||
.palette
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let mut palette_item = HashMap::from([(
|
||||
"Name".to_string(),
|
||||
Value::String(item.name.clone()),
|
||||
)]);
|
||||
if let Some(props) = &item.properties {
|
||||
palette_item
|
||||
.insert("Properties".to_string(), props.clone());
|
||||
}
|
||||
Value::Compound(palette_item)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)]);
|
||||
|
||||
// Only add the `data` attribute if it's non-empty
|
||||
// to maintain compatibility with third-party tools like Dynmap
|
||||
if let Some(data) = §ion.block_states.data {
|
||||
if !data.is_empty() {
|
||||
block_states
|
||||
.insert("data".to_string(), Value::LongArray(data.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
Value::Compound(HashMap::from([
|
||||
("Y".to_string(), Value::Byte(section.y)),
|
||||
("block_states".to_string(), Value::Compound(block_states)),
|
||||
]))
|
||||
})
|
||||
.collect(),
|
||||
HashMap::from([(
|
||||
"Level".to_string(),
|
||||
Value::Compound(HashMap::from([
|
||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||
(
|
||||
"isLightOn".to_string(),
|
||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
||||
),
|
||||
),
|
||||
]);
|
||||
(
|
||||
"sections".to_string(),
|
||||
Value::List(
|
||||
chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let mut block_states = HashMap::from([(
|
||||
"palette".to_string(),
|
||||
Value::List(
|
||||
section
|
||||
.block_states
|
||||
.palette
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let mut palette_item = HashMap::from([(
|
||||
"Name".to_string(),
|
||||
Value::String(item.name.clone()),
|
||||
)]);
|
||||
if let Some(props) = &item.properties {
|
||||
palette_item.insert(
|
||||
"Properties".to_string(),
|
||||
props.clone(),
|
||||
);
|
||||
}
|
||||
Value::Compound(palette_item)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)]);
|
||||
|
||||
for (key, value) in &chunk.other {
|
||||
level_map.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
HashMap::from([("Level".to_string(), Value::Compound(level_map))])
|
||||
}
|
||||
|
||||
/// Merge compound lists (entities, block_entities) from chunk_to_modify into chunk
|
||||
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||
#[allow(dead_code)]
|
||||
fn merge_compound_list(chunk: &mut Chunk, chunk_to_modify: &ChunkToModify, key: &str) {
|
||||
if let Some(existing_entities) = chunk.other.get_mut(key) {
|
||||
if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
||||
if let (Value::List(existing), Value::List(new)) = (existing_entities, new_entities) {
|
||||
existing.retain(|e| {
|
||||
if let Value::Compound(map) = e {
|
||||
if let Some((x, y, z)) = get_entity_coords(map) {
|
||||
return !new.iter().any(|new_e| {
|
||||
if let Value::Compound(new_map) = new_e {
|
||||
get_entity_coords(new_map) == Some((x, y, z))
|
||||
} else {
|
||||
false
|
||||
// Only add the `data` attribute if it's non-empty
|
||||
// to maintain compatibility with third-party tools like Dynmap
|
||||
if let Some(data) = §ion.block_states.data {
|
||||
if !data.is_empty() {
|
||||
block_states.insert(
|
||||
"data".to_string(),
|
||||
Value::LongArray(data.to_owned()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
existing.extend(new.clone());
|
||||
}
|
||||
}
|
||||
} else if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
||||
chunk.other.insert(key.to_string(), new_entities.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert NBT Value to i32
|
||||
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||
#[allow(dead_code)]
|
||||
fn value_to_i32(value: &Value) -> Option<i32> {
|
||||
match value {
|
||||
Value::Byte(v) => Some(i32::from(*v)),
|
||||
Value::Short(v) => Some(i32::from(*v)),
|
||||
Value::Int(v) => Some(*v),
|
||||
Value::Long(v) => i32::try_from(*v).ok(),
|
||||
Value::Float(v) => Some(*v as i32),
|
||||
Value::Double(v) => Some(*v as i32),
|
||||
_ => None,
|
||||
}
|
||||
Value::Compound(HashMap::from([
|
||||
("Y".to_string(), Value::Byte(section.y)),
|
||||
("block_states".to_string(), Value::Compound(block_states)),
|
||||
]))
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
])),
|
||||
)])
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use colored::Colorize;
|
||||
use fastnbt::{IntArray, Value};
|
||||
use fastnbt::Value;
|
||||
use serde::Serialize;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
@@ -151,19 +151,6 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ground level at a specific world coordinate (without any offset)
|
||||
#[inline(always)]
|
||||
pub fn get_ground_level(&self, x: i32, z: i32) -> i32 {
|
||||
if let Some(ground) = &self.ground {
|
||||
ground.level(XZPoint::new(
|
||||
x - self.xzbbox.min_x(),
|
||||
z - self.xzbbox.min_z(),
|
||||
))
|
||||
} else {
|
||||
0 // Default ground level if no terrain data
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the minimum world coordinates
|
||||
pub fn get_min_coords(&self) -> (i32, i32) {
|
||||
(self.xzbbox.min_x(), self.xzbbox.min_z())
|
||||
@@ -243,212 +230,6 @@ impl<'a> WorldEditor<'a> {
|
||||
self.set_block(SIGN, x, y, z, None, None);
|
||||
}
|
||||
|
||||
/// Adds an entity at the given coordinates (Y is ground-relative).
|
||||
#[allow(dead_code)]
|
||||
pub fn add_entity(
|
||||
&mut self,
|
||||
id: &str,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
extra_data: Option<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
|
||||
let mut entity = HashMap::new();
|
||||
entity.insert("id".to_string(), Value::String(id.to_string()));
|
||||
entity.insert(
|
||||
"Pos".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Double(x as f64 + 0.5),
|
||||
Value::Double(absolute_y as f64),
|
||||
Value::Double(z as f64 + 0.5),
|
||||
]),
|
||||
);
|
||||
entity.insert(
|
||||
"Motion".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
]),
|
||||
);
|
||||
entity.insert(
|
||||
"Rotation".to_string(),
|
||||
Value::List(vec![Value::Float(0.0), Value::Float(0.0)]),
|
||||
);
|
||||
entity.insert("OnGround".to_string(), Value::Byte(1));
|
||||
entity.insert("FallDistance".to_string(), Value::Float(0.0));
|
||||
entity.insert("Fire".to_string(), Value::Short(-20));
|
||||
entity.insert("Air".to_string(), Value::Short(300));
|
||||
entity.insert("PortalCooldown".to_string(), Value::Int(0));
|
||||
entity.insert(
|
||||
"UUID".to_string(),
|
||||
Value::IntArray(build_deterministic_uuid(id, x, absolute_y, z)),
|
||||
);
|
||||
|
||||
if let Some(extra) = extra_data {
|
||||
for (key, value) in extra {
|
||||
entity.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(entity));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(entity)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Places a chest with the provided items at the given coordinates (ground-relative Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_chest_with_items(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
self.set_chest_with_items_absolute(x, absolute_y, z, items);
|
||||
}
|
||||
|
||||
/// Places a chest with the provided items at the given coordinates (absolute Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_chest_with_items_absolute(
|
||||
&mut self,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let mut chest_data = HashMap::new();
|
||||
chest_data.insert(
|
||||
"id".to_string(),
|
||||
Value::String("minecraft:chest".to_string()),
|
||||
);
|
||||
chest_data.insert("x".to_string(), Value::Int(x));
|
||||
chest_data.insert("y".to_string(), Value::Int(absolute_y));
|
||||
chest_data.insert("z".to_string(), Value::Int(z));
|
||||
chest_data.insert(
|
||||
"Items".to_string(),
|
||||
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||
);
|
||||
chest_data.insert("keepPacked".to_string(), Value::Byte(0));
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("block_entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(chest_data));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(chest_data)]));
|
||||
}
|
||||
}
|
||||
|
||||
self.set_block_absolute(CHEST, x, absolute_y, z, None, None);
|
||||
}
|
||||
|
||||
/// Places a block entity with items at the given coordinates (ground-relative Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_block_entity_with_items(
|
||||
&mut self,
|
||||
block_with_props: BlockWithProperties,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
block_entity_id: &str,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
self.set_block_entity_with_items_absolute(
|
||||
block_with_props,
|
||||
x,
|
||||
absolute_y,
|
||||
z,
|
||||
block_entity_id,
|
||||
items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Places a block entity with items at the given coordinates (absolute Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_block_entity_with_items_absolute(
|
||||
&mut self,
|
||||
block_with_props: BlockWithProperties,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
block_entity_id: &str,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let mut block_entity = HashMap::new();
|
||||
block_entity.insert("id".to_string(), Value::String(block_entity_id.to_string()));
|
||||
block_entity.insert("x".to_string(), Value::Int(x));
|
||||
block_entity.insert("y".to_string(), Value::Int(absolute_y));
|
||||
block_entity.insert("z".to_string(), Value::Int(z));
|
||||
block_entity.insert(
|
||||
"Items".to_string(),
|
||||
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||
);
|
||||
block_entity.insert("keepPacked".to_string(), Value::Byte(0));
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("block_entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(block_entity));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(block_entity)]));
|
||||
}
|
||||
}
|
||||
|
||||
self.set_block_with_properties_absolute(block_with_props, x, absolute_y, z, None, None);
|
||||
}
|
||||
|
||||
/// Sets a block of the specified type at the given coordinates.
|
||||
///
|
||||
/// Y value is interpreted as an offset from ground level.
|
||||
@@ -805,30 +586,3 @@ impl<'a> WorldEditor<'a> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn build_deterministic_uuid(id: &str, x: i32, y: i32, z: i32) -> IntArray {
|
||||
let mut hash: i64 = 17;
|
||||
for byte in id.bytes() {
|
||||
hash = hash.wrapping_mul(31).wrapping_add(byte as i64);
|
||||
}
|
||||
|
||||
let seed_a = hash ^ (x as i64).wrapping_shl(32) ^ (y as i64).wrapping_mul(17);
|
||||
let seed_b = hash.rotate_left(7) ^ (z as i64).wrapping_mul(31) ^ (x as i64).wrapping_mul(13);
|
||||
|
||||
IntArray::new(vec![
|
||||
(seed_a >> 32) as i32,
|
||||
seed_a as i32,
|
||||
(seed_b >> 32) as i32,
|
||||
seed_b as i32,
|
||||
])
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn single_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Arnis",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.0",
|
||||
"identifier": "com.louisdev.arnis",
|
||||
"build": {
|
||||
"frontendDist": "src/gui"
|
||||
|
||||
Reference in New Issue
Block a user