mirror of
https://github.com/louis-e/arnis.git
synced 2025-12-24 06:48:00 -05:00
Compare commits
84 Commits
copilot/fi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d97391820 | ||
|
|
bef3cfb090 | ||
|
|
5a898944f7 | ||
|
|
9fdd960009 | ||
|
|
58e4a337d9 | ||
|
|
236a7e5af9 | ||
|
|
9173e5b4de | ||
|
|
1fd02d8005 | ||
|
|
438b2beceb | ||
|
|
a62e181c16 | ||
|
|
12abba3bc8 | ||
|
|
a8e31700d8 | ||
|
|
7a109cce0b | ||
|
|
86543714af | ||
|
|
b84a565210 | ||
|
|
93becaae7f | ||
|
|
06e377ce29 | ||
|
|
e22380bdd3 | ||
|
|
35cac44209 | ||
|
|
61af45d2f4 | ||
|
|
393f1f9bd8 | ||
|
|
e6f8466177 | ||
|
|
02d3a32a03 | ||
|
|
f00304ff3a | ||
|
|
a93b908104 | ||
|
|
7cbc4fa263 | ||
|
|
7e7f7ed476 | ||
|
|
3c0ba60657 | ||
|
|
fb438c4a0f | ||
|
|
5015c8b9b4 | ||
|
|
af0ace422f | ||
|
|
0bb39b7d9e | ||
|
|
5b5e93b89a | ||
|
|
958dc2107e | ||
|
|
562a3bca66 | ||
|
|
f1b37fbbb6 | ||
|
|
b34cbf4307 | ||
|
|
a03318bb98 | ||
|
|
8bb779d6cc | ||
|
|
6d164102ad | ||
|
|
127a0e5e68 | ||
|
|
4a326c3dad | ||
|
|
d4fd9b9cd3 | ||
|
|
ee0521f232 | ||
|
|
8b3a41b131 | ||
|
|
02594b1cae | ||
|
|
06ba4db97e | ||
|
|
59d31cfbb8 | ||
|
|
94388e4164 | ||
|
|
f8c9fd8f4c | ||
|
|
2ee2d48f6a | ||
|
|
56c2f2e5cd | ||
|
|
9d34bc8e92 | ||
|
|
c95b78fdcd | ||
|
|
6e52e08b8a | ||
|
|
57a4a801cf | ||
|
|
0c47e365bc | ||
|
|
dad3ab3b34 | ||
|
|
b8b63a2bc5 | ||
|
|
cab20b5e50 | ||
|
|
0e879837fa | ||
|
|
92be2ccf00 | ||
|
|
3b76d707d9 | ||
|
|
be8559dee7 | ||
|
|
94eda2fad3 | ||
|
|
7d86854e3c | ||
|
|
cddaa89d35 | ||
|
|
453845977d | ||
|
|
4e196e51bd | ||
|
|
ea4dc5dc08 | ||
|
|
c56ff83094 | ||
|
|
2b40a520ff | ||
|
|
a192be981a | ||
|
|
eb77bca10d | ||
|
|
4a891c3603 | ||
|
|
84adfdd931 | ||
|
|
823b6ba052 | ||
|
|
2ba8157ec9 | ||
|
|
7235ba0be9 | ||
|
|
dee580c564 | ||
|
|
41fc5662e0 | ||
|
|
ac884b8c2a | ||
|
|
7a9b792bee | ||
|
|
83e9a634e5 |
4
.github/workflows/ci-build.yml
vendored
4
.github/workflows/ci-build.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
|
||||
12
.github/workflows/pr-benchmark.yml
vendored
12
.github/workflows/pr-benchmark.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
seconds=$((duration % 60))
|
||||
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
||||
|
||||
baseline_time=135
|
||||
baseline_time=30
|
||||
diff=$((duration - baseline_time))
|
||||
abs_diff=${diff#-}
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
verdict="🚨 This PR **drastically worsens generation time**."
|
||||
fi
|
||||
|
||||
baseline_mem=5865
|
||||
baseline_mem=935
|
||||
mem_annotation=""
|
||||
if [ "$peak_mem" -gt 2000 ]; then
|
||||
mem_diff=$((peak_mem - baseline_mem))
|
||||
@@ -87,6 +87,8 @@ jobs:
|
||||
mem_annotation=" (↗ ${mem_percent}% more)"
|
||||
fi
|
||||
|
||||
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
{
|
||||
echo "summary<<EOF"
|
||||
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
|
||||
@@ -98,6 +100,8 @@ jobs:
|
||||
echo ""
|
||||
echo "${verdict}"
|
||||
echo ""
|
||||
echo "📅 **Last benchmark:** ${benchmark_time}"
|
||||
echo ""
|
||||
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
@@ -108,4 +112,4 @@ jobs:
|
||||
message: ${{ steps.comment_body.outputs.summary }}
|
||||
comment-tag: benchmark-report
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Windows build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/wiki
|
||||
*.mcworld
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
346
Cargo.lock
generated
346
Cargo.lock
generated
@@ -25,7 +25,7 @@ version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
@@ -182,8 +182,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arnis"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bedrockrs_level",
|
||||
"bedrockrs_shared",
|
||||
"byteorder",
|
||||
"clap",
|
||||
"colored",
|
||||
"dirs",
|
||||
@@ -197,6 +201,7 @@ dependencies = [
|
||||
"indicatif",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"nbtx",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"rayon",
|
||||
@@ -211,7 +216,9 @@ dependencies = [
|
||||
"tauri-plugin-shell",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"vek",
|
||||
"windows",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -297,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
@@ -343,7 +350,7 @@ dependencies = [
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix 0.38.42",
|
||||
@@ -370,7 +377,7 @@ dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix 0.38.42",
|
||||
@@ -466,6 +473,73 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bedrockrs_core"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
|
||||
|
||||
[[package]]
|
||||
name = "bedrockrs_level"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
|
||||
dependencies = [
|
||||
"bedrockrs_core",
|
||||
"bedrockrs_shared",
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"concat-idents",
|
||||
"len-trait",
|
||||
"miniz_oxide",
|
||||
"nbtx",
|
||||
"rusty-leveldb",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"uuid",
|
||||
"vek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bedrockrs_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bedrockrs_proto_core"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bedrockrs_macros",
|
||||
"byteorder",
|
||||
"jsonwebtoken",
|
||||
"nbtx",
|
||||
"paste",
|
||||
"seq-macro",
|
||||
"serde_json",
|
||||
"thiserror 2.0.9",
|
||||
"uuid",
|
||||
"varint-rs",
|
||||
"vek",
|
||||
"xuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bedrockrs_shared"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
|
||||
dependencies = [
|
||||
"bedrockrs_macros",
|
||||
"bedrockrs_proto_core",
|
||||
"byteorder",
|
||||
"log",
|
||||
"varint-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
@@ -636,9 +710,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.21.0"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -766,6 +840,12 @@ dependencies = [
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -862,6 +942,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concat-idents"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -959,13 +1049,28 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1141,7 +1246,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1295,7 +1400,7 @@ version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1351,6 +1456,17 @@ dependencies = [
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.10"
|
||||
@@ -1361,6 +1477,16 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
@@ -1469,7 +1595,7 @@ version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -1824,7 +1950,7 @@ version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
@@ -1835,9 +1961,11 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1846,7 +1974,7 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
@@ -2035,7 +2163,7 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
@@ -2549,6 +2677,12 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "integer-encoding"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02"
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
@@ -2654,7 +2788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
@@ -2716,6 +2850,21 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.7.0"
|
||||
@@ -2751,6 +2900,15 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "len-trait"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "723558ab8acaa07cb831b424cd164b587ddc1648b34748a30953c404e9a4a65b"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libappindicator"
|
||||
version = "0.9.0"
|
||||
@@ -2797,7 +2955,7 @@ version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -2923,7 +3081,7 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
@@ -3013,6 +3171,18 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nbtx"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/bedrock-crustaceans/nbtx#551c38ac74f2e68a07d3dbdd354faac0c0ac966e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"paste",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"varint-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -3056,7 +3226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
@@ -3482,7 +3652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
@@ -3592,7 +3762,7 @@ version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
@@ -3611,6 +3781,16 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -3812,7 +3992,7 @@ version = "3.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
@@ -4136,7 +4316,7 @@ dependencies = [
|
||||
"av1-grain",
|
||||
"bitstream-io",
|
||||
"built",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"interpolate_name",
|
||||
"itertools 0.12.1",
|
||||
"libc",
|
||||
@@ -4342,7 +4522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"untrusted",
|
||||
@@ -4427,7 +4607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"errno 0.3.10",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -4440,7 +4620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"errno 0.3.10",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.4",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -4491,6 +4671,20 @@ version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "rusty-leveldb"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c48d2f060dd1286adc9c3d179cb5af1292a9d2fcf291abcfe056023fc1977b44"
|
||||
dependencies = [
|
||||
"crc",
|
||||
"errno 0.2.8",
|
||||
"fs2",
|
||||
"integer-encoding",
|
||||
"rand 0.8.5",
|
||||
"snap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
@@ -4603,19 +4797,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
name = "seq-macro"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
@@ -4640,10 +4842,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4782,7 +4993,7 @@ version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
@@ -4833,6 +5044,18 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror 2.0.9",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
@@ -4854,6 +5077,12 @@ version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "snap"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
@@ -5430,7 +5659,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.7",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5801,7 +6030,7 @@ version = "1.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
@@ -5980,12 +6209,31 @@ version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
||||
|
||||
[[package]]
|
||||
name = "varint-rs"
|
||||
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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25215c4675beead435b254fc510932ff7f519cbc585b1b9fe2539ee9f20ca331"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -6064,7 +6312,7 @@ version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
@@ -6090,7 +6338,7 @@ version = "0.4.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -6750,7 +6998,7 @@ version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -6860,6 +7108,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xuid"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cc57e8e1819a2c06319a1387a6f1b0f8148a0221d17694a43ae63b60f407f0"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
@@ -7017,6 +7271,18 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -14,12 +14,15 @@ overflow-checks = true
|
||||
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {version = "2", optional = true}
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
dirs = {version = "6.0.0", optional = true }
|
||||
@@ -38,13 +41,18 @@ rand = "0.8.5"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.15.4", optional = true }
|
||||
semver = "1.0.26"
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "2", optional = true }
|
||||
tauri-plugin-log = { version = "2.6.0", optional = true }
|
||||
tauri-plugin-shell = { version = "2", optional = true }
|
||||
tokio = { version = "1.48.0", features = ["full"], optional = true }
|
||||
bedrockrs_level = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_level", optional = true }
|
||||
bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_shared", optional = true }
|
||||
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 }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Arnis [](https://github.com/louis-e/arnis/actions) [<img alt="GitHub Release" src="https://img.shields.io/github/v/release/louis-e/arnis" />](https://github.com/louis-e/arnis/releases) [<img alt="GitHub Downloads (all assets, all releases" src="https://img.shields.io/github/downloads/louis-e/arnis/total" />](https://github.com/louis-e/arnis/releases) [](https://github.com/louis-e/arnis/releases) [](https://discord.gg/mA2g69Fhxq)
|
||||
|
||||
Arnis creates complex and accurate Minecraft Java Edition worlds that reflect real-world geography, topography, and architecture.
|
||||
Arnis creates complex and accurate Minecraft Java Edition (1.17+) and Bedrock Edition worlds that reflect real-world geography, topography, and architecture.
|
||||
|
||||
This free and open source project is designed to handle large-scale geographic data from the real world and generate detailed Minecraft worlds. The algorithm processes geospatial data from OpenStreetMap as well as elevation data to create an accurate Minecraft representation of terrain and architecture.
|
||||
Generate your hometown, big cities, and natural landscapes with ease!
|
||||
|
||||
BIN
assets/minecraft/world_icon.jpeg
Normal file
BIN
assets/minecraft/world_icon.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
849
src/bedrock_block_map.rs
Normal file
849
src/bedrock_block_map.rs
Normal file
@@ -0,0 +1,849 @@
|
||||
//! Bedrock Block Mapping
|
||||
//!
|
||||
//! This module provides translation between the internal Block representation
|
||||
//! and Bedrock Edition block format. Bedrock uses string identifiers with
|
||||
//! state properties that differ slightly from Java Edition.
|
||||
|
||||
use crate::block_definitions::Block;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents a Bedrock block with its identifier and state properties.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BedrockBlock {
|
||||
/// The Bedrock block identifier (e.g., "minecraft:stone")
|
||||
pub name: String,
|
||||
/// Block state properties as key-value pairs
|
||||
pub states: HashMap<String, BedrockBlockStateValue>,
|
||||
}
|
||||
|
||||
/// Bedrock block state values can be strings, booleans, or integers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BedrockBlockStateValue {
|
||||
String(String),
|
||||
Bool(bool),
|
||||
Int(i32),
|
||||
}
|
||||
|
||||
impl BedrockBlock {
|
||||
/// Creates a simple block with no state properties.
|
||||
pub fn simple(name: &str) -> Self {
|
||||
Self {
|
||||
name: format!("minecraft:{name}"),
|
||||
states: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a block with state properties.
|
||||
pub fn with_states(name: &str, states: Vec<(&str, BedrockBlockStateValue)>) -> Self {
|
||||
let mut state_map = HashMap::new();
|
||||
for (key, value) in states {
|
||||
state_map.insert(key.to_string(), value);
|
||||
}
|
||||
Self {
|
||||
name: format!("minecraft:{name}"),
|
||||
states: state_map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an internal Block to a BedrockBlock representation.
|
||||
///
|
||||
/// This function handles the mapping between Java Edition block names/properties
|
||||
/// and their Bedrock Edition equivalents. Many blocks are identical, but some
|
||||
/// require translation of property names or values.
|
||||
pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
let java_name = block.name();
|
||||
|
||||
// Most blocks have the same name in both editions
|
||||
// Handle special cases first, then fall back to direct mapping
|
||||
match java_name {
|
||||
// Grass block is just "grass_block" in both editions
|
||||
"grass_block" => BedrockBlock::simple("grass_block"),
|
||||
|
||||
// Short grass is just "short_grass" in Java but "tallgrass" in Bedrock
|
||||
"short_grass" => BedrockBlock::with_states(
|
||||
"tallgrass",
|
||||
vec![(
|
||||
"tall_grass_type",
|
||||
BedrockBlockStateValue::String("tall".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Tall grass needs height state
|
||||
"tall_grass" => BedrockBlock::with_states(
|
||||
"double_plant",
|
||||
vec![(
|
||||
"double_plant_type",
|
||||
BedrockBlockStateValue::String("grass".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Oak leaves with persistence
|
||||
"oak_leaves" => BedrockBlock::with_states(
|
||||
"leaves",
|
||||
vec![
|
||||
(
|
||||
"old_leaf_type",
|
||||
BedrockBlockStateValue::String("oak".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Birch leaves with persistence
|
||||
"birch_leaves" => BedrockBlock::with_states(
|
||||
"leaves",
|
||||
vec![
|
||||
(
|
||||
"old_leaf_type",
|
||||
BedrockBlockStateValue::String("birch".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Oak log with axis (default up_down)
|
||||
"oak_log" => BedrockBlock::with_states(
|
||||
"oak_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Birch log with axis
|
||||
"birch_log" => BedrockBlock::with_states(
|
||||
"birch_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Spruce log with axis
|
||||
"spruce_log" => BedrockBlock::with_states(
|
||||
"spruce_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Stone slab (bottom half by default)
|
||||
"stone_slab" => BedrockBlock::with_states(
|
||||
"stone_block_slab",
|
||||
vec![
|
||||
(
|
||||
"stone_slab_type",
|
||||
BedrockBlockStateValue::String("smooth_stone".to_string()),
|
||||
),
|
||||
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||
],
|
||||
),
|
||||
|
||||
// Stone brick slab
|
||||
"stone_brick_slab" => BedrockBlock::with_states(
|
||||
"stone_block_slab",
|
||||
vec![
|
||||
(
|
||||
"stone_slab_type",
|
||||
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||
),
|
||||
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||
],
|
||||
),
|
||||
|
||||
// Oak slab
|
||||
"oak_slab" => BedrockBlock::with_states(
|
||||
"wooden_slab",
|
||||
vec![
|
||||
(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("oak".to_string()),
|
||||
),
|
||||
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||
],
|
||||
),
|
||||
|
||||
// Water (flowing by default)
|
||||
"water" => BedrockBlock::with_states(
|
||||
"water",
|
||||
vec![("liquid_depth", BedrockBlockStateValue::Int(0))],
|
||||
),
|
||||
|
||||
// Rail with shape state
|
||||
"rail" => BedrockBlock::with_states(
|
||||
"rail",
|
||||
vec![("rail_direction", BedrockBlockStateValue::Int(0))],
|
||||
),
|
||||
|
||||
// Farmland with moisture
|
||||
"farmland" => BedrockBlock::with_states(
|
||||
"farmland",
|
||||
vec![("moisturized_amount", BedrockBlockStateValue::Int(7))],
|
||||
),
|
||||
|
||||
// Snow layer
|
||||
"snow" => BedrockBlock::with_states(
|
||||
"snow_layer",
|
||||
vec![("height", BedrockBlockStateValue::Int(0))],
|
||||
),
|
||||
|
||||
// Cobblestone wall
|
||||
"cobblestone_wall" => BedrockBlock::with_states(
|
||||
"cobblestone_wall",
|
||||
vec![(
|
||||
"wall_block_type",
|
||||
BedrockBlockStateValue::String("cobblestone".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Andesite wall
|
||||
"andesite_wall" => BedrockBlock::with_states(
|
||||
"cobblestone_wall",
|
||||
vec![(
|
||||
"wall_block_type",
|
||||
BedrockBlockStateValue::String("andesite".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Stone brick wall
|
||||
"stone_brick_wall" => BedrockBlock::with_states(
|
||||
"cobblestone_wall",
|
||||
vec![(
|
||||
"wall_block_type",
|
||||
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Flowers - poppy is just "red_flower" in Bedrock
|
||||
"poppy" => BedrockBlock::with_states(
|
||||
"red_flower",
|
||||
vec![(
|
||||
"flower_type",
|
||||
BedrockBlockStateValue::String("poppy".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Dandelion is "yellow_flower" in Bedrock
|
||||
"dandelion" => BedrockBlock::simple("yellow_flower"),
|
||||
|
||||
// Blue orchid
|
||||
"blue_orchid" => BedrockBlock::with_states(
|
||||
"red_flower",
|
||||
vec![(
|
||||
"flower_type",
|
||||
BedrockBlockStateValue::String("orchid".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Azure bluet
|
||||
"azure_bluet" => BedrockBlock::with_states(
|
||||
"red_flower",
|
||||
vec![(
|
||||
"flower_type",
|
||||
BedrockBlockStateValue::String("houstonia".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Concrete colors (Bedrock uses a single block with color state)
|
||||
"white_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"black_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||
),
|
||||
"gray_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||
),
|
||||
"light_gray_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("silver".to_string()),
|
||||
)],
|
||||
),
|
||||
"light_blue_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||
)],
|
||||
),
|
||||
"cyan_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||
),
|
||||
"blue_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||
),
|
||||
"purple_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("purple".to_string()),
|
||||
)],
|
||||
),
|
||||
"magenta_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("magenta".to_string()),
|
||||
)],
|
||||
),
|
||||
"red_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
"orange_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("orange".to_string()),
|
||||
)],
|
||||
),
|
||||
"yellow_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
"lime_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("lime".to_string()))],
|
||||
),
|
||||
"brown_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
|
||||
// Terracotta colors
|
||||
"white_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"orange_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("orange".to_string()),
|
||||
)],
|
||||
),
|
||||
"yellow_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
"light_blue_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||
)],
|
||||
),
|
||||
"blue_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||
),
|
||||
"gray_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||
),
|
||||
"green_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||
),
|
||||
"red_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
"brown_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
"black_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||
),
|
||||
// Plain terracotta
|
||||
"terracotta" => BedrockBlock::simple("hardened_clay"),
|
||||
|
||||
// Wool colors
|
||||
"white_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"red_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
"green_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||
),
|
||||
"brown_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
"cyan_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||
),
|
||||
"yellow_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Carpets
|
||||
"white_carpet" => BedrockBlock::with_states(
|
||||
"carpet",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"red_carpet" => BedrockBlock::with_states(
|
||||
"carpet",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
|
||||
// Stained glass
|
||||
"white_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"gray_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||
),
|
||||
"light_gray_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("silver".to_string()),
|
||||
)],
|
||||
),
|
||||
"brown_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
|
||||
// Planks - Bedrock uses single "planks" block with wood_type state
|
||||
"oak_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("oak".to_string()),
|
||||
)],
|
||||
),
|
||||
"spruce_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("spruce".to_string()),
|
||||
)],
|
||||
),
|
||||
"birch_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("birch".to_string()),
|
||||
)],
|
||||
),
|
||||
"jungle_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("jungle".to_string()),
|
||||
)],
|
||||
),
|
||||
"acacia_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("acacia".to_string()),
|
||||
)],
|
||||
),
|
||||
"dark_oak_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("dark_oak".to_string()),
|
||||
)],
|
||||
),
|
||||
"crimson_planks" => BedrockBlock::simple("crimson_planks"),
|
||||
"warped_planks" => BedrockBlock::simple("warped_planks"),
|
||||
|
||||
// Stone variants
|
||||
"stone" => BedrockBlock::simple("stone"),
|
||||
"granite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("granite".to_string()),
|
||||
)],
|
||||
),
|
||||
"polished_granite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("granite_smooth".to_string()),
|
||||
)],
|
||||
),
|
||||
"diorite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("diorite".to_string()),
|
||||
)],
|
||||
),
|
||||
"polished_diorite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("diorite_smooth".to_string()),
|
||||
)],
|
||||
),
|
||||
"andesite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("andesite".to_string()),
|
||||
)],
|
||||
),
|
||||
"polished_andesite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("andesite_smooth".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Blocks with different names in Bedrock
|
||||
"bricks" => BedrockBlock::simple("brick_block"),
|
||||
"end_stone_bricks" => BedrockBlock::simple("end_bricks"),
|
||||
"nether_bricks" => BedrockBlock::simple("nether_brick"),
|
||||
"red_nether_bricks" => BedrockBlock::simple("red_nether_brick"),
|
||||
"snow_block" => BedrockBlock::simple("snow"),
|
||||
"dirt_path" => BedrockBlock::simple("grass_path"),
|
||||
"dead_bush" => BedrockBlock::simple("deadbush"),
|
||||
"note_block" => BedrockBlock::simple("noteblock"),
|
||||
|
||||
// Oak items mapped to dark_oak in Bedrock (or generic equivalents)
|
||||
"oak_pressure_plate" => BedrockBlock::simple("wooden_pressure_plate"),
|
||||
"oak_door" => BedrockBlock::simple("wooden_door"),
|
||||
"oak_trapdoor" => BedrockBlock::simple("trapdoor"),
|
||||
|
||||
// Bed (Bedrock uses single "bed" block with color state)
|
||||
"red_bed" => BedrockBlock::with_states(
|
||||
"bed",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
|
||||
// Default: use the same name (works for many blocks)
|
||||
// Log unmapped blocks to help identify missing mappings
|
||||
_ => BedrockBlock::simple(java_name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an internal Block with optional Java properties to a BedrockBlock.
|
||||
///
|
||||
/// This function extends `to_bedrock_block` by also handling block-specific properties
|
||||
/// like stair facing/shape, slab type, etc. Java property names and values are converted
|
||||
/// to their Bedrock equivalents.
|
||||
pub fn to_bedrock_block_with_properties(
|
||||
block: Block,
|
||||
java_properties: Option<&fastnbt::Value>,
|
||||
) -> BedrockBlock {
|
||||
let java_name = block.name();
|
||||
|
||||
// Extract Java properties as a map if present
|
||||
let props_map = java_properties.and_then(|v| {
|
||||
if let fastnbt::Value::Compound(map) = v {
|
||||
Some(map)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Handle stairs with facing/shape properties
|
||||
if java_name.ends_with("_stairs") {
|
||||
return convert_stairs(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);
|
||||
}
|
||||
|
||||
// Handle logs with axis property
|
||||
if java_name.ends_with("_log") || java_name.ends_with("_wood") {
|
||||
return convert_log(java_name, props_map);
|
||||
}
|
||||
|
||||
// Fall back to basic conversion without properties
|
||||
to_bedrock_block(block)
|
||||
}
|
||||
|
||||
/// Convert Java stair block to Bedrock format with proper orientation.
|
||||
fn convert_stairs(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
// Map Java stair names to Bedrock equivalents
|
||||
let bedrock_name = match java_name {
|
||||
"end_stone_brick_stairs" => "end_brick_stairs",
|
||||
_ => java_name, // Most stairs have the same name
|
||||
};
|
||||
|
||||
let mut states = HashMap::new();
|
||||
|
||||
// Convert facing: Java uses "north/south/east/west", Bedrock uses "weirdo_direction" (0-3)
|
||||
// Bedrock: 0=east, 1=west, 2=south, 3=north
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||
let direction = match facing.as_str() {
|
||||
"east" => 0,
|
||||
"west" => 1,
|
||||
"south" => 2,
|
||||
"north" => 3,
|
||||
_ => 0,
|
||||
};
|
||||
states.insert(
|
||||
"weirdo_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(direction),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert half: Java uses "top/bottom", Bedrock uses "upside_down_bit"
|
||||
if let Some(fastnbt::Value::String(half)) = props.get("half") {
|
||||
let upside_down = half == "top";
|
||||
states.insert(
|
||||
"upside_down_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(upside_down),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If no properties were set, use defaults
|
||||
if states.is_empty() {
|
||||
states.insert(
|
||||
"weirdo_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(0),
|
||||
);
|
||||
states.insert(
|
||||
"upside_down_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(false),
|
||||
);
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java slab block to Bedrock format with proper type.
|
||||
fn convert_slab(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let mut states = HashMap::new();
|
||||
|
||||
// Convert type: Java uses "top/bottom/double", Bedrock uses "top_slot_bit"
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(slab_type)) = props.get("type") {
|
||||
let top_slot = slab_type == "top";
|
||||
states.insert(
|
||||
"top_slot_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(top_slot),
|
||||
);
|
||||
// Note: "double" slabs in Java become full blocks in Bedrock (different block ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Default to bottom if not specified
|
||||
if !states.contains_key("top_slot_bit") {
|
||||
states.insert(
|
||||
"top_slot_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(false),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle special slab name mappings (same as in to_bedrock_block)
|
||||
let bedrock_name = match java_name {
|
||||
"stone_slab" => "stone_block_slab",
|
||||
"stone_brick_slab" => "stone_block_slab",
|
||||
"oak_slab" => "wooden_slab",
|
||||
"spruce_slab" => "wooden_slab",
|
||||
"birch_slab" => "wooden_slab",
|
||||
"jungle_slab" => "wooden_slab",
|
||||
"acacia_slab" => "wooden_slab",
|
||||
"dark_oak_slab" => "wooden_slab",
|
||||
_ => java_name,
|
||||
};
|
||||
|
||||
// Add wood_type for wooden slabs
|
||||
if bedrock_name == "wooden_slab" {
|
||||
let wood_type = java_name.trim_end_matches("_slab");
|
||||
states.insert(
|
||||
"wood_type".to_string(),
|
||||
BedrockBlockStateValue::String(wood_type.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// Add stone_slab_type for stone slabs
|
||||
if bedrock_name == "stone_block_slab" {
|
||||
let slab_type = if java_name == "stone_brick_slab" {
|
||||
"stone_brick"
|
||||
} else {
|
||||
"stone"
|
||||
};
|
||||
states.insert(
|
||||
"stone_slab_type".to_string(),
|
||||
BedrockBlockStateValue::String(slab_type.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java log/wood block to Bedrock format with proper axis.
|
||||
fn convert_log(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let bedrock_name = java_name;
|
||||
let mut states = HashMap::new();
|
||||
|
||||
// Convert axis: Java uses "x/y/z", Bedrock uses "pillar_axis"
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(axis)) = props.get("axis") {
|
||||
states.insert(
|
||||
"pillar_axis".to_string(),
|
||||
BedrockBlockStateValue::String(axis.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to y-axis if not specified
|
||||
if states.is_empty() {
|
||||
states.insert(
|
||||
"pillar_axis".to_string(),
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::block_definitions::{AIR, GRASS_BLOCK, STONE};
|
||||
|
||||
#[test]
|
||||
fn test_simple_blocks() {
|
||||
let bedrock = to_bedrock_block(STONE);
|
||||
assert_eq!(bedrock.name, "minecraft:stone");
|
||||
assert!(bedrock.states.is_empty());
|
||||
|
||||
let bedrock = to_bedrock_block(AIR);
|
||||
assert_eq!(bedrock.name, "minecraft:air");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grass_block() {
|
||||
let bedrock = to_bedrock_block(GRASS_BLOCK);
|
||||
assert_eq!(bedrock.name, "minecraft:grass_block");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_colored_blocks() {
|
||||
use crate::block_definitions::WHITE_CONCRETE;
|
||||
let bedrock = to_bedrock_block(WHITE_CONCRETE);
|
||||
assert_eq!(bedrock.name, "minecraft:concrete");
|
||||
assert!(matches!(
|
||||
bedrock.states.get("color"),
|
||||
Some(BedrockBlockStateValue::String(s)) if s == "white"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stairs_with_properties() {
|
||||
use crate::block_definitions::OAK_STAIRS;
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
|
||||
// Create Java properties for a south-facing stair
|
||||
let mut props = StdHashMap::new();
|
||||
props.insert(
|
||||
"facing".to_string(),
|
||||
fastnbt::Value::String("south".to_string()),
|
||||
);
|
||||
props.insert(
|
||||
"half".to_string(),
|
||||
fastnbt::Value::String("bottom".to_string()),
|
||||
);
|
||||
let java_props = fastnbt::Value::Compound(props);
|
||||
|
||||
let bedrock = to_bedrock_block_with_properties(OAK_STAIRS, Some(&java_props));
|
||||
assert_eq!(bedrock.name, "minecraft:oak_stairs");
|
||||
|
||||
// Check weirdo_direction is set correctly (south = 2)
|
||||
assert!(matches!(
|
||||
bedrock.states.get("weirdo_direction"),
|
||||
Some(BedrockBlockStateValue::Int(2))
|
||||
));
|
||||
|
||||
// Check upside_down_bit is false for bottom half
|
||||
assert!(matches!(
|
||||
bedrock.states.get("upside_down_bit"),
|
||||
Some(BedrockBlockStateValue::Bool(false))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stairs_upside_down() {
|
||||
use crate::block_definitions::STONE_BRICK_STAIRS;
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
|
||||
// Create Java properties for an upside-down north-facing stair
|
||||
let mut props = StdHashMap::new();
|
||||
props.insert(
|
||||
"facing".to_string(),
|
||||
fastnbt::Value::String("north".to_string()),
|
||||
);
|
||||
props.insert(
|
||||
"half".to_string(),
|
||||
fastnbt::Value::String("top".to_string()),
|
||||
);
|
||||
let java_props = fastnbt::Value::Compound(props);
|
||||
|
||||
let bedrock = to_bedrock_block_with_properties(STONE_BRICK_STAIRS, Some(&java_props));
|
||||
|
||||
// Check weirdo_direction is set correctly (north = 3)
|
||||
assert!(matches!(
|
||||
bedrock.states.get("weirdo_direction"),
|
||||
Some(BedrockBlockStateValue::Int(3))
|
||||
));
|
||||
|
||||
// Check upside_down_bit is true for top half
|
||||
assert!(matches!(
|
||||
bedrock.states.get("upside_down_bit"),
|
||||
Some(BedrockBlockStateValue::Bool(true))
|
||||
));
|
||||
}
|
||||
}
|
||||
706
src/clipping.rs
Normal file
706
src/clipping.rs
Normal file
@@ -0,0 +1,706 @@
|
||||
// Sutherland-Hodgman polygon clipping and related geometry utilities.
|
||||
//
|
||||
// Provides bbox clipping for polygons, polylines, and water rings with
|
||||
// proper corner insertion for closed shapes.
|
||||
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Clips a way to the bounding box using Sutherland-Hodgman for polygons or
|
||||
/// simple line clipping for polylines. Preserves endpoint IDs for ring assembly.
|
||||
pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
|
||||
if nodes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let is_closed = is_closed_polygon(nodes);
|
||||
|
||||
if !is_closed {
|
||||
return clip_polyline_to_bbox(nodes, xzbbox);
|
||||
}
|
||||
|
||||
// If all nodes are inside the bbox, return unchanged
|
||||
let has_nodes_outside = nodes
|
||||
.iter()
|
||||
.any(|node| !xzbbox.contains(&XZPoint::new(node.x, node.z)));
|
||||
|
||||
if !has_nodes_outside {
|
||||
return nodes.to_vec();
|
||||
}
|
||||
|
||||
let min_x = xzbbox.min_x() as f64;
|
||||
let min_z = xzbbox.min_z() as f64;
|
||||
let max_x = xzbbox.max_x() as f64;
|
||||
let max_z = xzbbox.max_z() as f64;
|
||||
|
||||
let mut polygon: Vec<(f64, f64)> = nodes.iter().map(|n| (n.x as f64, n.z as f64)).collect();
|
||||
|
||||
polygon = clip_polygon_sutherland_hodgman(polygon, min_x, min_z, max_x, max_z);
|
||||
|
||||
if polygon.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Final clamping for floating-point errors
|
||||
for p in &mut polygon {
|
||||
p.0 = p.0.clamp(min_x, max_x);
|
||||
p.1 = p.1.clamp(min_z, max_z);
|
||||
}
|
||||
|
||||
let polygon = remove_consecutive_duplicates(polygon);
|
||||
if polygon.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Clips a water polygon ring to bbox using Sutherland-Hodgman (post-ring-merge).
|
||||
pub fn clip_water_ring_to_bbox(
|
||||
ring: &[ProcessedNode],
|
||||
xzbbox: &XZBBox,
|
||||
) -> Option<Vec<ProcessedNode>> {
|
||||
if ring.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let min_x = xzbbox.min_x() as f64;
|
||||
let min_z = xzbbox.min_z() as f64;
|
||||
let max_x = xzbbox.max_x() as f64;
|
||||
let max_z = xzbbox.max_z() as f64;
|
||||
|
||||
// Check if entire ring is inside bbox
|
||||
let all_inside = ring.iter().all(|n| {
|
||||
n.x as f64 >= min_x && n.x as f64 <= max_x && n.z as f64 >= min_z && n.z as f64 <= max_z
|
||||
});
|
||||
|
||||
if all_inside {
|
||||
return Some(ring.to_vec());
|
||||
}
|
||||
|
||||
// Check if entire ring is outside bbox
|
||||
if is_ring_outside_bbox(ring, min_x, min_z, max_x, max_z) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Convert to f64 coordinates and ensure closed
|
||||
let mut polygon: Vec<(f64, f64)> = ring.iter().map(|n| (n.x as f64, n.z as f64)).collect();
|
||||
if !polygon.is_empty() && polygon.first() != polygon.last() {
|
||||
polygon.push(polygon[0]);
|
||||
}
|
||||
|
||||
// Clip with full-range clamping (water uses simpler approach)
|
||||
polygon = clip_polygon_sutherland_hodgman_simple(polygon, min_x, min_z, max_x, max_z);
|
||||
|
||||
if polygon.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Verify all points are within bbox
|
||||
let all_points_inside = polygon
|
||||
.iter()
|
||||
.all(|&(x, z)| x >= min_x && x <= max_x && z >= min_z && z <= max_z);
|
||||
|
||||
if !all_points_inside {
|
||||
eprintln!("ERROR: clip_water_ring_to_bbox produced points outside bbox!");
|
||||
return None;
|
||||
}
|
||||
|
||||
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||
if polygon.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Convert back to ProcessedNode with synthetic IDs
|
||||
let mut result: Vec<ProcessedNode> = polygon
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &(x, z))| ProcessedNode {
|
||||
id: 1_000_000_000 + i as u64,
|
||||
tags: HashMap::new(),
|
||||
x: x.clamp(min_x, max_x).round() as i32,
|
||||
z: z.clamp(min_z, max_z).round() as i32,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Close the loop by matching first and last ID
|
||||
if !result.is_empty() {
|
||||
let first_id = result[0].id;
|
||||
result.last_mut().unwrap().id = first_id;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Checks if a way forms a closed polygon.
|
||||
fn is_closed_polygon(nodes: &[ProcessedNode]) -> bool {
|
||||
if nodes.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
let first = nodes.first().unwrap();
|
||||
let last = nodes.last().unwrap();
|
||||
first.id == last.id || (first.x == last.x && first.z == last.z)
|
||||
}
|
||||
|
||||
/// Checks if an entire ring is outside the bbox.
|
||||
fn is_ring_outside_bbox(
|
||||
ring: &[ProcessedNode],
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> bool {
|
||||
let all_left = ring.iter().all(|n| (n.x as f64) < min_x);
|
||||
let all_right = ring.iter().all(|n| (n.x as f64) > max_x);
|
||||
let all_top = ring.iter().all(|n| (n.z as f64) < min_z);
|
||||
let all_bottom = ring.iter().all(|n| (n.z as f64) > max_z);
|
||||
all_left || all_right || all_top || all_bottom
|
||||
}
|
||||
|
||||
/// Clips a polyline (open path) to the bounding box.
|
||||
fn clip_polyline_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
|
||||
if nodes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let min_x = xzbbox.min_x() as f64;
|
||||
let min_z = xzbbox.min_z() as f64;
|
||||
let max_x = xzbbox.max_x() as f64;
|
||||
let max_z = xzbbox.max_z() as f64;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for i in 0..nodes.len() {
|
||||
let current = &nodes[i];
|
||||
let current_point = (current.x as f64, current.z as f64);
|
||||
let current_inside = point_in_bbox(current_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
if current_inside {
|
||||
result.push(current.clone());
|
||||
}
|
||||
|
||||
if i + 1 < nodes.len() {
|
||||
let next = &nodes[i + 1];
|
||||
let next_point = (next.x as f64, next.z as f64);
|
||||
let next_inside = point_in_bbox(next_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
if current_inside != next_inside {
|
||||
// One endpoint inside, one outside, find single intersection
|
||||
let intersections =
|
||||
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
for intersection in intersections {
|
||||
let synthetic_id = nodes[0]
|
||||
.id
|
||||
.wrapping_mul(10000000)
|
||||
.wrapping_add(result.len() as u64);
|
||||
result.push(ProcessedNode {
|
||||
id: synthetic_id,
|
||||
x: intersection.0.round() as i32,
|
||||
z: intersection.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
});
|
||||
}
|
||||
} else if !current_inside && !next_inside {
|
||||
// Both endpoints outside, segment might still cross through bbox
|
||||
let mut intersections =
|
||||
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
if intersections.len() >= 2 {
|
||||
// Sort intersections by distance from current point
|
||||
intersections.sort_by(|a, b| {
|
||||
let dist_a =
|
||||
(a.0 - current_point.0).powi(2) + (a.1 - current_point.1).powi(2);
|
||||
let dist_b =
|
||||
(b.0 - current_point.0).powi(2) + (b.1 - current_point.1).powi(2);
|
||||
dist_a
|
||||
.partial_cmp(&dist_b)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
for intersection in intersections {
|
||||
let synthetic_id = nodes[0]
|
||||
.id
|
||||
.wrapping_mul(10000000)
|
||||
.wrapping_add(result.len() as u64);
|
||||
result.push(ProcessedNode {
|
||||
id: synthetic_id,
|
||||
x: intersection.0.round() as i32,
|
||||
z: intersection.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve endpoint IDs where possible
|
||||
if result.len() >= 2 {
|
||||
let tolerance = 50.0;
|
||||
if let Some(first_orig) = nodes.first() {
|
||||
if matches_endpoint(
|
||||
(result[0].x as f64, result[0].z as f64),
|
||||
first_orig,
|
||||
tolerance,
|
||||
) {
|
||||
result[0].id = first_orig.id;
|
||||
}
|
||||
}
|
||||
if let Some(last_orig) = nodes.last() {
|
||||
let last_idx = result.len() - 1;
|
||||
if matches_endpoint(
|
||||
(result[last_idx].x as f64, result[last_idx].z as f64),
|
||||
last_orig,
|
||||
tolerance,
|
||||
) {
|
||||
result[last_idx].id = last_orig.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Sutherland-Hodgman polygon clipping with edge-specific clamping.
|
||||
fn clip_polygon_sutherland_hodgman(
|
||||
mut polygon: Vec<(f64, f64)>,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
// Edges: bottom, right, top, left (counter-clockwise traversal)
|
||||
let bbox_edges = [
|
||||
(min_x, min_z, max_x, min_z, 0), // Bottom: clamp z
|
||||
(max_x, min_z, max_x, max_z, 1), // Right: clamp x
|
||||
(max_x, max_z, min_x, max_z, 2), // Top: clamp z
|
||||
(min_x, max_z, min_x, min_z, 3), // Left: clamp x
|
||||
];
|
||||
|
||||
for (edge_x1, edge_z1, edge_x2, edge_z2, edge_idx) in bbox_edges {
|
||||
if polygon.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut clipped = Vec::new();
|
||||
let is_closed = !polygon.is_empty() && polygon.first() == polygon.last();
|
||||
let edge_count = if is_closed {
|
||||
polygon.len().saturating_sub(1)
|
||||
} else {
|
||||
polygon.len()
|
||||
};
|
||||
|
||||
for i in 0..edge_count {
|
||||
let current = polygon[i];
|
||||
let next = polygon.get(i + 1).copied().unwrap_or(polygon[0]);
|
||||
|
||||
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
|
||||
if next_inside {
|
||||
if !current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
// Clamp to current edge only
|
||||
match edge_idx {
|
||||
0 => intersection.1 = min_z,
|
||||
1 => intersection.0 = max_x,
|
||||
2 => intersection.1 = max_z,
|
||||
3 => intersection.0 = min_x,
|
||||
_ => {}
|
||||
}
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
clipped.push(next);
|
||||
} else if current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
match edge_idx {
|
||||
0 => intersection.1 = min_z,
|
||||
1 => intersection.0 = max_x,
|
||||
2 => intersection.1 = max_z,
|
||||
3 => intersection.0 = min_x,
|
||||
_ => {}
|
||||
}
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
polygon = clipped;
|
||||
}
|
||||
|
||||
polygon
|
||||
}
|
||||
|
||||
/// Sutherland-Hodgman with full bbox clamping (simpler, for water rings).
|
||||
fn clip_polygon_sutherland_hodgman_simple(
|
||||
mut polygon: Vec<(f64, f64)>,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
let bbox_edges = [
|
||||
(min_x, min_z, max_x, min_z),
|
||||
(max_x, min_z, max_x, max_z),
|
||||
(max_x, max_z, min_x, max_z),
|
||||
(min_x, max_z, min_x, min_z),
|
||||
];
|
||||
|
||||
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
|
||||
if polygon.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut clipped = Vec::new();
|
||||
|
||||
for i in 0..(polygon.len().saturating_sub(1)) {
|
||||
let current = polygon[i];
|
||||
let next = polygon[i + 1];
|
||||
|
||||
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
|
||||
if next_inside {
|
||||
if !current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
intersection.0 = intersection.0.clamp(min_x, max_x);
|
||||
intersection.1 = intersection.1.clamp(min_z, max_z);
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
clipped.push(next);
|
||||
} else if current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
intersection.0 = intersection.0.clamp(min_x, max_x);
|
||||
intersection.1 = intersection.1.clamp(min_z, max_z);
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
polygon = clipped;
|
||||
}
|
||||
|
||||
polygon
|
||||
}
|
||||
|
||||
/// Checks if point is inside bbox.
|
||||
fn point_in_bbox(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> bool {
|
||||
point.0 >= min_x && point.0 <= max_x && point.1 >= min_z && point.1 <= max_z
|
||||
}
|
||||
|
||||
/// Checks if point is on the "inside" side of an edge (cross product test).
|
||||
fn point_inside_edge(
|
||||
point: (f64, f64),
|
||||
edge_x1: f64,
|
||||
edge_z1: f64,
|
||||
edge_x2: f64,
|
||||
edge_z2: f64,
|
||||
) -> bool {
|
||||
let edge_dx = edge_x2 - edge_x1;
|
||||
let edge_dz = edge_z2 - edge_z1;
|
||||
let point_dx = point.0 - edge_x1;
|
||||
let point_dz = point.1 - edge_z1;
|
||||
(edge_dx * point_dz - edge_dz * point_dx) >= 0.0
|
||||
}
|
||||
|
||||
/// Finds intersection between a line segment and an edge.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn line_edge_intersection(
|
||||
line_x1: f64,
|
||||
line_z1: f64,
|
||||
line_x2: f64,
|
||||
line_z2: f64,
|
||||
edge_x1: f64,
|
||||
edge_z1: f64,
|
||||
edge_x2: f64,
|
||||
edge_z2: f64,
|
||||
) -> Option<(f64, f64)> {
|
||||
let line_dx = line_x2 - line_x1;
|
||||
let line_dz = line_z2 - line_z1;
|
||||
let edge_dx = edge_x2 - edge_x1;
|
||||
let edge_dz = edge_z2 - edge_z1;
|
||||
|
||||
let denom = line_dx * edge_dz - line_dz * edge_dx;
|
||||
if denom.abs() < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dx = edge_x1 - line_x1;
|
||||
let dz = edge_z1 - line_z1;
|
||||
let t = (dx * edge_dz - dz * edge_dx) / denom;
|
||||
|
||||
if (0.0..=1.0).contains(&t) {
|
||||
Some((line_x1 + t * line_dx, line_z1 + t * line_dz))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds intersections between a line segment and bbox edges.
|
||||
fn find_bbox_intersections(
|
||||
start: (f64, f64),
|
||||
end: (f64, f64),
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
let mut intersections = Vec::new();
|
||||
|
||||
let bbox_edges = [
|
||||
(min_x, min_z, max_x, min_z),
|
||||
(max_x, min_z, max_x, max_z),
|
||||
(max_x, max_z, min_x, max_z),
|
||||
(min_x, max_z, min_x, min_z),
|
||||
];
|
||||
|
||||
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
|
||||
if let Some(intersection) = line_edge_intersection(
|
||||
start.0, start.1, end.0, end.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
let on_edge = point_in_bbox(intersection, min_x, min_z, max_x, max_z)
|
||||
&& ((intersection.0 == min_x || intersection.0 == max_x)
|
||||
|| (intersection.1 == min_z || intersection.1 == max_z));
|
||||
|
||||
if on_edge {
|
||||
intersections.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
// Handle corners (assign to edge in counter-clockwise order)
|
||||
if on_bottom && on_left {
|
||||
return 3;
|
||||
}
|
||||
if on_bottom && on_right {
|
||||
return 0;
|
||||
}
|
||||
if on_top && on_right {
|
||||
return 1;
|
||||
}
|
||||
if on_top && on_left {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if on_bottom {
|
||||
return 0;
|
||||
}
|
||||
if on_right {
|
||||
return 1;
|
||||
}
|
||||
if on_top {
|
||||
return 2;
|
||||
}
|
||||
if on_left {
|
||||
return 3;
|
||||
}
|
||||
|
||||
-1
|
||||
}
|
||||
|
||||
/// Returns corners to insert when traversing from edge1 to edge2 via shorter path.
|
||||
fn get_corners_between_edges(
|
||||
edge1: i32,
|
||||
edge2: i32,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
if edge1 == edge2 || edge1 < 0 || edge2 < 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let corners = [
|
||||
(max_x, min_z), // 0: bottom-right
|
||||
(max_x, max_z), // 1: top-right
|
||||
(min_x, max_z), // 2: top-left
|
||||
(min_x, min_z), // 3: bottom-left
|
||||
];
|
||||
|
||||
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
|
||||
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
|
||||
|
||||
// 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 {
|
||||
let mut current = edge1;
|
||||
for _ in 0..ccw_dist {
|
||||
result.push(corners[current as usize]);
|
||||
current = (current + 1) % 4;
|
||||
}
|
||||
} else {
|
||||
let mut current = edge1;
|
||||
for _ in 0..cw_dist {
|
||||
current = (current + 4 - 1) % 4;
|
||||
result.push(corners[current as usize]);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Inserts bbox corners where polygon transitions between different bbox edges.
|
||||
fn insert_bbox_corners(
|
||||
polygon: Vec<(f64, f64)>,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
if polygon.len() < 3 {
|
||||
return polygon;
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(polygon.len() + 4);
|
||||
|
||||
for i in 0..polygon.len() {
|
||||
let current = polygon[i];
|
||||
let next = polygon[(i + 1) % polygon.len()];
|
||||
|
||||
result.push(current);
|
||||
|
||||
let edge1 = get_bbox_edge(current, min_x, min_z, max_x, max_z);
|
||||
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
|
||||
|
||||
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
|
||||
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
|
||||
result.push(corner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Removes consecutive duplicate points (within epsilon tolerance).
|
||||
fn remove_consecutive_duplicates(polygon: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
|
||||
if polygon.is_empty() {
|
||||
return polygon;
|
||||
}
|
||||
|
||||
let eps = 0.1;
|
||||
let mut result: Vec<(f64, f64)> = Vec::with_capacity(polygon.len());
|
||||
|
||||
for p in &polygon {
|
||||
if let Some(last) = result.last() {
|
||||
if (p.0 - last.0).abs() < eps && (p.1 - last.1).abs() < eps {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(*p);
|
||||
}
|
||||
|
||||
// Check first/last duplicates for closed polygons
|
||||
if result.len() > 1 {
|
||||
let first = result.first().unwrap();
|
||||
let last = result.last().unwrap();
|
||||
if (first.0 - last.0).abs() < eps && (first.1 - last.1).abs() < eps {
|
||||
result.pop();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Checks if a clipped coordinate matches an original endpoint.
|
||||
fn matches_endpoint(coord: (f64, f64), endpoint: &ProcessedNode, tolerance: f64) -> bool {
|
||||
let dx = (coord.0 - endpoint.x as f64).abs();
|
||||
let dz = (coord.1 - endpoint.z as f64).abs();
|
||||
dx * dx + dz * dz < tolerance * tolerance
|
||||
}
|
||||
|
||||
/// Assigns node IDs to clipped coordinates, preserving original endpoint IDs.
|
||||
fn assign_node_ids_preserving_endpoints(
|
||||
original_nodes: &[ProcessedNode],
|
||||
clipped_coords: Vec<(f64, f64)>,
|
||||
way_id: u64,
|
||||
) -> Vec<ProcessedNode> {
|
||||
if clipped_coords.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let original_first = original_nodes.first();
|
||||
let original_last = original_nodes.last();
|
||||
let tolerance = 50.0;
|
||||
let last_index = clipped_coords.len() - 1;
|
||||
|
||||
clipped_coords
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, coord)| {
|
||||
let is_first = i == 0;
|
||||
let is_last = i == last_index;
|
||||
|
||||
if is_first || is_last {
|
||||
if let Some(first) = original_first {
|
||||
if matches_endpoint(coord, first, tolerance) {
|
||||
return ProcessedNode {
|
||||
id: first.id,
|
||||
x: coord.0.round() as i32,
|
||||
z: coord.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(last) = original_last {
|
||||
if matches_endpoint(coord, last, tolerance) {
|
||||
return ProcessedNode {
|
||||
id: last.id,
|
||||
x: coord.0.round() as i32,
|
||||
z: coord.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProcessedNode {
|
||||
id: way_id.wrapping_mul(10000000).wrapping_add(i as u64),
|
||||
x: coord.0.round() as i32,
|
||||
z: coord.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -4,16 +4,27 @@ use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::element_processing::*;
|
||||
use crate::ground::Ground;
|
||||
use crate::map_renderer;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const MIN_Y: i32 = -64;
|
||||
|
||||
/// Generation options that can be passed separately from CLI Args
|
||||
#[derive(Clone)]
|
||||
pub struct GenerationOptions {
|
||||
pub path: PathBuf,
|
||||
pub format: WorldFormat,
|
||||
pub level_name: Option<String>,
|
||||
pub spawn_point: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
pub fn generate_world(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
@@ -21,10 +32,41 @@ pub fn generate_world(
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
) -> Result<(), String> {
|
||||
let mut editor: WorldEditor = WorldEditor::new(args.path.clone(), &xzbbox, llbbox);
|
||||
// Default to Java format when called from CLI
|
||||
let options = GenerationOptions {
|
||||
path: args.path.clone(),
|
||||
format: WorldFormat::JavaAnvil,
|
||||
level_name: None,
|
||||
spawn_point: None,
|
||||
};
|
||||
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options).map(|_| ())
|
||||
}
|
||||
|
||||
/// Generate world with explicit format options (used by GUI for Bedrock support)
|
||||
pub fn generate_world_with_options(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
options: GenerationOptions,
|
||||
) -> Result<PathBuf, String> {
|
||||
let output_path = options.path.clone();
|
||||
let world_format = options.format;
|
||||
let mut editor: WorldEditor = WorldEditor::new_with_format_and_name(
|
||||
options.path,
|
||||
&xzbbox,
|
||||
llbbox,
|
||||
options.format,
|
||||
options.level_name,
|
||||
options.spawn_point,
|
||||
);
|
||||
|
||||
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(&ground);
|
||||
|
||||
@@ -66,7 +108,7 @@ pub fn generate_world(
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
buildings::generate_buildings(&mut editor, way, args, None);
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args, &elements);
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
} else if way.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse(&mut editor, way, args);
|
||||
} else if way.tags.contains_key("natural") {
|
||||
@@ -80,7 +122,7 @@ pub fn generate_world(
|
||||
} else if let Some(val) = way.tags.get("waterway") {
|
||||
if val == "dock" {
|
||||
// docks count as water areas
|
||||
water_areas::generate_water_area_from_way(&mut editor, way);
|
||||
water_areas::generate_water_area_from_way(&mut editor, way, &xzbbox);
|
||||
} else {
|
||||
waterways::generate_waterways(&mut editor, way);
|
||||
}
|
||||
@@ -111,7 +153,7 @@ pub fn generate_world(
|
||||
} else if node.tags.contains_key("barrier") {
|
||||
barriers::generate_barrier_nodes(&mut editor, node);
|
||||
} else if node.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args, &elements);
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
} else if node.tags.contains_key("tourism") {
|
||||
tourisms::generate_tourisms(&mut editor, node);
|
||||
} else if node.tags.contains_key("man_made") {
|
||||
@@ -128,7 +170,7 @@ pub fn generate_world(
|
||||
.map(|val| val == "water" || val == "bay")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
water_areas::generate_water_areas_from_relation(&mut editor, rel);
|
||||
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
|
||||
} else if rel.tags.contains_key("natural") {
|
||||
natural::generate_natural_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.contains_key("landuse") {
|
||||
@@ -233,33 +275,109 @@ pub fn generate_world(
|
||||
// Save world
|
||||
editor.save();
|
||||
|
||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
||||
|
||||
// Update player spawn Y coordinate based on terrain height after generation
|
||||
#[cfg(feature = "gui")]
|
||||
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()
|
||||
);
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
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()
|
||||
);
|
||||
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
Some(*spawn_coords),
|
||||
bbox_string,
|
||||
args.scale,
|
||||
&ground,
|
||||
) {
|
||||
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,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
Ok(())
|
||||
// For Bedrock format, emit event to open the mcworld file
|
||||
if world_format == WorldFormat::BedrockMcWorld {
|
||||
if let Some(path_str) = output_path.to_str() {
|
||||
emit_open_mcworld_file(path_str);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Information needed to generate a map preview after world generation is complete
|
||||
#[derive(Clone)]
|
||||
pub struct MapPreviewInfo {
|
||||
pub world_path: PathBuf,
|
||||
pub min_x: i32,
|
||||
pub max_x: i32,
|
||||
pub min_z: i32,
|
||||
pub max_z: i32,
|
||||
pub world_area: i64,
|
||||
}
|
||||
|
||||
impl MapPreviewInfo {
|
||||
/// Create MapPreviewInfo from world bounds
|
||||
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
|
||||
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||
Self {
|
||||
world_path,
|
||||
min_x: xzbbox.min_x(),
|
||||
max_x: xzbbox.max_x(),
|
||||
min_z: xzbbox.min_z(),
|
||||
max_z: xzbbox.max_z(),
|
||||
world_area: world_width * world_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
|
||||
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||
|
||||
/// Start map preview generation in a background thread.
|
||||
/// This should be called AFTER the world generation is complete, the session lock is released,
|
||||
/// and the GUI has been notified of 100% completion.
|
||||
///
|
||||
/// For Java worlds only, and only if the world area is within limits.
|
||||
pub fn start_map_preview_generation(info: MapPreviewInfo) {
|
||||
if info.world_area > MAX_MAP_PREVIEW_AREA {
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(
|
||||
&info.world_path,
|
||||
info.min_x,
|
||||
info.max_x,
|
||||
info.min_z,
|
||||
info.max_z,
|
||||
)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,19 +7,21 @@ use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Type alias for highway connectivity map
|
||||
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||
|
||||
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
||||
pub fn generate_highways(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
all_elements: &[ProcessedElement],
|
||||
highway_connectivity: &HighwayConnectivityMap,
|
||||
) {
|
||||
let highway_connectivity = build_highway_connectivity_map(all_elements);
|
||||
generate_highways_internal(editor, element, args, &highway_connectivity);
|
||||
generate_highways_internal(editor, element, args, highway_connectivity);
|
||||
}
|
||||
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed
|
||||
fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HashMap<(i32, i32), Vec<i32>> {
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
|
||||
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
||||
|
||||
for element in elements {
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::clipping::clip_water_ring_to_bbox;
|
||||
use crate::{
|
||||
block_definitions::WATER,
|
||||
coordinate_system::cartesian::XZPoint,
|
||||
coordinate_system::cartesian::{XZBBox, XZPoint},
|
||||
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay},
|
||||
world_editor::WorldEditor,
|
||||
};
|
||||
|
||||
pub fn generate_water_area_from_way(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
pub fn generate_water_area_from_way(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
_xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let outers = [element.nodes.clone()];
|
||||
if !verify_loopy_loops(&outers) {
|
||||
if !verify_closed_rings(&outers) {
|
||||
println!("Skipping way {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
@@ -20,7 +26,11 @@ pub fn generate_water_area_from_way(editor: &mut WorldEditor, element: &Processe
|
||||
generate_water_areas(editor, &outers, &[], start_time);
|
||||
}
|
||||
|
||||
pub fn generate_water_areas_from_relation(editor: &mut WorldEditor, element: &ProcessedRelation) {
|
||||
pub fn generate_water_areas_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedRelation,
|
||||
xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Check if this is a water relation (either with water tag or natural=water)
|
||||
@@ -52,14 +62,63 @@ pub fn generate_water_areas_from_relation(editor: &mut WorldEditor, element: &Pr
|
||||
}
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut outers);
|
||||
if !verify_loopy_loops(&outers) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
// Preserve OSM-defined outer/inner roles without modification
|
||||
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();
|
||||
merge_way_segments(&mut inners);
|
||||
inners = inners
|
||||
.into_iter()
|
||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||
.collect();
|
||||
|
||||
if !verify_closed_rings(&outers) {
|
||||
// For clipped multipolygons, some loops may not close perfectly
|
||||
// Instead of force-closing with straight lines (which creates wedges),
|
||||
// filter out unclosed loops and only render the properly closed ones
|
||||
|
||||
// Filter: Keep only loops that are already closed OR can be closed within 1 block
|
||||
outers.retain(|loop_nodes| {
|
||||
if loop_nodes.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
let first = &loop_nodes[0];
|
||||
let last = loop_nodes.last().unwrap();
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
|
||||
// Keep if already closed by ID or endpoints are within 1 block
|
||||
first.id == last.id || (dx <= 1 && dz <= 1)
|
||||
});
|
||||
|
||||
// Now close the remaining loops that are within 1 block tolerance
|
||||
for loop_nodes in outers.iter_mut() {
|
||||
let first = loop_nodes[0].clone();
|
||||
let last_idx = loop_nodes.len() - 1;
|
||||
if loop_nodes[0].id != loop_nodes[last_idx].id {
|
||||
// Endpoints are close (within tolerance), close the loop
|
||||
loop_nodes.push(first);
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid outer loops remain, skip the relation
|
||||
if outers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify again after filtering and closing
|
||||
if !verify_closed_rings(&outers) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut inners);
|
||||
if !verify_loopy_loops(&inners) {
|
||||
merge_way_segments(&mut inners);
|
||||
if !verify_closed_rings(&inners) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
@@ -73,8 +132,34 @@ fn generate_water_areas(
|
||||
inners: &[Vec<ProcessedNode>],
|
||||
start_time: Instant,
|
||||
) {
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
// Calculate polygon bounding box to limit fill area
|
||||
let mut poly_min_x = i32::MAX;
|
||||
let mut poly_min_z = i32::MAX;
|
||||
let mut poly_max_x = i32::MIN;
|
||||
let mut poly_max_z = i32::MIN;
|
||||
|
||||
for outer in outers {
|
||||
for node in outer {
|
||||
poly_min_x = poly_min_x.min(node.x);
|
||||
poly_min_z = poly_min_z.min(node.z);
|
||||
poly_max_x = poly_max_x.max(node.x);
|
||||
poly_max_z = poly_max_z.max(node.z);
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid bounds, nothing to fill
|
||||
if poly_min_x == i32::MAX || poly_max_x == i32::MIN {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp to world bounds just in case
|
||||
let (world_min_x, world_min_z) = editor.get_min_coords();
|
||||
let (world_max_x, world_max_z) = editor.get_max_coords();
|
||||
let min_x = poly_min_x.max(world_min_x);
|
||||
let min_z = poly_min_z.max(world_min_z);
|
||||
let max_x = poly_max_x.min(world_max_x);
|
||||
let max_z = poly_max_z.min(world_max_z);
|
||||
|
||||
let outers_xz: Vec<Vec<XZPoint>> = outers
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
@@ -89,13 +174,23 @@ fn generate_water_areas(
|
||||
);
|
||||
}
|
||||
|
||||
// Merges ways that share nodes into full loops
|
||||
fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
/// 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![];
|
||||
|
||||
for i in 0..loops.len() {
|
||||
for j in 0..loops.len() {
|
||||
// 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;
|
||||
}
|
||||
@@ -104,20 +199,29 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x: &Vec<ProcessedNode> = &loops[i];
|
||||
let y: &Vec<ProcessedNode> = &loops[j];
|
||||
let x: &Vec<ProcessedNode> = &rings[i];
|
||||
let y: &Vec<ProcessedNode> = &rings[j];
|
||||
|
||||
// it's looped already
|
||||
if x[0].id == x.last().unwrap().id {
|
||||
// Skip empty rings (can happen after clipping)
|
||||
if x.is_empty() || y.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// it's looped already
|
||||
if y[0].id == y.last().unwrap().id {
|
||||
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 x[0].id == y[0].id {
|
||||
if nodes_match(y_first, y_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(x_first, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -125,7 +229,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
x.reverse();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
merged.push(x);
|
||||
} else if x.last().unwrap().id == y.last().unwrap().id {
|
||||
} else if nodes_match(x_last, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -133,7 +237,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
x.extend(y.iter().rev().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
} else if x[0].id == y.last().unwrap().id {
|
||||
} else if nodes_match(x_first, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -141,7 +245,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
y.extend(x.iter().skip(1).cloned());
|
||||
|
||||
merged.push(y);
|
||||
} else if x.last().unwrap().id == y[0].id {
|
||||
} else if nodes_match(x_last, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -156,24 +260,35 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
removed.sort();
|
||||
|
||||
for r in removed.iter().rev() {
|
||||
loops.remove(*r);
|
||||
rings.remove(*r);
|
||||
}
|
||||
|
||||
let merged_len: usize = merged.len();
|
||||
for m in merged {
|
||||
loops.push(m);
|
||||
rings.push(m);
|
||||
}
|
||||
|
||||
if merged_len > 0 {
|
||||
merge_loopy_loops(loops);
|
||||
merge_way_segments(rings);
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_loopy_loops(loops: &[Vec<ProcessedNode>]) -> bool {
|
||||
let mut valid: bool = true;
|
||||
for l in loops {
|
||||
if l[0].id != l.last().unwrap().id {
|
||||
eprintln!("WARN: Disconnected loop");
|
||||
/// Verifies all rings are properly closed (first node matches last).
|
||||
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
||||
let mut valid = true;
|
||||
for ring in rings {
|
||||
let first = &ring[0];
|
||||
let last = ring.last().unwrap();
|
||||
|
||||
// Check if ring is closed (by ID or proximity)
|
||||
let is_closed = first.id == last.id || {
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
if !is_closed {
|
||||
eprintln!("WARN: Disconnected ring");
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
@@ -195,6 +310,7 @@ fn inverse_floodfill(
|
||||
editor: &mut WorldEditor,
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Convert to geo Polygons with normalized winding order
|
||||
let inners: Vec<_> = inners
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
@@ -206,6 +322,7 @@ fn inverse_floodfill(
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -220,6 +337,7 @@ fn inverse_floodfill(
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -116,11 +116,6 @@ pub fn fetch_elevation_data(
|
||||
tile_path.display(),
|
||||
file_size
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile appears to be too small. Refetching tile.",
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
@@ -251,25 +246,26 @@ pub fn fetch_elevation_data(
|
||||
filter_elevation_outliers(&mut height_grid);
|
||||
|
||||
// Calculate blur sigma based on grid resolution
|
||||
// Reference points for tuning:
|
||||
const SMALL_GRID_REF: f64 = 100.0; // Reference grid size
|
||||
const SMALL_SIGMA_REF: f64 = 15.0; // Sigma for 100x100 grid
|
||||
const LARGE_GRID_REF: f64 = 1000.0; // Reference grid size
|
||||
const LARGE_SIGMA_REF: f64 = 7.0; // Sigma for 1000x1000 grid
|
||||
// Use sqrt scaling to maintain consistent relative smoothing across different area sizes.
|
||||
// This prevents larger generation areas from appearing noisier than smaller ones.
|
||||
// Reference: 100x100 grid uses sigma=5 (5% relative blur)
|
||||
const BASE_GRID_REF: f64 = 100.0;
|
||||
const BASE_SIGMA_REF: f64 = 5.0;
|
||||
|
||||
let grid_size: f64 = (grid_width.min(grid_height) as f64).max(1.0);
|
||||
|
||||
let sigma: f64 = if grid_size <= SMALL_GRID_REF {
|
||||
// Linear scaling for small grids
|
||||
SMALL_SIGMA_REF * (grid_size / SMALL_GRID_REF)
|
||||
} else {
|
||||
// Logarithmic scaling for larger grids
|
||||
let ln_small: f64 = SMALL_GRID_REF.ln();
|
||||
let ln_large: f64 = LARGE_GRID_REF.ln();
|
||||
let log_grid_size: f64 = grid_size.ln();
|
||||
let t: f64 = (log_grid_size - ln_small) / (ln_large - ln_small);
|
||||
SMALL_SIGMA_REF + t * (LARGE_SIGMA_REF - SMALL_SIGMA_REF)
|
||||
};
|
||||
// Sqrt scaling provides a good balance:
|
||||
// - 100x100: sigma = 5 (5% relative)
|
||||
// - 500x500: sigma ≈ 11.2 (2.2% relative)
|
||||
// - 1000x1000: sigma ≈ 15.8 (1.6% relative)
|
||||
// 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!(
|
||||
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
|
||||
grid_width, grid_height, sigma, blur_percentage
|
||||
);
|
||||
|
||||
/* eprintln!(
|
||||
"Grid: {}x{}, Blur sigma: {:.2}",
|
||||
|
||||
@@ -68,14 +68,11 @@ fn optimized_flood_fill_area(
|
||||
|
||||
// Pre-allocate queue with reasonable capacity to avoid reallocations
|
||||
let mut queue = VecDeque::with_capacity(1024);
|
||||
let mut iterations = 0u64;
|
||||
const MAX_ITERATIONS: u64 = 1_000_000; // Safety limit to prevent infinite loops
|
||||
|
||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||
// Check timeout more frequently for small areas
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 50 == 0 {
|
||||
// Fast timeout check, only every few iterations
|
||||
if filled_area.len() % 100 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if start_time.elapsed() > *timeout {
|
||||
return filled_area;
|
||||
@@ -83,16 +80,6 @@ fn optimized_flood_fill_area(
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: prevent infinite loops
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Skip if already visited or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
@@ -106,26 +93,6 @@ fn optimized_flood_fill_area(
|
||||
global_visited.insert((x, z));
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Additional iteration check inside inner loop
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Timeout check in inner loop for problematic polygons
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 1000 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if start_time.elapsed() > *timeout {
|
||||
return filled_area;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add current point to filled area
|
||||
filled_area.push((curr_x, curr_z));
|
||||
|
||||
@@ -188,32 +155,18 @@ fn original_flood_fill_area(
|
||||
// Pre-allocate queue and reserve space for filled_area
|
||||
let mut queue: VecDeque<(i32, i32)> = VecDeque::with_capacity(2048);
|
||||
filled_area.reserve(1000); // Reserve space to reduce reallocations
|
||||
let mut iterations = 0u64;
|
||||
const MAX_ITERATIONS: u64 = 1_000_000; // Safety limit to prevent infinite loops
|
||||
|
||||
// Scan for multiple seed points to handle U-shapes and concave polygons
|
||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||
// Check timeout more frequently for problematic polygons
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 50 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if &start_time.elapsed() > timeout {
|
||||
return filled_area;
|
||||
}
|
||||
// Reduced timeout checking frequency for better performance
|
||||
// Use manual % check since is_multiple_of() is unstable on stable Rust
|
||||
if let Some(timeout) = timeout {
|
||||
if &start_time.elapsed() > timeout {
|
||||
return filled_area;
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: prevent infinite loops
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Skip if already processed or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
@@ -227,26 +180,6 @@ fn original_flood_fill_area(
|
||||
global_visited.insert((x, z));
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Additional iteration check inside inner loop
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Timeout check in inner loop
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 1000 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if &start_time.elapsed() > timeout {
|
||||
return filled_area;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only check polygon containment once per point when adding to filled_area
|
||||
if polygon.contains(&Point::new(curr_x as f64, curr_z as f64)) {
|
||||
filled_area.push((curr_x, curr_z));
|
||||
|
||||
243
src/gui.rs
243
src/gui.rs
@@ -2,14 +2,16 @@ use crate::args::Args;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
use crate::data_processing;
|
||||
use crate::data_processing::{self, GenerationOptions};
|
||||
use crate::ground::{self, Ground};
|
||||
use crate::map_transformation;
|
||||
use crate::osm_parser;
|
||||
use crate::progress;
|
||||
use crate::progress::{self, emit_gui_progress_update};
|
||||
use crate::retrieve_data;
|
||||
use crate::telemetry::{self, send_log, LogLevel};
|
||||
use crate::version_check;
|
||||
use crate::world_editor::WorldFormat;
|
||||
use colored::Colorize;
|
||||
use fastnbt::Value;
|
||||
use flate2::read::GzDecoder;
|
||||
use fs2::FileExt;
|
||||
@@ -60,6 +62,17 @@ impl Drop for SessionLock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the area name for a given bounding box using the center point
|
||||
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
||||
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
||||
let center_lon = (bbox.min().lng() + bbox.max().lng()) / 2.0;
|
||||
|
||||
match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||
Ok(Some(name)) => name,
|
||||
_ => "Unknown Location".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_gui() {
|
||||
// Launch the UI
|
||||
println!("Launching UI...");
|
||||
@@ -100,7 +113,9 @@ pub fn run_gui() {
|
||||
gui_select_world,
|
||||
gui_start_generation,
|
||||
gui_get_version,
|
||||
gui_check_for_updates
|
||||
gui_check_for_updates,
|
||||
gui_get_world_map_data,
|
||||
gui_show_in_folder
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle();
|
||||
@@ -663,6 +678,114 @@ fn gui_check_for_updates() -> Result<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the world map image data as base64 and geo bounds for overlay display.
|
||||
/// Returns None if the map image or metadata doesn't exist.
|
||||
#[tauri::command]
|
||||
fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, String> {
|
||||
let world_dir = PathBuf::from(&world_path);
|
||||
let map_path = world_dir.join("arnis_world_map.png");
|
||||
let metadata_path = world_dir.join("metadata.json");
|
||||
|
||||
// Check if both files exist
|
||||
if !map_path.exists() || !metadata_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Read and encode the map image as base64
|
||||
let image_data = fs::read(&map_path).map_err(|e| format!("Failed to read map image: {e}"))?;
|
||||
let base64_image =
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &image_data);
|
||||
|
||||
// Read metadata
|
||||
let metadata_content =
|
||||
fs::read_to_string(&metadata_path).map_err(|e| format!("Failed to read metadata: {e}"))?;
|
||||
let metadata: serde_json::Value = serde_json::from_str(&metadata_content)
|
||||
.map_err(|e| format!("Failed to parse metadata: {e}"))?;
|
||||
|
||||
// Extract geo bounds (metadata uses camelCase from serde)
|
||||
let min_lat = metadata["minGeoLat"]
|
||||
.as_f64()
|
||||
.ok_or("Missing minGeoLat in metadata")?;
|
||||
let max_lat = metadata["maxGeoLat"]
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLat in metadata")?;
|
||||
let min_lon = metadata["minGeoLon"]
|
||||
.as_f64()
|
||||
.ok_or("Missing minGeoLon in metadata")?;
|
||||
let max_lon = metadata["maxGeoLon"]
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLon in metadata")?;
|
||||
|
||||
Ok(Some(WorldMapData {
|
||||
image_base64: format!("data:image/png;base64,{}", base64_image),
|
||||
min_lat,
|
||||
max_lat,
|
||||
min_lon,
|
||||
max_lon,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Data structure for world map overlay
|
||||
#[derive(serde::Serialize)]
|
||||
struct WorldMapData {
|
||||
image_base64: String,
|
||||
min_lat: f64,
|
||||
max_lat: f64,
|
||||
min_lon: f64,
|
||||
max_lon: f64,
|
||||
}
|
||||
|
||||
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
||||
#[tauri::command]
|
||||
fn gui_show_in_folder(path: String) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, try to open with default application (Minecraft Bedrock)
|
||||
// If that fails, show in Explorer
|
||||
if std::process::Command::new("cmd")
|
||||
.args(["/C", "start", "", &path])
|
||||
.spawn()
|
||||
.is_err()
|
||||
{
|
||||
std::process::Command::new("explorer")
|
||||
.args(["/select,", &path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open explorer: {}", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// On macOS, just reveal in Finder
|
||||
std::process::Command::new("open")
|
||||
.args(["-R", &path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open Finder: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// On Linux, just show in file manager
|
||||
let path_parent = std::path::Path::new(&path)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.clone());
|
||||
|
||||
// Try nautilus with select first, then fall back to xdg-open on parent
|
||||
if std::process::Command::new("nautilus")
|
||||
.args(["--select", &path])
|
||||
.spawn()
|
||||
.is_err()
|
||||
{
|
||||
let _ = std::process::Command::new("xdg-open")
|
||||
.arg(&path_parent)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(unused_variables)]
|
||||
@@ -680,6 +803,7 @@ fn gui_start_generation(
|
||||
is_new_world: bool,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
telemetry_consent: bool,
|
||||
world_format: String,
|
||||
) -> Result<(), String> {
|
||||
use progress::emit_gui_error;
|
||||
use LLBBox;
|
||||
@@ -691,7 +815,9 @@ fn gui_start_generation(
|
||||
telemetry::send_generation_click();
|
||||
|
||||
// If spawn point was chosen and the world is new, check and set the spawn point
|
||||
if is_new_world && spawn_point.is_some() {
|
||||
// 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 && 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) {
|
||||
@@ -745,19 +871,73 @@ fn gui_start_generation(
|
||||
}
|
||||
};
|
||||
|
||||
// Add localized name to the world if user generated a new world
|
||||
let updated_world_path = if is_new_world {
|
||||
add_localized_world_name(world_path, &bbox)
|
||||
// Determine world format from UI selection
|
||||
let world_format = if world_format == "bedrock" {
|
||||
WorldFormat::BedrockMcWorld
|
||||
} else {
|
||||
world_path
|
||||
WorldFormat::JavaAnvil
|
||||
};
|
||||
|
||||
// Create an Args instance with the chosen bounding box and world directory path
|
||||
// Determine output path and level name based on format
|
||||
let (generation_path, level_name) = match world_format {
|
||||
WorldFormat::JavaAnvil => {
|
||||
// Java: use the selected world path, add localized name if new
|
||||
let updated_path = if is_new_world {
|
||||
add_localized_world_name(world_path.clone(), &bbox)
|
||||
} else {
|
||||
world_path.clone()
|
||||
};
|
||||
(updated_path, None)
|
||||
}
|
||||
WorldFormat::BedrockMcWorld => {
|
||||
// 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 = 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
|
||||
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, _)) =
|
||||
CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale)
|
||||
{
|
||||
let xzpoint = transformer.transform_point(llpoint);
|
||||
Some((xzpoint.x, xzpoint.z))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create generation options
|
||||
let generation_options = GenerationOptions {
|
||||
path: generation_path.clone(),
|
||||
format: world_format,
|
||||
level_name,
|
||||
spawn_point: mc_spawn_point,
|
||||
};
|
||||
|
||||
// Create an Args instance with the chosen bounding box
|
||||
// Note: path is used for Java-specific features like spawn point update
|
||||
let args: Args = Args {
|
||||
bbox,
|
||||
file: None,
|
||||
save_json_file: None,
|
||||
path: updated_world_path,
|
||||
path: if world_format == WorldFormat::JavaAnvil {
|
||||
generation_path
|
||||
} else {
|
||||
world_path
|
||||
},
|
||||
downloader: "requests".to_string(),
|
||||
scale: world_scale,
|
||||
ground_level,
|
||||
@@ -781,14 +961,29 @@ fn gui_start_generation(
|
||||
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
|
||||
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
|
||||
|
||||
let _ = data_processing::generate_world(
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
generation_options.clone(),
|
||||
);
|
||||
// Session lock will be automatically released when _session_lock goes out of scope
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -818,16 +1013,30 @@ fn gui_start_generation(
|
||||
&mut xzbbox,
|
||||
&mut ground,
|
||||
);
|
||||
send_log(LogLevel::Info, "Map transformation completed.");
|
||||
|
||||
let _ = data_processing::generate_world(
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
generation_options.clone(),
|
||||
);
|
||||
// Session lock will be automatically released when _session_lock goes out of scope
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
34
src/gui/css/bbox.css
vendored
34
src/gui/css/bbox.css
vendored
@@ -344,4 +344,38 @@ body,
|
||||
filter: blur(1px) sepia(1) invert(1);
|
||||
transition: all 1s ease;
|
||||
|
||||
}
|
||||
|
||||
/* World Preview Button in Edit Toolbar */
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview {
|
||||
background-position: -31px -2px;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.active {
|
||||
background-color: #a0d0ff;
|
||||
}
|
||||
|
||||
.world-preview-slider-container {
|
||||
padding: 6px 8px !important;
|
||||
background: white !important;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.world-preview-slider-container a {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.world-preview-slider {
|
||||
width: 80px;
|
||||
height: 8px;
|
||||
cursor: pointer;
|
||||
accent-color: #3887BE;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
BIN
src/gui/css/maps/images/spritesheet-2x.png
vendored
BIN
src/gui/css/maps/images/spritesheet-2x.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
src/gui/css/maps/images/spritesheet.png
vendored
BIN
src/gui/css/maps/images/spritesheet.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.1 KiB |
5
src/gui/css/maps/leaflet.draw.css
vendored
5
src/gui/css/maps/leaflet.draw.css
vendored
@@ -158,12 +158,13 @@
|
||||
background-position: -182px -2px;
|
||||
}
|
||||
|
||||
/* Disabled states reuse same sprites; opacity indicates disabled */
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled {
|
||||
background-position: -212px -2px;
|
||||
background-position: -152px -2px;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
|
||||
background-position: -242px -2px;
|
||||
background-position: -182px -2px;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
|
||||
62
src/gui/css/styles.css
vendored
62
src/gui/css/styles.css
vendored
@@ -222,6 +222,68 @@ button:hover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* World Selection Container */
|
||||
.world-selection-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.choose-world-btn {
|
||||
padding: 10px;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
border-radius: 8px 8px 0 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* World Format Toggle */
|
||||
.format-toggle-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.format-toggle-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
margin-top: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.format-toggle-btn:first-child {
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
|
||||
.format-toggle-btn:last-child {
|
||||
border-radius: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.format-toggle-btn:not(.format-active) {
|
||||
background-color: #3a3a3a;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.format-toggle-btn:not(.format-active):hover {
|
||||
background-color: #4a4a4a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.format-toggle-btn.format-active {
|
||||
background-color: var(--primary-accent);
|
||||
color: #0f0f0f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.format-toggle-btn.format-active:hover {
|
||||
background-color: var(--primary-accent-dark);
|
||||
}
|
||||
|
||||
/* Customization Settings */
|
||||
.modal {
|
||||
position: fixed;
|
||||
|
||||
29
src/gui/index.html
vendored
29
src/gui/index.html
vendored
@@ -37,15 +37,26 @@
|
||||
<div class="controls-content">
|
||||
<h2 data-localize="select_world">Select World</h2>
|
||||
|
||||
<!-- Updated Tooltip Structure -->
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" onclick="openWorldPicker()" style="padding: 10px; line-height: 1.2; width: 100%;">
|
||||
<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>
|
||||
<!-- 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">
|
||||
|
||||
213
src/gui/js/bbox.js
vendored
213
src/gui/js/bbox.js
vendored
@@ -529,7 +529,7 @@ $(document).ready(function () {
|
||||
failureCount++;
|
||||
|
||||
// After a few failures, try HTTP fallback
|
||||
if (failureCount >= 3 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
|
||||
if (failureCount >= 6 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
|
||||
console.log('HTTPS tile loading failed, attempting HTTP fallback for', themeKey);
|
||||
this._httpFallbackAttempted = true;
|
||||
|
||||
@@ -558,11 +558,208 @@ $(document).ready(function () {
|
||||
var savedTheme = localStorage.getItem('selectedTileTheme') || 'osm';
|
||||
changeTileTheme(savedTheme);
|
||||
|
||||
// Listen for theme changes from parent window (settings modal)
|
||||
// World overlay state
|
||||
var worldOverlay = null;
|
||||
var worldOverlayData = null;
|
||||
var worldOverlayEnabled = false;
|
||||
var worldPreviewAvailable = false;
|
||||
var sliderControl = null;
|
||||
|
||||
// Create the opacity slider as a proper Leaflet control
|
||||
var SliderControl = L.Control.extend({
|
||||
options: { position: 'topleft' },
|
||||
onAdd: function(map) {
|
||||
var container = L.DomUtil.create('div', 'leaflet-bar world-preview-slider-container');
|
||||
container.id = 'world-preview-slider-container';
|
||||
container.style.display = 'none';
|
||||
|
||||
var slider = L.DomUtil.create('input', 'world-preview-slider', container);
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
slider.value = '50';
|
||||
slider.id = 'world-preview-opacity';
|
||||
slider.title = 'Overlay Opacity';
|
||||
|
||||
L.DomEvent.on(slider, 'input', function(e) {
|
||||
if (worldOverlay) {
|
||||
worldOverlay.setOpacity(e.target.value / 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent all map interactions
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
L.DomEvent.disableScrollPropagation(container);
|
||||
L.DomEvent.on(container, 'mousedown', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(container, 'touchstart', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(slider, 'mousedown', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(slider, 'touchstart', L.DomEvent.stopPropagation);
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
// Function to add world preview button to the draw control's edit toolbar
|
||||
function addWorldPreviewToEditToolbar() {
|
||||
// Find the edit toolbar (contains Edit layers and Delete layers buttons)
|
||||
var editToolbar = document.querySelector('.leaflet-draw-toolbar:not(.leaflet-draw-toolbar-top)');
|
||||
if (!editToolbar) {
|
||||
// Try finding by the edit/delete buttons
|
||||
var deleteBtn = document.querySelector('.leaflet-draw-edit-remove');
|
||||
if (deleteBtn) {
|
||||
editToolbar = deleteBtn.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (editToolbar) {
|
||||
// Create the preview button
|
||||
var toggleBtn = document.createElement('a');
|
||||
toggleBtn.className = 'leaflet-draw-edit-preview disabled';
|
||||
toggleBtn.href = '#';
|
||||
toggleBtn.title = 'Show World Preview (not available yet)';
|
||||
toggleBtn.id = 'world-preview-btn';
|
||||
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (worldPreviewAvailable) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
editToolbar.appendChild(toggleBtn);
|
||||
|
||||
// Add the slider control to the map
|
||||
sliderControl = new SliderControl();
|
||||
map.addControl(sliderControl);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle world overlay function
|
||||
function toggleWorldOverlay() {
|
||||
if (!worldPreviewAvailable || !worldOverlayData) return;
|
||||
|
||||
worldOverlayEnabled = !worldOverlayEnabled;
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
var sliderContainer = document.getElementById('world-preview-slider-container');
|
||||
|
||||
if (worldOverlayEnabled) {
|
||||
// Show overlay
|
||||
var data = worldOverlayData;
|
||||
var bounds = L.latLngBounds(
|
||||
[data.min_lat, data.min_lon],
|
||||
[data.max_lat, data.max_lon]
|
||||
);
|
||||
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
}
|
||||
|
||||
var opacity = document.getElementById('world-preview-opacity');
|
||||
var opacityValue = opacity ? opacity.value / 100 : 0.5;
|
||||
|
||||
worldOverlay = L.imageOverlay(data.image_base64, bounds, {
|
||||
opacity: opacityValue,
|
||||
interactive: false,
|
||||
zIndex: 500
|
||||
});
|
||||
worldOverlay.addTo(map);
|
||||
|
||||
if (btn) {
|
||||
btn.classList.add('active');
|
||||
btn.title = 'Hide World Preview';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
// Hide overlay
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
worldOverlay = null;
|
||||
}
|
||||
if (btn) {
|
||||
btn.classList.remove('active');
|
||||
btn.title = 'Show World Preview';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the preview button when data is available
|
||||
function enableWorldPreview(data) {
|
||||
worldOverlayData = data;
|
||||
worldPreviewAvailable = true;
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
if (btn) {
|
||||
btn.classList.remove('disabled');
|
||||
btn.title = 'Show World Preview';
|
||||
}
|
||||
}
|
||||
|
||||
// Disable and reset preview (when world changes)
|
||||
function disableWorldPreview() {
|
||||
worldPreviewAvailable = false;
|
||||
worldOverlayData = null;
|
||||
worldOverlayEnabled = false;
|
||||
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
worldOverlay = null;
|
||||
}
|
||||
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
var sliderContainer = document.getElementById('world-preview-slider-container');
|
||||
if (btn) {
|
||||
btn.classList.add('disabled');
|
||||
btn.classList.remove('active');
|
||||
btn.title = 'Show World Preview (not available yet)';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'changeTileTheme') {
|
||||
changeTileTheme(event.data.theme);
|
||||
}
|
||||
|
||||
// Handle world preview data ready (after generation completes)
|
||||
if (event.data && event.data.type === 'worldPreviewReady') {
|
||||
enableWorldPreview(event.data.data);
|
||||
|
||||
// Auto-enable the overlay when generation completes
|
||||
if (!worldOverlayEnabled) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle existing world map load (zoom to location and auto-enable)
|
||||
if (event.data && event.data.type === 'loadExistingWorldMap') {
|
||||
var data = event.data.data;
|
||||
enableWorldPreview(data);
|
||||
|
||||
// Calculate bounds and zoom to them
|
||||
var bounds = L.latLngBounds(
|
||||
[data.min_lat, data.min_lon],
|
||||
[data.max_lat, data.max_lon]
|
||||
);
|
||||
map.fitBounds(bounds, { padding: [50, 50] });
|
||||
|
||||
// Auto-enable the overlay
|
||||
if (!worldOverlayEnabled) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle world changed (disable preview)
|
||||
if (event.data && event.data.type === 'worldChanged') {
|
||||
disableWorldPreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the dropdown value in parent window if it exists
|
||||
@@ -652,6 +849,9 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
// Add world preview button to the edit toolbar after drawControl is added
|
||||
addWorldPreviewToEditToolbar();
|
||||
/*
|
||||
**
|
||||
** create bounds layer
|
||||
@@ -699,6 +899,15 @@ $(document).ready(function () {
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a rectangle, remove any existing rectangles first
|
||||
if (e.layerType === 'rectangle') {
|
||||
drawnItems.eachLayer(function(layer) {
|
||||
if (layer instanceof L.Rectangle) {
|
||||
drawnItems.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a rectangle and set proper styles before adding it to the layer
|
||||
if (e.layerType === 'rectangle') {
|
||||
e.layer.setStyle({
|
||||
|
||||
4
src/gui/js/license.js
vendored
4
src/gui/js/license.js
vendored
@@ -24,6 +24,10 @@ export const licenseText = `
|
||||
Elevation data derived from the <a href="https://registry.opendata.aws/terrain-tiles/" style="color: inherit;" target="_blank">AWS Terrain Tiles</a> dataset.
|
||||
<br><br>
|
||||
|
||||
<b>bedrock-rs:</b><br>
|
||||
Bedrock Edition world format support uses the <a href="https://github.com/bedrock-crustaceans/bedrock-rs" style="color: inherit;" target="_blank">bedrock-rs</a> library, licensed under the Apache License 2.0.
|
||||
<br><br>
|
||||
|
||||
<p><b>Privacy Policy:</b></p>
|
||||
If you consent to telemetry data collection, please review our Privacy Policy at:
|
||||
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">https://arnismc.com/privacypolicy.html</a>.
|
||||
|
||||
191
src/gui/js/main.js
vendored
191
src/gui/js/main.js
vendored
@@ -24,6 +24,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
handleBboxInput();
|
||||
const localization = await getLocalization();
|
||||
await applyLocalization(localization);
|
||||
updateFormatToggleUI(selectedWorldFormat);
|
||||
initFooter();
|
||||
await checkForUpdates();
|
||||
});
|
||||
@@ -214,6 +215,23 @@ function setupProgressListener() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for map preview ready event from backend
|
||||
window.__TAURI__.event.listen("map-preview-ready", () => {
|
||||
console.log("Map preview ready event received");
|
||||
showWorldPreviewButton();
|
||||
});
|
||||
|
||||
// Listen for open-mcworld-file event to show the generated Bedrock world in file explorer
|
||||
window.__TAURI__.event.listen("open-mcworld-file", async (event) => {
|
||||
const filePath = event.payload;
|
||||
try {
|
||||
// Use our custom command to show the file in the system file explorer
|
||||
await invoke("gui_show_in_folder", { path: filePath });
|
||||
} catch (error) {
|
||||
console.error("Failed to show mcworld file in folder:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSettings() {
|
||||
@@ -242,6 +260,9 @@ function initSettings() {
|
||||
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
|
||||
});
|
||||
|
||||
// World format toggle (Java/Bedrock)
|
||||
initWorldFormatToggle();
|
||||
|
||||
// Language selector
|
||||
const languageSelect = document.getElementById("language-select");
|
||||
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
||||
@@ -344,6 +365,72 @@ function initSettings() {
|
||||
window.closeLicense = closeLicense;
|
||||
}
|
||||
|
||||
// World format selection (Java/Bedrock)
|
||||
let selectedWorldFormat = 'java'; // Default to Java
|
||||
|
||||
function initWorldFormatToggle() {
|
||||
// Load saved format preference
|
||||
const savedFormat = localStorage.getItem('arnis-world-format');
|
||||
if (savedFormat && (savedFormat === 'java' || savedFormat === 'bedrock')) {
|
||||
selectedWorldFormat = savedFormat;
|
||||
}
|
||||
|
||||
// Apply the saved selection to UI
|
||||
updateFormatToggleUI(selectedWorldFormat);
|
||||
}
|
||||
|
||||
function setWorldFormat(format) {
|
||||
if (format !== 'java' && format !== 'bedrock') return;
|
||||
|
||||
selectedWorldFormat = format;
|
||||
localStorage.setItem('arnis-world-format', format);
|
||||
updateFormatToggleUI(format);
|
||||
}
|
||||
|
||||
function updateFormatToggleUI(format) {
|
||||
const javaBtn = document.getElementById('format-java');
|
||||
const bedrockBtn = document.getElementById('format-bedrock');
|
||||
const chooseWorldBtn = document.getElementById('choose-world-btn');
|
||||
const selectedWorldText = document.getElementById('selected-world');
|
||||
|
||||
if (format === 'java') {
|
||||
javaBtn.classList.add('format-active');
|
||||
bedrockBtn.classList.remove('format-active');
|
||||
// Enable Choose World button for Java
|
||||
if (chooseWorldBtn) {
|
||||
chooseWorldBtn.disabled = false;
|
||||
chooseWorldBtn.style.opacity = '1';
|
||||
chooseWorldBtn.style.cursor = 'pointer';
|
||||
}
|
||||
// Show default text (world was cleared when switching to Bedrock)
|
||||
if (selectedWorldText) {
|
||||
const noWorldText = window.localization?.no_world_selected || 'No world selected';
|
||||
selectedWorldText.textContent = noWorldText;
|
||||
selectedWorldText.style.color = '#fecc44';
|
||||
}
|
||||
} else {
|
||||
javaBtn.classList.remove('format-active');
|
||||
bedrockBtn.classList.add('format-active');
|
||||
// Disable Choose World button for Bedrock and clear any selected world
|
||||
if (chooseWorldBtn) {
|
||||
chooseWorldBtn.disabled = true;
|
||||
chooseWorldBtn.style.opacity = '0.5';
|
||||
chooseWorldBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
// Clear world selection and show Bedrock info message
|
||||
worldPath = "";
|
||||
isNewWorld = false;
|
||||
if (selectedWorldText) {
|
||||
const bedrockText = window.localization?.bedrock_use_java || 'Use Java to select worlds';
|
||||
selectedWorldText.textContent = bedrockText;
|
||||
selectedWorldText.style.color = '#fecc44';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose to window for onclick handlers
|
||||
window.setWorldFormat = setWorldFormat;
|
||||
|
||||
// Telemetry consent (first run only)
|
||||
function initTelemetryConsent() {
|
||||
const key = 'telemetry-consent'; // values: 'true' | 'false'
|
||||
@@ -533,8 +620,8 @@ function normalizeLongitude(lon) {
|
||||
return ((lon + 180) % 360 + 360) % 360 - 180;
|
||||
}
|
||||
|
||||
const threshold1 = 30000000.00;
|
||||
const threshold2 = 45000000.00;
|
||||
const threshold1 = 44000000.00; // Yellow warning threshold (~6.2km x 7km)
|
||||
const threshold2 = 85000000.00; // Red error threshold (~8.7km x 9.8km)
|
||||
let selectedBBox = "";
|
||||
let mapSelectedBBox = ""; // Tracks bbox from map selection
|
||||
let customBBoxValid = false; // Tracks if custom input is valid
|
||||
@@ -591,6 +678,14 @@ async function selectWorld(generate_new_world) {
|
||||
const lastSegment = worldName.split(/[\\/]/).pop();
|
||||
document.getElementById('selected-world').textContent = lastSegment;
|
||||
document.getElementById('selected-world').style.color = "#fecc44";
|
||||
|
||||
// Notify that world changed (reset preview)
|
||||
notifyWorldChanged();
|
||||
|
||||
// If selecting an existing world, check for existing map data
|
||||
if (!generate_new_world) {
|
||||
await loadExistingWorldMapData();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleWorldSelectionError(error);
|
||||
@@ -599,6 +694,32 @@ async function selectWorld(generate_new_world) {
|
||||
closeWorldPicker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads existing world map data if available (for existing worlds)
|
||||
* This will zoom to the location and auto-enable the preview
|
||||
*/
|
||||
async function loadExistingWorldMapData() {
|
||||
if (!worldPath) return;
|
||||
|
||||
try {
|
||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
||||
if (mapData) {
|
||||
currentWorldMapData = mapData;
|
||||
|
||||
// Send data to the map iframe with instruction to zoom and auto-enable
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'loadExistingWorldMap',
|
||||
data: mapData
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("No existing world map data found:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles world selection errors and displays appropriate messages
|
||||
* @param {number} errorCode - Error code from the backend
|
||||
@@ -638,13 +759,17 @@ async function startGeneration() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!worldPath || worldPath === "") {
|
||||
// Only require world selection for Java format (Bedrock generates a new .mcworld file)
|
||||
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
|
||||
const selectedWorld = document.getElementById('selected-world');
|
||||
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
|
||||
selectedWorld.style.color = "#fa7878";
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing world preview since we're generating a new one
|
||||
notifyWorldChanged();
|
||||
|
||||
// Get the map iframe reference
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
// Get spawn point coordinates if marker exists
|
||||
@@ -692,7 +817,8 @@ async function startGeneration() {
|
||||
fillgroundEnabled: fill_ground,
|
||||
isNewWorld: isNewWorld,
|
||||
spawnPoint: spawnPoint,
|
||||
telemetryConsent: telemetryConsent || false
|
||||
telemetryConsent: telemetryConsent || false,
|
||||
worldFormat: selectedWorldFormat
|
||||
});
|
||||
|
||||
console.log("Generation process started.");
|
||||
@@ -702,3 +828,60 @@ async function startGeneration() {
|
||||
generationButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// World preview overlay state
|
||||
let worldPreviewEnabled = false;
|
||||
let currentWorldMapData = null;
|
||||
|
||||
/**
|
||||
* Notifies the map iframe that world preview data is ready
|
||||
* Called when the backend emits the map-preview-ready event
|
||||
*/
|
||||
async function showWorldPreviewButton() {
|
||||
// Try to load the world map data
|
||||
await loadWorldMapData();
|
||||
|
||||
if (currentWorldMapData) {
|
||||
// Send data to the map iframe
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'worldPreviewReady',
|
||||
data: currentWorldMapData
|
||||
}, '*');
|
||||
console.log("World preview data sent to map iframe");
|
||||
}
|
||||
} else {
|
||||
console.warn("Map data not available yet");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the map iframe that the world has changed (reset preview)
|
||||
*/
|
||||
function notifyWorldChanged() {
|
||||
currentWorldMapData = null;
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'worldChanged'
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the world map data from the backend
|
||||
*/
|
||||
async function loadWorldMapData() {
|
||||
if (!worldPath) return;
|
||||
|
||||
try {
|
||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
||||
if (mapData) {
|
||||
currentWorldMapData = mapData;
|
||||
console.log("World map data loaded successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load world map data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
3
src/gui/locales/ar.json
vendored
3
src/gui/locales/ar.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "تضاريس فقط",
|
||||
"interior": "توليد الداخلية",
|
||||
"roof": "توليد السقف",
|
||||
"fillground": "ملء الأرض"
|
||||
"fillground": "ملء الأرض",
|
||||
"bedrock_use_java": "استخدم Java لاختيار العوالم"
|
||||
}
|
||||
|
||||
3
src/gui/locales/de.json
vendored
3
src/gui/locales/de.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Nur Terrain",
|
||||
"interior": "Innenraum Generierung",
|
||||
"roof": "Dach Generierung",
|
||||
"fillground": "Boden füllen"
|
||||
"fillground": "Boden füllen",
|
||||
"bedrock_use_java": "Java für Weltauswahl nutzen"
|
||||
}
|
||||
3
src/gui/locales/en-US.json
vendored
3
src/gui/locales/en-US.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Terrain only",
|
||||
"interior": "Interior Generation",
|
||||
"roof": "Roof Generation",
|
||||
"fillground": "Fill Ground"
|
||||
"fillground": "Fill Ground",
|
||||
"bedrock_use_java": "Use Java to select worlds"
|
||||
}
|
||||
3
src/gui/locales/es.json
vendored
3
src/gui/locales/es.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Solo Terreno",
|
||||
"interior": "Generación Interior",
|
||||
"roof": "Generación de Tejado",
|
||||
"fillground": "Rellenar Suelo"
|
||||
"fillground": "Rellenar Suelo",
|
||||
"bedrock_use_java": "Usa Java para elegir mundos"
|
||||
}
|
||||
3
src/gui/locales/fi.json
vendored
3
src/gui/locales/fi.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Vain maasto",
|
||||
"interior": "Sisätilan luonti",
|
||||
"roof": "Katon luonti",
|
||||
"fillground": "Täytä maa"
|
||||
"fillground": "Täytä maa",
|
||||
"bedrock_use_java": "Käytä Javaa maailmojen valintaan"
|
||||
}
|
||||
|
||||
3
src/gui/locales/fr-FR.json
vendored
3
src/gui/locales/fr-FR.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Terrain uniquement",
|
||||
"interior": "Génération d'intérieur",
|
||||
"roof": "Génération de toit",
|
||||
"fillground": "Remplir le sol"
|
||||
"fillground": "Remplir le sol",
|
||||
"bedrock_use_java": "Utilisez Java pour les mondes"
|
||||
}
|
||||
|
||||
3
src/gui/locales/hu.json
vendored
3
src/gui/locales/hu.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Csak terep",
|
||||
"interior": "Belső generálás",
|
||||
"roof": "Tető generálás",
|
||||
"fillground": "Talaj feltöltése"
|
||||
"fillground": "Talaj feltöltése",
|
||||
"bedrock_use_java": "Java világválasztáshoz"
|
||||
}
|
||||
3
src/gui/locales/ko.json
vendored
3
src/gui/locales/ko.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "지형만",
|
||||
"interior": "내부 생성",
|
||||
"roof": "지붕 생성",
|
||||
"fillground": "지면 채우기"
|
||||
"fillground": "지면 채우기",
|
||||
"bedrock_use_java": "Java로 세계 선택"
|
||||
}
|
||||
3
src/gui/locales/lt.json
vendored
3
src/gui/locales/lt.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Tik reljefas",
|
||||
"interior": "Interjero generavimas",
|
||||
"roof": "Stogo generavimas",
|
||||
"fillground": "Užpildyti pagrindą"
|
||||
"fillground": "Užpildyti pagrindą",
|
||||
"bedrock_use_java": "Naudok Java pasauliams"
|
||||
}
|
||||
3
src/gui/locales/lv.json
vendored
3
src/gui/locales/lv.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Tikai reljefs",
|
||||
"interior": "Interjera ģenerēšana",
|
||||
"roof": "Jumta ģenerēšana",
|
||||
"fillground": "Aizpildīt zemi"
|
||||
"fillground": "Aizpildīt zemi",
|
||||
"bedrock_use_java": "Izmanto Java pasaulēm"
|
||||
}
|
||||
3
src/gui/locales/pl.json
vendored
3
src/gui/locales/pl.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Tylko teren",
|
||||
"interior": "Generowanie wnętrza",
|
||||
"roof": "Generowanie dachu",
|
||||
"fillground": "Wypełnij podłoże"
|
||||
"fillground": "Wypełnij podłoże",
|
||||
"bedrock_use_java": "Użyj Java do wyboru światów"
|
||||
}
|
||||
3
src/gui/locales/ru.json
vendored
3
src/gui/locales/ru.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Только Рельеф",
|
||||
"interior": "Генерация Интерьера",
|
||||
"roof": "Генерация Крыши",
|
||||
"fillground": "Заполнить Землю"
|
||||
"fillground": "Заполнить Землю",
|
||||
"bedrock_use_java": "Используйте Java для миров"
|
||||
}
|
||||
|
||||
3
src/gui/locales/sv.json
vendored
3
src/gui/locales/sv.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Endast terräng",
|
||||
"interior": "Interiörgenerering",
|
||||
"roof": "Takgenerering",
|
||||
"fillground": "Fyll mark"
|
||||
"fillground": "Fyll mark",
|
||||
"bedrock_use_java": "Använd Java för världar"
|
||||
}
|
||||
3
src/gui/locales/ua.json
vendored
3
src/gui/locales/ua.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "Тільки рельєф",
|
||||
"interior": "Генерація інтер'єру",
|
||||
"roof": "Генерація даху",
|
||||
"fillground": "Заповнити землю"
|
||||
"fillground": "Заповнити землю",
|
||||
"bedrock_use_java": "Використовуй Java для світів"
|
||||
}
|
||||
3
src/gui/locales/zh-CN.json
vendored
3
src/gui/locales/zh-CN.json
vendored
@@ -44,5 +44,6 @@
|
||||
"mode_terrain_only": "仅地形",
|
||||
"interior": "内部生成",
|
||||
"roof": "屋顶生成",
|
||||
"fillground": "填充地面"
|
||||
"fillground": "填充地面",
|
||||
"bedrock_use_java": "使用Java选择世界"
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod args;
|
||||
#[cfg(feature = "bedrock")]
|
||||
mod bedrock_block_map;
|
||||
mod block_definitions;
|
||||
mod bresenham;
|
||||
mod clipping;
|
||||
mod colors;
|
||||
mod coordinate_system;
|
||||
mod data_processing;
|
||||
mod element_processing;
|
||||
mod elevation_data;
|
||||
mod floodfill;
|
||||
mod ground;
|
||||
mod map_renderer;
|
||||
mod map_transformation;
|
||||
mod osm_parser;
|
||||
#[cfg(feature = "gui")]
|
||||
@@ -26,7 +31,6 @@ use clap::Parser;
|
||||
use colored::*;
|
||||
use std::{env, fs, io::Write};
|
||||
|
||||
mod elevation_data;
|
||||
#[cfg(feature = "gui")]
|
||||
mod gui;
|
||||
|
||||
@@ -35,6 +39,8 @@ mod gui;
|
||||
mod progress {
|
||||
pub fn emit_gui_error(_message: &str) {}
|
||||
pub fn emit_gui_progress_update(_progress: f64, _message: &str) {}
|
||||
pub fn emit_map_preview_ready() {}
|
||||
pub fn emit_open_mcworld_file(_path: &str) {}
|
||||
pub fn is_running_with_gui() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
944
src/map_renderer.rs
Normal file
944
src/map_renderer.rs
Normal file
@@ -0,0 +1,944 @@
|
||||
// Top-down world map renderer for GUI preview.
|
||||
//
|
||||
// Generates a 1:1 pixel-per-block PNG image of the generated world,
|
||||
// showing the topmost visible block at each position.
|
||||
|
||||
use fastanvil::Region;
|
||||
use fastnbt::{from_bytes, Value};
|
||||
use fnv::FnvHashMap;
|
||||
use image::{Rgb, RgbImage};
|
||||
use once_cell::sync::Lazy;
|
||||
use rayon::prelude::*;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Pre-computed block colors for fast lookup
|
||||
static BLOCK_COLORS: Lazy<FnvHashMap<&'static str, Rgb<u8>>> = Lazy::new(get_block_colors);
|
||||
|
||||
/// Renders a top-down view of the generated Minecraft world.
|
||||
/// Returns the path to the saved image file.
|
||||
pub fn render_world_map(
|
||||
world_dir: &Path,
|
||||
min_x: i32,
|
||||
max_x: i32,
|
||||
min_z: i32,
|
||||
max_z: i32,
|
||||
) -> Result<std::path::PathBuf, String> {
|
||||
let width = (max_x - min_x + 1) as u32;
|
||||
let height = (max_z - min_z + 1) as u32;
|
||||
|
||||
if width == 0 || height == 0 {
|
||||
return Err("Invalid world bounds".to_string());
|
||||
}
|
||||
|
||||
// Use Mutex for thread-safe image access
|
||||
let img = Mutex::new(RgbImage::from_pixel(width, height, Rgb([255, 255, 255])));
|
||||
|
||||
// Calculate region range
|
||||
let min_region_x = min_x >> 9; // divide by 512 (32 chunks * 16 blocks)
|
||||
let max_region_x = max_x >> 9;
|
||||
let min_region_z = min_z >> 9;
|
||||
let max_region_z = max_z >> 9;
|
||||
|
||||
let region_dir = world_dir.join("region");
|
||||
|
||||
// Collect all region coordinates for parallel processing
|
||||
let region_coords: Vec<(i32, i32)> = (min_region_x..=max_region_x)
|
||||
.flat_map(|rx| (min_region_z..=max_region_z).map(move |rz| (rx, rz)))
|
||||
.collect();
|
||||
|
||||
// Process regions in parallel
|
||||
region_coords.par_iter().for_each(|&(region_x, region_z)| {
|
||||
let region_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
||||
|
||||
if !region_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(file) = File::open(®ion_path) {
|
||||
if let Ok(mut region) = Region::from_stream(file) {
|
||||
// Collect all pixels from this region first
|
||||
let pixels = render_region_to_pixels(
|
||||
&mut region,
|
||||
region_x,
|
||||
region_z,
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
);
|
||||
|
||||
// Then batch-write to image under lock
|
||||
if !pixels.is_empty() {
|
||||
let mut img_guard = img.lock().unwrap();
|
||||
for (x, z, color) in pixels {
|
||||
if x < img_guard.width() && z < img_guard.height() {
|
||||
img_guard.put_pixel(x, z, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save the image
|
||||
let output_path = world_dir.join("arnis_world_map.png");
|
||||
img.into_inner()
|
||||
.unwrap()
|
||||
.save(&output_path)
|
||||
.map_err(|e| format!("Failed to save map image: {}", e))?;
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Renders all chunks within a region and returns pixel data
|
||||
fn render_region_to_pixels(
|
||||
region: &mut Region<File>,
|
||||
region_x: i32,
|
||||
region_z: i32,
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
max_x: i32,
|
||||
max_z: i32,
|
||||
) -> Vec<(u32, u32, Rgb<u8>)> {
|
||||
let mut pixels = Vec::new();
|
||||
let region_base_x = region_x * 512;
|
||||
let region_base_z = region_z * 512;
|
||||
|
||||
for chunk_local_x in 0..32 {
|
||||
for chunk_local_z in 0..32 {
|
||||
let chunk_base_x = region_base_x + chunk_local_x * 16;
|
||||
let chunk_base_z = region_base_z + chunk_local_z * 16;
|
||||
|
||||
// Skip chunks outside our bounds
|
||||
if chunk_base_x + 15 < min_x
|
||||
|| chunk_base_x > max_x
|
||||
|| chunk_base_z + 15 < min_z
|
||||
|| chunk_base_z > max_z
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(Some(chunk_data)) =
|
||||
region.read_chunk(chunk_local_x as usize, chunk_local_z as usize)
|
||||
{
|
||||
render_chunk_to_pixels(
|
||||
&chunk_data,
|
||||
&mut pixels,
|
||||
chunk_base_x,
|
||||
chunk_base_z,
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pixels
|
||||
}
|
||||
|
||||
/// Renders a single chunk and appends pixel data
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_chunk_to_pixels(
|
||||
chunk_data: &[u8],
|
||||
pixels: &mut Vec<(u32, u32, Rgb<u8>)>,
|
||||
chunk_base_x: i32,
|
||||
chunk_base_z: i32,
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
max_x: i32,
|
||||
max_z: i32,
|
||||
) {
|
||||
// Parse chunk NBT - look for Level.sections or sections depending on format
|
||||
let chunk: Value = match from_bytes(chunk_data) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// Try to get sections from the chunk data
|
||||
let sections = get_sections_from_chunk(&chunk);
|
||||
if sections.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-sort sections by Y (descending) once per chunk, not per column
|
||||
let sorted_sections = get_sorted_sections(§ions);
|
||||
if sorted_sections.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// For each column in the chunk
|
||||
for local_x in 0..16 {
|
||||
for local_z in 0..16 {
|
||||
let world_x = chunk_base_x + local_x;
|
||||
let world_z = chunk_base_z + local_z;
|
||||
|
||||
// Skip if outside our bounds
|
||||
if world_x < min_x || world_x > max_x || world_z < min_z || world_z > max_z {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find topmost non-air block using pre-sorted sections
|
||||
if let Some((block_name, world_y)) =
|
||||
find_top_block_sorted(&sorted_sections, local_x as usize, local_z as usize)
|
||||
{
|
||||
// Strip minecraft: prefix for lookup
|
||||
let short_name = block_name.strip_prefix("minecraft:").unwrap_or(&block_name);
|
||||
|
||||
let base_color = BLOCK_COLORS
|
||||
.get(short_name)
|
||||
.copied()
|
||||
.unwrap_or_else(|| get_fallback_color(&block_name));
|
||||
|
||||
// Apply elevation shading
|
||||
let color = apply_elevation_shading(base_color, world_y);
|
||||
|
||||
let img_x = (world_x - min_x) as u32;
|
||||
let img_z = (world_z - min_z) as u32;
|
||||
|
||||
pixels.push((img_x, img_z, color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies elevation-based shading to a color
|
||||
/// Higher elevations are brighter, lower are darker
|
||||
#[inline]
|
||||
fn apply_elevation_shading(color: Rgb<u8>, y: i32) -> Rgb<u8> {
|
||||
// Base brightness boost of 10%, plus elevation shading
|
||||
// Shading range: -20% darker to +20% brighter (asymmetric, more bright than dark)
|
||||
|
||||
// Normalize Y to a -1.0 to 1.0 range (roughly)
|
||||
// y=0 -> -0.5, y=0 -> 0, y=200 -> +1.0
|
||||
let normalized = (y as f32 / 100.0).clamp(-1.0, 1.0);
|
||||
|
||||
// Base 10% brightness boost + asymmetric elevation shading
|
||||
let elevation_adjust = if normalized >= 0.0 {
|
||||
// Above sea level: up to +20% brighter
|
||||
normalized * 0.20
|
||||
} else {
|
||||
// Below sea level: up to -20% darker
|
||||
normalized * 0.20
|
||||
};
|
||||
|
||||
let multiplier = 1.10 + elevation_adjust;
|
||||
|
||||
Rgb([
|
||||
(color.0[0] as f32 * multiplier).clamp(0.0, 255.0) as u8,
|
||||
(color.0[1] as f32 * multiplier).clamp(0.0, 255.0) as u8,
|
||||
(color.0[2] as f32 * multiplier).clamp(0.0, 255.0) as u8,
|
||||
])
|
||||
}
|
||||
|
||||
/// Extracts sections from chunk data (handles both old and new formats)
|
||||
fn get_sections_from_chunk(chunk: &Value) -> Vec<&Value> {
|
||||
let mut sections = Vec::new();
|
||||
|
||||
// Try new format (1.18+): directly in chunk
|
||||
if let Value::Compound(map) = chunk {
|
||||
if let Some(Value::List(secs)) = map.get("sections") {
|
||||
for sec in secs {
|
||||
sections.push(sec);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
// Try via Level wrapper (older format)
|
||||
if let Some(Value::Compound(level)) = map.get("Level") {
|
||||
if let Some(Value::List(secs)) = level.get("sections") {
|
||||
for sec in secs {
|
||||
sections.push(sec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sections
|
||||
}
|
||||
|
||||
/// Pre-sorts sections by Y coordinate (descending) - called once per chunk
|
||||
/// Returns Vec of (section_y, section_value) for Y tracking
|
||||
fn get_sorted_sections<'a>(sections: &[&'a Value]) -> Vec<(i8, &'a Value)> {
|
||||
let mut sorted: Vec<(i8, &Value)> = sections
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if let Value::Compound(map) = s {
|
||||
if let Some(Value::Byte(y)) = map.get("Y") {
|
||||
return Some((*y, *s));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
sorted.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
sorted
|
||||
}
|
||||
|
||||
/// Finds the topmost non-air block using pre-sorted sections
|
||||
/// Returns (block_name, world_y) where world_y is the actual Y coordinate
|
||||
fn find_top_block_sorted(
|
||||
sorted_sections: &[(i8, &Value)],
|
||||
local_x: usize,
|
||||
local_z: usize,
|
||||
) -> Option<(String, i32)> {
|
||||
for (section_y, section) in sorted_sections {
|
||||
if let Some((block_name, local_y)) = get_block_at_section(section, local_x, local_z) {
|
||||
if !is_transparent_block(&block_name) {
|
||||
// Calculate world Y: section_y * 16 + local_y
|
||||
let world_y = (*section_y as i32) * 16 + local_y as i32;
|
||||
return Some((block_name, world_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Gets the topmost non-air block in a section at the given x,z
|
||||
/// Returns (block_name, local_y) where local_y is 0-15 within the section
|
||||
fn get_block_at_section(
|
||||
section: &Value,
|
||||
local_x: usize,
|
||||
local_z: usize,
|
||||
) -> Option<(String, usize)> {
|
||||
let section_map = match section {
|
||||
Value::Compound(m) => m,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let block_states = match section_map.get("block_states") {
|
||||
Some(Value::Compound(bs)) => bs,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let palette = match block_states.get("palette") {
|
||||
Some(Value::List(p)) => p,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// If palette has only one block, that's the block for the entire section
|
||||
if palette.len() == 1 {
|
||||
// Return with local_y=15 (top of section) for single-block sections
|
||||
return get_block_name_from_palette(&palette[0]).map(|name| (name, 15));
|
||||
}
|
||||
|
||||
let data = match block_states.get("data") {
|
||||
Some(Value::LongArray(d)) => d,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Calculate bits per block
|
||||
let bits_per_block = std::cmp::max(4, (palette.len() as f64).log2().ceil() as usize);
|
||||
let blocks_per_long = 64 / bits_per_block;
|
||||
let mask = (1u64 << bits_per_block) - 1;
|
||||
|
||||
// Search from top (y=15) to bottom (y=0) within this section
|
||||
for local_y in (0..16).rev() {
|
||||
let block_index = local_y * 256 + local_z * 16 + local_x;
|
||||
let long_index = block_index / blocks_per_long;
|
||||
let bit_offset = (block_index % blocks_per_long) * bits_per_block;
|
||||
|
||||
if long_index >= data.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let palette_index = ((data[long_index] as u64 >> bit_offset) & mask) as usize;
|
||||
|
||||
if palette_index < palette.len() {
|
||||
if let Some(name) = get_block_name_from_palette(&palette[palette_index]) {
|
||||
if !is_transparent_block(&name) {
|
||||
return Some((name, local_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Extracts block name from a palette entry
|
||||
fn get_block_name_from_palette(entry: &Value) -> Option<String> {
|
||||
if let Value::Compound(map) = entry {
|
||||
if let Some(Value::String(name)) = map.get("Name") {
|
||||
return Some(name.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Checks if a block should be considered transparent (look through it)
|
||||
fn is_transparent_block(name: &str) -> bool {
|
||||
let short_name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
matches!(
|
||||
short_name,
|
||||
"air"
|
||||
| "cave_air"
|
||||
| "void_air"
|
||||
| "glass"
|
||||
| "glass_pane"
|
||||
| "white_stained_glass"
|
||||
| "gray_stained_glass"
|
||||
| "light_gray_stained_glass"
|
||||
| "brown_stained_glass"
|
||||
| "tinted_glass"
|
||||
| "barrier"
|
||||
| "light"
|
||||
| "short_grass"
|
||||
| "tall_grass"
|
||||
| "dead_bush"
|
||||
| "poppy"
|
||||
| "dandelion"
|
||||
| "blue_orchid"
|
||||
| "azure_bluet"
|
||||
| "iron_bars"
|
||||
| "ladder"
|
||||
| "scaffolding"
|
||||
| "rail"
|
||||
| "powered_rail"
|
||||
| "detector_rail"
|
||||
| "activator_rail"
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a fallback color based on block name patterns
|
||||
fn get_fallback_color(name: &str) -> Rgb<u8> {
|
||||
// Try to guess color from name
|
||||
if name.contains("stone") || name.contains("cobble") || name.contains("andesite") {
|
||||
return Rgb([128, 128, 128]);
|
||||
}
|
||||
if name.contains("dirt") || name.contains("mud") {
|
||||
return Rgb([139, 90, 43]);
|
||||
}
|
||||
if name.contains("sand") {
|
||||
return Rgb([219, 211, 160]);
|
||||
}
|
||||
if name.contains("grass") {
|
||||
return Rgb([86, 125, 70]);
|
||||
}
|
||||
if name.contains("water") {
|
||||
return Rgb([59, 86, 165]);
|
||||
}
|
||||
if name.contains("log") || name.contains("wood") {
|
||||
return Rgb([101, 76, 48]);
|
||||
}
|
||||
if name.contains("leaves") {
|
||||
return Rgb([55, 95, 36]);
|
||||
}
|
||||
if name.contains("planks") {
|
||||
return Rgb([162, 130, 78]);
|
||||
}
|
||||
if name.contains("brick") {
|
||||
return Rgb([150, 97, 83]);
|
||||
}
|
||||
if name.contains("concrete") {
|
||||
return Rgb([128, 128, 128]);
|
||||
}
|
||||
if name.contains("wool") || name.contains("carpet") {
|
||||
return Rgb([220, 220, 220]);
|
||||
}
|
||||
if name.contains("terracotta") {
|
||||
return Rgb([152, 94, 67]);
|
||||
}
|
||||
if name.contains("iron") {
|
||||
return Rgb([200, 200, 200]);
|
||||
}
|
||||
if name.contains("gold") {
|
||||
return Rgb([255, 215, 0]);
|
||||
}
|
||||
if name.contains("diamond") {
|
||||
return Rgb([97, 219, 213]);
|
||||
}
|
||||
if name.contains("emerald") {
|
||||
return Rgb([17, 160, 54]);
|
||||
}
|
||||
if name.contains("lapis") {
|
||||
return Rgb([38, 67, 156]);
|
||||
}
|
||||
if name.contains("redstone") {
|
||||
return Rgb([170, 0, 0]);
|
||||
}
|
||||
if name.contains("netherrack") || name.contains("nether") {
|
||||
return Rgb([111, 54, 53]);
|
||||
}
|
||||
if name.contains("end_stone") {
|
||||
return Rgb([219, 222, 158]);
|
||||
}
|
||||
if name.contains("obsidian") {
|
||||
return Rgb([15, 10, 24]);
|
||||
}
|
||||
if name.contains("deepslate") {
|
||||
return Rgb([72, 72, 73]);
|
||||
}
|
||||
if name.contains("blackstone") {
|
||||
return Rgb([42, 36, 41]);
|
||||
}
|
||||
if name.contains("quartz") {
|
||||
return Rgb([235, 229, 222]);
|
||||
}
|
||||
if name.contains("prismarine") {
|
||||
return Rgb([76, 128, 113]);
|
||||
}
|
||||
if name.contains("copper") {
|
||||
return Rgb([192, 107, 79]);
|
||||
}
|
||||
if name.contains("amethyst") {
|
||||
return Rgb([133, 97, 191]);
|
||||
}
|
||||
if name.contains("moss") {
|
||||
return Rgb([89, 109, 45]);
|
||||
}
|
||||
if name.contains("dripstone") {
|
||||
return Rgb([134, 107, 92]);
|
||||
}
|
||||
|
||||
// Default gray for unknown blocks
|
||||
Rgb([160, 160, 160])
|
||||
}
|
||||
|
||||
/// Returns a mapping of common block names to RGB colors (without minecraft: prefix)
|
||||
fn get_block_colors() -> FnvHashMap<&'static str, Rgb<u8>> {
|
||||
FnvHashMap::from_iter([
|
||||
("grass_block", Rgb([86, 125, 70])),
|
||||
("short_grass", Rgb([86, 125, 70])),
|
||||
("tall_grass", Rgb([86, 125, 70])),
|
||||
("dirt", Rgb([139, 90, 43])),
|
||||
("coarse_dirt", Rgb([119, 85, 59])),
|
||||
("podzol", Rgb([91, 63, 24])),
|
||||
("rooted_dirt", Rgb([144, 103, 76])),
|
||||
("mud", Rgb([60, 57, 61])),
|
||||
("stone", Rgb([128, 128, 128])),
|
||||
("granite", Rgb([149, 108, 91])),
|
||||
("polished_granite", Rgb([154, 112, 98])),
|
||||
("diorite", Rgb([189, 188, 189])),
|
||||
("polished_diorite", Rgb([195, 195, 195])),
|
||||
("andesite", Rgb([136, 136, 137])),
|
||||
("polished_andesite", Rgb([132, 135, 134])),
|
||||
("deepslate", Rgb([72, 72, 73])),
|
||||
("cobbled_deepslate", Rgb([77, 77, 80])),
|
||||
("polished_deepslate", Rgb([72, 72, 73])),
|
||||
("deepslate_bricks", Rgb([70, 70, 71])),
|
||||
("deepslate_tiles", Rgb([54, 54, 55])),
|
||||
("calcite", Rgb([223, 224, 220])),
|
||||
("tuff", Rgb([108, 109, 102])),
|
||||
("dripstone_block", Rgb([134, 107, 92])),
|
||||
("sand", Rgb([219, 211, 160])),
|
||||
("red_sand", Rgb([190, 102, 33])),
|
||||
("gravel", Rgb([131, 127, 126])),
|
||||
("clay", Rgb([160, 166, 179])),
|
||||
("bedrock", Rgb([85, 85, 85])),
|
||||
("water", Rgb([59, 86, 165])),
|
||||
("ice", Rgb([145, 183, 253])),
|
||||
("packed_ice", Rgb([141, 180, 250])),
|
||||
("blue_ice", Rgb([116, 167, 253])),
|
||||
("snow", Rgb([249, 254, 254])),
|
||||
("snow_block", Rgb([249, 254, 254])),
|
||||
("powder_snow", Rgb([248, 253, 253])),
|
||||
("oak_log", Rgb([109, 85, 50])),
|
||||
("oak_planks", Rgb([162, 130, 78])),
|
||||
("oak_slab", Rgb([162, 130, 78])),
|
||||
("oak_stairs", Rgb([162, 130, 78])),
|
||||
("oak_fence", Rgb([162, 130, 78])),
|
||||
("oak_door", Rgb([162, 130, 78])),
|
||||
("spruce_log", Rgb([58, 37, 16])),
|
||||
("spruce_planks", Rgb([115, 85, 49])),
|
||||
("spruce_slab", Rgb([115, 85, 49])),
|
||||
("spruce_stairs", Rgb([115, 85, 49])),
|
||||
("spruce_fence", Rgb([115, 85, 49])),
|
||||
("spruce_door", Rgb([115, 85, 49])),
|
||||
("birch_log", Rgb([216, 215, 210])),
|
||||
("birch_planks", Rgb([196, 179, 123])),
|
||||
("birch_slab", Rgb([196, 179, 123])),
|
||||
("birch_stairs", Rgb([196, 179, 123])),
|
||||
("birch_fence", Rgb([196, 179, 123])),
|
||||
("birch_door", Rgb([196, 179, 123])),
|
||||
("jungle_log", Rgb([85, 68, 25])),
|
||||
("jungle_planks", Rgb([160, 115, 81])),
|
||||
("acacia_log", Rgb([103, 96, 86])),
|
||||
("acacia_planks", Rgb([168, 90, 50])),
|
||||
("dark_oak_log", Rgb([60, 46, 26])),
|
||||
("dark_oak_planks", Rgb([67, 43, 20])),
|
||||
("dark_oak_slab", Rgb([67, 43, 20])),
|
||||
("dark_oak_stairs", Rgb([67, 43, 20])),
|
||||
("dark_oak_fence", Rgb([67, 43, 20])),
|
||||
("dark_oak_door", Rgb([67, 43, 20])),
|
||||
("mangrove_log", Rgb([84, 66, 36])),
|
||||
("mangrove_planks", Rgb([117, 54, 48])),
|
||||
("cherry_log", Rgb([54, 33, 44])),
|
||||
("cherry_planks", Rgb([226, 178, 172])),
|
||||
("bamboo_block", Rgb([122, 129, 52])),
|
||||
("bamboo_planks", Rgb([194, 175, 93])),
|
||||
("crimson_stem", Rgb([92, 25, 29])),
|
||||
("crimson_planks", Rgb([101, 48, 70])),
|
||||
("warped_stem", Rgb([58, 58, 77])),
|
||||
("warped_planks", Rgb([43, 104, 99])),
|
||||
("oak_leaves", Rgb([55, 95, 36])),
|
||||
("spruce_leaves", Rgb([61, 99, 61])),
|
||||
("birch_leaves", Rgb([80, 106, 47])),
|
||||
("jungle_leaves", Rgb([48, 113, 20])),
|
||||
("acacia_leaves", Rgb([75, 104, 40])),
|
||||
("dark_oak_leaves", Rgb([35, 82, 11])),
|
||||
("mangrove_leaves", Rgb([69, 123, 38])),
|
||||
("cherry_leaves", Rgb([228, 177, 197])),
|
||||
("azalea_leaves", Rgb([71, 96, 37])),
|
||||
("stone_bricks", Rgb([122, 122, 122])),
|
||||
("stone_brick_slab", Rgb([122, 122, 122])),
|
||||
("stone_brick_stairs", Rgb([122, 122, 122])),
|
||||
("stone_brick_wall", Rgb([122, 122, 122])),
|
||||
("mossy_stone_bricks", Rgb([115, 121, 105])),
|
||||
("mossy_stone_brick_slab", Rgb([115, 121, 105])),
|
||||
("mossy_stone_brick_stairs", Rgb([115, 121, 105])),
|
||||
("mossy_stone_brick_wall", Rgb([115, 121, 105])),
|
||||
("cracked_stone_bricks", Rgb([118, 117, 118])),
|
||||
("chiseled_stone_bricks", Rgb([119, 119, 119])),
|
||||
("cobblestone", Rgb([128, 127, 127])),
|
||||
("cobblestone_slab", Rgb([128, 127, 127])),
|
||||
("cobblestone_stairs", Rgb([128, 127, 127])),
|
||||
("cobblestone_wall", Rgb([128, 127, 127])),
|
||||
("mossy_cobblestone", Rgb([110, 118, 94])),
|
||||
("mossy_cobblestone_slab", Rgb([110, 118, 94])),
|
||||
("mossy_cobblestone_stairs", Rgb([110, 118, 94])),
|
||||
("mossy_cobblestone_wall", Rgb([110, 118, 94])),
|
||||
("stone_slab", Rgb([128, 128, 128])),
|
||||
("stone_stairs", Rgb([128, 128, 128])),
|
||||
("smooth_stone", Rgb([158, 158, 158])),
|
||||
("smooth_stone_slab", Rgb([158, 158, 158])),
|
||||
("bricks", Rgb([150, 97, 83])),
|
||||
("brick_slab", Rgb([150, 97, 83])),
|
||||
("brick_stairs", Rgb([150, 97, 83])),
|
||||
("brick_wall", Rgb([150, 97, 83])),
|
||||
("mud_bricks", Rgb([137, 103, 79])),
|
||||
("mud_brick_slab", Rgb([137, 103, 79])),
|
||||
("mud_brick_stairs", Rgb([137, 103, 79])),
|
||||
("mud_brick_wall", Rgb([137, 103, 79])),
|
||||
("terracotta", Rgb([152, 94, 67])),
|
||||
("white_terracotta", Rgb([210, 178, 161])),
|
||||
("orange_terracotta", Rgb([162, 84, 38])),
|
||||
("magenta_terracotta", Rgb([149, 88, 109])),
|
||||
("light_blue_terracotta", Rgb([113, 109, 138])),
|
||||
("yellow_terracotta", Rgb([186, 133, 35])),
|
||||
("lime_terracotta", Rgb([104, 118, 53])),
|
||||
("pink_terracotta", Rgb([162, 78, 79])),
|
||||
("gray_terracotta", Rgb([58, 42, 36])),
|
||||
("light_gray_terracotta", Rgb([135, 107, 98])),
|
||||
("cyan_terracotta", Rgb([87, 91, 91])),
|
||||
("purple_terracotta", Rgb([118, 70, 86])),
|
||||
("blue_terracotta", Rgb([74, 60, 91])),
|
||||
("brown_terracotta", Rgb([77, 51, 36])),
|
||||
("green_terracotta", Rgb([76, 83, 42])),
|
||||
("red_terracotta", Rgb([143, 61, 47])),
|
||||
("black_terracotta", Rgb([37, 23, 16])),
|
||||
("white_concrete", Rgb([207, 213, 214])),
|
||||
("orange_concrete", Rgb([224, 97, 0])),
|
||||
("magenta_concrete", Rgb([169, 48, 159])),
|
||||
("light_blue_concrete", Rgb([35, 137, 198])),
|
||||
("yellow_concrete", Rgb([241, 175, 21])),
|
||||
("lime_concrete", Rgb([94, 169, 24])),
|
||||
("pink_concrete", Rgb([214, 101, 143])),
|
||||
("gray_concrete", Rgb([55, 58, 62])),
|
||||
("light_gray_concrete", Rgb([125, 125, 115])),
|
||||
("cyan_concrete", Rgb([21, 119, 136])),
|
||||
("purple_concrete", Rgb([100, 32, 156])),
|
||||
("blue_concrete", Rgb([45, 47, 143])),
|
||||
("brown_concrete", Rgb([96, 60, 32])),
|
||||
("green_concrete", Rgb([73, 91, 36])),
|
||||
("red_concrete", Rgb([142, 33, 33])),
|
||||
("black_concrete", Rgb([8, 10, 15])),
|
||||
("white_wool", Rgb([234, 236, 237])),
|
||||
("orange_wool", Rgb([241, 118, 20])),
|
||||
("magenta_wool", Rgb([190, 68, 179])),
|
||||
("light_blue_wool", Rgb([58, 175, 217])),
|
||||
("yellow_wool", Rgb([249, 198, 40])),
|
||||
("lime_wool", Rgb([112, 185, 26])),
|
||||
("pink_wool", Rgb([238, 141, 172])),
|
||||
("gray_wool", Rgb([63, 68, 72])),
|
||||
("light_gray_wool", Rgb([142, 142, 135])),
|
||||
("cyan_wool", Rgb([21, 138, 145])),
|
||||
("purple_wool", Rgb([122, 42, 173])),
|
||||
("blue_wool", Rgb([53, 57, 157])),
|
||||
("brown_wool", Rgb([114, 72, 41])),
|
||||
("green_wool", Rgb([85, 110, 28])),
|
||||
("red_wool", Rgb([161, 39, 35])),
|
||||
("black_wool", Rgb([21, 21, 26])),
|
||||
("sandstone", Rgb([223, 214, 170])),
|
||||
("sandstone_slab", Rgb([223, 214, 170])),
|
||||
("sandstone_stairs", Rgb([223, 214, 170])),
|
||||
("sandstone_wall", Rgb([223, 214, 170])),
|
||||
("chiseled_sandstone", Rgb([223, 214, 170])),
|
||||
("cut_sandstone", Rgb([225, 217, 171])),
|
||||
("cut_sandstone_slab", Rgb([225, 217, 171])),
|
||||
("smooth_sandstone", Rgb([223, 214, 170])),
|
||||
("smooth_sandstone_slab", Rgb([223, 214, 170])),
|
||||
("smooth_sandstone_stairs", Rgb([223, 214, 170])),
|
||||
("red_sandstone", Rgb([186, 99, 29])),
|
||||
("red_sandstone_slab", Rgb([186, 99, 29])),
|
||||
("red_sandstone_stairs", Rgb([186, 99, 29])),
|
||||
("red_sandstone_wall", Rgb([186, 99, 29])),
|
||||
("smooth_red_sandstone", Rgb([186, 99, 29])),
|
||||
("netherrack", Rgb([111, 54, 53])),
|
||||
("nether_bricks", Rgb([44, 21, 26])),
|
||||
("nether_brick_slab", Rgb([44, 21, 26])),
|
||||
("nether_brick_stairs", Rgb([44, 21, 26])),
|
||||
("nether_brick_wall", Rgb([44, 21, 26])),
|
||||
("nether_brick_fence", Rgb([44, 21, 26])),
|
||||
("red_nether_bricks", Rgb([69, 7, 9])),
|
||||
("red_nether_brick_slab", Rgb([69, 7, 9])),
|
||||
("red_nether_brick_stairs", Rgb([69, 7, 9])),
|
||||
("red_nether_brick_wall", Rgb([69, 7, 9])),
|
||||
("soul_sand", Rgb([81, 62, 51])),
|
||||
("soul_soil", Rgb([75, 57, 46])),
|
||||
("basalt", Rgb([73, 72, 77])),
|
||||
("polished_basalt", Rgb([88, 87, 91])),
|
||||
("smooth_basalt", Rgb([72, 72, 78])),
|
||||
("blackstone", Rgb([42, 36, 41])),
|
||||
("blackstone_slab", Rgb([42, 36, 41])),
|
||||
("blackstone_stairs", Rgb([42, 36, 41])),
|
||||
("blackstone_wall", Rgb([42, 36, 41])),
|
||||
("polished_blackstone", Rgb([53, 49, 56])),
|
||||
("polished_blackstone_bricks", Rgb([48, 43, 50])),
|
||||
("polished_blackstone_brick_slab", Rgb([48, 43, 50])),
|
||||
("polished_blackstone_brick_stairs", Rgb([48, 43, 50])),
|
||||
("polished_blackstone_brick_wall", Rgb([48, 43, 50])),
|
||||
("glowstone", Rgb([171, 131, 84])),
|
||||
("shroomlight", Rgb([240, 146, 70])),
|
||||
("crying_obsidian", Rgb([32, 10, 60])),
|
||||
("obsidian", Rgb([15, 10, 24])),
|
||||
("end_stone", Rgb([219, 222, 158])),
|
||||
("end_stone_bricks", Rgb([218, 224, 162])),
|
||||
("end_stone_brick_slab", Rgb([218, 224, 162])),
|
||||
("end_stone_brick_stairs", Rgb([218, 224, 162])),
|
||||
("end_stone_brick_wall", Rgb([218, 224, 162])),
|
||||
("purpur_block", Rgb([170, 126, 170])),
|
||||
("purpur_pillar", Rgb([171, 129, 171])),
|
||||
("purpur_slab", Rgb([170, 126, 170])),
|
||||
("purpur_stairs", Rgb([170, 126, 170])),
|
||||
("coal_ore", Rgb([105, 105, 105])),
|
||||
("iron_ore", Rgb([136, 130, 127])),
|
||||
("copper_ore", Rgb([124, 125, 120])),
|
||||
("gold_ore", Rgb([143, 140, 125])),
|
||||
("redstone_ore", Rgb([133, 107, 107])),
|
||||
("emerald_ore", Rgb([108, 136, 115])),
|
||||
("lapis_ore", Rgb([99, 112, 135])),
|
||||
("diamond_ore", Rgb([121, 141, 140])),
|
||||
("coal_block", Rgb([16, 15, 15])),
|
||||
("iron_block", Rgb([220, 220, 220])),
|
||||
("copper_block", Rgb([192, 107, 79])),
|
||||
("gold_block", Rgb([246, 208, 62])),
|
||||
("redstone_block", Rgb([170, 0, 0])),
|
||||
("emerald_block", Rgb([42, 203, 88])),
|
||||
("lapis_block", Rgb([38, 67, 156])),
|
||||
("diamond_block", Rgb([97, 219, 213])),
|
||||
("netherite_block", Rgb([66, 61, 63])),
|
||||
("amethyst_block", Rgb([133, 97, 191])),
|
||||
("raw_iron_block", Rgb([166, 136, 107])),
|
||||
("raw_copper_block", Rgb([154, 105, 79])),
|
||||
("raw_gold_block", Rgb([221, 169, 46])),
|
||||
("quartz_block", Rgb([235, 229, 222])),
|
||||
("quartz_slab", Rgb([235, 229, 222])),
|
||||
("quartz_stairs", Rgb([235, 229, 222])),
|
||||
("smooth_quartz", Rgb([235, 229, 222])),
|
||||
("smooth_quartz_slab", Rgb([235, 229, 222])),
|
||||
("smooth_quartz_stairs", Rgb([235, 229, 222])),
|
||||
("quartz_bricks", Rgb([234, 229, 221])),
|
||||
("quartz_pillar", Rgb([235, 230, 224])),
|
||||
("chiseled_quartz_block", Rgb([231, 226, 218])),
|
||||
("prismarine", Rgb([76, 128, 113])),
|
||||
("prismarine_slab", Rgb([76, 128, 113])),
|
||||
("prismarine_stairs", Rgb([76, 128, 113])),
|
||||
("prismarine_wall", Rgb([76, 128, 113])),
|
||||
("prismarine_bricks", Rgb([99, 172, 158])),
|
||||
("prismarine_brick_slab", Rgb([99, 172, 158])),
|
||||
("prismarine_brick_stairs", Rgb([99, 172, 158])),
|
||||
("dark_prismarine", Rgb([51, 91, 75])),
|
||||
("dark_prismarine_slab", Rgb([51, 91, 75])),
|
||||
("dark_prismarine_stairs", Rgb([51, 91, 75])),
|
||||
("sea_lantern", Rgb([172, 199, 190])),
|
||||
("exposed_copper", Rgb([161, 125, 103])),
|
||||
("weathered_copper", Rgb([109, 145, 107])),
|
||||
("oxidized_copper", Rgb([82, 162, 132])),
|
||||
("cut_copper", Rgb([191, 106, 80])),
|
||||
("cut_copper_slab", Rgb([191, 106, 80])),
|
||||
("cut_copper_stairs", Rgb([191, 106, 80])),
|
||||
("exposed_cut_copper", Rgb([154, 121, 101])),
|
||||
("exposed_cut_copper_slab", Rgb([154, 121, 101])),
|
||||
("exposed_cut_copper_stairs", Rgb([154, 121, 101])),
|
||||
("weathered_cut_copper", Rgb([109, 145, 107])),
|
||||
("weathered_cut_copper_slab", Rgb([109, 145, 107])),
|
||||
("weathered_cut_copper_stairs", Rgb([109, 145, 107])),
|
||||
("oxidized_cut_copper", Rgb([79, 153, 126])),
|
||||
("oxidized_cut_copper_slab", Rgb([79, 153, 126])),
|
||||
("oxidized_cut_copper_stairs", Rgb([79, 153, 126])),
|
||||
("glass", Rgb([200, 220, 230])),
|
||||
("glass_pane", Rgb([200, 220, 230])),
|
||||
("white_stained_glass", Rgb([255, 255, 255])),
|
||||
("white_stained_glass_pane", Rgb([255, 255, 255])),
|
||||
("orange_stained_glass", Rgb([216, 127, 51])),
|
||||
("orange_stained_glass_pane", Rgb([216, 127, 51])),
|
||||
("magenta_stained_glass", Rgb([178, 76, 216])),
|
||||
("magenta_stained_glass_pane", Rgb([178, 76, 216])),
|
||||
("light_blue_stained_glass", Rgb([102, 153, 216])),
|
||||
("light_blue_stained_glass_pane", Rgb([102, 153, 216])),
|
||||
("yellow_stained_glass", Rgb([229, 229, 51])),
|
||||
("yellow_stained_glass_pane", Rgb([229, 229, 51])),
|
||||
("lime_stained_glass", Rgb([127, 204, 25])),
|
||||
("lime_stained_glass_pane", Rgb([127, 204, 25])),
|
||||
("pink_stained_glass", Rgb([242, 127, 165])),
|
||||
("pink_stained_glass_pane", Rgb([242, 127, 165])),
|
||||
("gray_stained_glass", Rgb([76, 76, 76])),
|
||||
("gray_stained_glass_pane", Rgb([76, 76, 76])),
|
||||
("light_gray_stained_glass", Rgb([153, 153, 153])),
|
||||
("light_gray_stained_glass_pane", Rgb([153, 153, 153])),
|
||||
("cyan_stained_glass", Rgb([76, 127, 153])),
|
||||
("cyan_stained_glass_pane", Rgb([76, 127, 153])),
|
||||
("purple_stained_glass", Rgb([127, 63, 178])),
|
||||
("purple_stained_glass_pane", Rgb([127, 63, 178])),
|
||||
("blue_stained_glass", Rgb([51, 76, 178])),
|
||||
("blue_stained_glass_pane", Rgb([51, 76, 178])),
|
||||
("brown_stained_glass", Rgb([102, 76, 51])),
|
||||
("brown_stained_glass_pane", Rgb([102, 76, 51])),
|
||||
("green_stained_glass", Rgb([102, 127, 51])),
|
||||
("green_stained_glass_pane", Rgb([102, 127, 51])),
|
||||
("red_stained_glass", Rgb([153, 51, 51])),
|
||||
("red_stained_glass_pane", Rgb([153, 51, 51])),
|
||||
("black_stained_glass", Rgb([25, 25, 25])),
|
||||
("black_stained_glass_pane", Rgb([25, 25, 25])),
|
||||
("bookshelf", Rgb([116, 89, 53])),
|
||||
("hay_block", Rgb([166, 139, 12])),
|
||||
("melon", Rgb([111, 145, 31])),
|
||||
("pumpkin", Rgb([198, 118, 24])),
|
||||
("jack_o_lantern", Rgb([213, 139, 42])),
|
||||
("carved_pumpkin", Rgb([198, 118, 24])),
|
||||
("tnt", Rgb([219, 68, 52])),
|
||||
("sponge", Rgb([195, 192, 74])),
|
||||
("wet_sponge", Rgb([171, 181, 70])),
|
||||
("moss_block", Rgb([89, 109, 45])),
|
||||
("moss_carpet", Rgb([89, 109, 45])),
|
||||
("sculk", Rgb([12, 28, 36])),
|
||||
("honeycomb_block", Rgb([229, 148, 29])),
|
||||
("slime_block", Rgb([111, 192, 91])),
|
||||
("honey_block", Rgb([251, 185, 52])),
|
||||
("barrel", Rgb([140, 106, 60])),
|
||||
("chest", Rgb([155, 113, 48])),
|
||||
("trapped_chest", Rgb([155, 113, 48])),
|
||||
("crafting_table", Rgb([144, 109, 67])),
|
||||
("furnace", Rgb([110, 110, 110])),
|
||||
("blast_furnace", Rgb([80, 80, 85])),
|
||||
("smoker", Rgb([90, 80, 70])),
|
||||
("anvil", Rgb([68, 68, 68])),
|
||||
("lectern", Rgb([180, 140, 90])),
|
||||
("composter", Rgb([100, 80, 45])),
|
||||
("cauldron", Rgb([60, 60, 60])),
|
||||
("hopper", Rgb([70, 70, 70])),
|
||||
("jukebox", Rgb([130, 90, 70])),
|
||||
("note_block", Rgb([120, 80, 65])),
|
||||
("bell", Rgb([200, 170, 50])),
|
||||
("dirt_path", Rgb([148, 121, 65])),
|
||||
("farmland", Rgb([143, 88, 46])),
|
||||
("mycelium", Rgb([111, 99, 107])),
|
||||
("rail", Rgb([125, 108, 77])),
|
||||
("powered_rail", Rgb([153, 126, 55])),
|
||||
("detector_rail", Rgb([120, 97, 80])),
|
||||
("activator_rail", Rgb([117, 85, 76])),
|
||||
("redstone_wire", Rgb([170, 0, 0])),
|
||||
("redstone_torch", Rgb([170, 0, 0])),
|
||||
("redstone_lamp", Rgb([180, 130, 70])),
|
||||
("lever", Rgb([100, 80, 60])),
|
||||
("tripwire_hook", Rgb([120, 100, 80])),
|
||||
("torch", Rgb([255, 200, 100])),
|
||||
("wall_torch", Rgb([255, 200, 100])),
|
||||
("lantern", Rgb([200, 150, 80])),
|
||||
("soul_lantern", Rgb([80, 200, 200])),
|
||||
("soul_torch", Rgb([80, 200, 200])),
|
||||
("soul_wall_torch", Rgb([80, 200, 200])),
|
||||
("campfire", Rgb([200, 100, 50])),
|
||||
("soul_campfire", Rgb([80, 200, 200])),
|
||||
("candle", Rgb([200, 180, 130])),
|
||||
("dandelion", Rgb([255, 236, 85])),
|
||||
("poppy", Rgb([200, 30, 30])),
|
||||
("blue_orchid", Rgb([47, 186, 199])),
|
||||
("allium", Rgb([190, 130, 200])),
|
||||
("azure_bluet", Rgb([220, 230, 220])),
|
||||
("red_tulip", Rgb([200, 50, 50])),
|
||||
("orange_tulip", Rgb([230, 130, 50])),
|
||||
("white_tulip", Rgb([230, 230, 220])),
|
||||
("pink_tulip", Rgb([220, 150, 170])),
|
||||
("oxeye_daisy", Rgb([230, 230, 200])),
|
||||
("cornflower", Rgb([70, 90, 180])),
|
||||
("lily_of_the_valley", Rgb([230, 230, 230])),
|
||||
("wither_rose", Rgb([30, 30, 30])),
|
||||
("sunflower", Rgb([255, 200, 50])),
|
||||
("lilac", Rgb([200, 150, 200])),
|
||||
("rose_bush", Rgb([180, 40, 40])),
|
||||
("peony", Rgb([230, 180, 200])),
|
||||
("fern", Rgb([80, 120, 60])),
|
||||
("large_fern", Rgb([80, 120, 60])),
|
||||
("dead_bush", Rgb([150, 120, 80])),
|
||||
("seagrass", Rgb([40, 100, 60])),
|
||||
("tall_seagrass", Rgb([40, 100, 60])),
|
||||
("kelp", Rgb([50, 110, 60])),
|
||||
("kelp_plant", Rgb([50, 110, 60])),
|
||||
("sugar_cane", Rgb([140, 180, 100])),
|
||||
("bamboo", Rgb([90, 140, 50])),
|
||||
("vine", Rgb([50, 100, 40])),
|
||||
("lily_pad", Rgb([40, 110, 40])),
|
||||
("sweet_berry_bush", Rgb([60, 90, 50])),
|
||||
("cactus", Rgb([85, 127, 52])),
|
||||
("white_carpet", Rgb([234, 236, 237])),
|
||||
("orange_carpet", Rgb([241, 118, 20])),
|
||||
("magenta_carpet", Rgb([190, 68, 179])),
|
||||
("light_blue_carpet", Rgb([58, 175, 217])),
|
||||
("yellow_carpet", Rgb([249, 198, 40])),
|
||||
("lime_carpet", Rgb([112, 185, 26])),
|
||||
("pink_carpet", Rgb([238, 141, 172])),
|
||||
("gray_carpet", Rgb([63, 68, 72])),
|
||||
("light_gray_carpet", Rgb([142, 142, 135])),
|
||||
("cyan_carpet", Rgb([21, 138, 145])),
|
||||
("purple_carpet", Rgb([122, 42, 173])),
|
||||
("blue_carpet", Rgb([53, 57, 157])),
|
||||
("brown_carpet", Rgb([114, 72, 41])),
|
||||
("green_carpet", Rgb([85, 110, 28])),
|
||||
("red_carpet", Rgb([161, 39, 35])),
|
||||
("black_carpet", Rgb([21, 21, 26])),
|
||||
("oak_sign", Rgb([162, 130, 78])),
|
||||
("oak_wall_sign", Rgb([162, 130, 78])),
|
||||
("spruce_sign", Rgb([115, 85, 49])),
|
||||
("spruce_wall_sign", Rgb([115, 85, 49])),
|
||||
("birch_sign", Rgb([196, 179, 123])),
|
||||
("birch_wall_sign", Rgb([196, 179, 123])),
|
||||
("dark_oak_sign", Rgb([67, 43, 20])),
|
||||
("dark_oak_wall_sign", Rgb([67, 43, 20])),
|
||||
("white_bed", Rgb([234, 236, 237])),
|
||||
("orange_bed", Rgb([241, 118, 20])),
|
||||
("magenta_bed", Rgb([190, 68, 179])),
|
||||
("light_blue_bed", Rgb([58, 175, 217])),
|
||||
("yellow_bed", Rgb([249, 198, 40])),
|
||||
("lime_bed", Rgb([112, 185, 26])),
|
||||
("pink_bed", Rgb([238, 141, 172])),
|
||||
("gray_bed", Rgb([63, 68, 72])),
|
||||
("light_gray_bed", Rgb([142, 142, 135])),
|
||||
("cyan_bed", Rgb([21, 138, 145])),
|
||||
("purple_bed", Rgb([122, 42, 173])),
|
||||
("blue_bed", Rgb([53, 57, 157])),
|
||||
("brown_bed", Rgb([114, 72, 41])),
|
||||
("green_bed", Rgb([85, 110, 28])),
|
||||
("red_bed", Rgb([161, 39, 35])),
|
||||
("black_bed", Rgb([21, 21, 26])),
|
||||
("oak_trapdoor", Rgb([162, 130, 78])),
|
||||
("spruce_trapdoor", Rgb([115, 85, 49])),
|
||||
("birch_trapdoor", Rgb([196, 179, 123])),
|
||||
("dark_oak_trapdoor", Rgb([67, 43, 20])),
|
||||
("iron_trapdoor", Rgb([200, 200, 200])),
|
||||
("iron_bars", Rgb([150, 150, 150])),
|
||||
("ladder", Rgb([160, 130, 70])),
|
||||
("wheat", Rgb([200, 180, 80])),
|
||||
("carrots", Rgb([230, 140, 30])),
|
||||
("potatoes", Rgb([180, 160, 80])),
|
||||
("beetroots", Rgb([150, 50, 50])),
|
||||
("pumpkin_stem", Rgb([120, 140, 70])),
|
||||
("melon_stem", Rgb([120, 140, 70])),
|
||||
])
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::clipping::clip_way_to_bbox;
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
@@ -211,7 +212,14 @@ pub fn parse_osm_data(
|
||||
|
||||
nodes_map.insert(element.id, processed.clone());
|
||||
|
||||
processed_elements.push(ProcessedElement::Node(processed));
|
||||
// Only add tagged nodes to processed_elements if they're within or near the bbox
|
||||
// This significantly improves performance by filtering out distant nodes
|
||||
if !element.tags.as_ref().map(|t| t.is_empty()).unwrap_or(true) {
|
||||
// Node has tags, check if it's in the bbox (with some margin)
|
||||
if xzbbox.contains(&xzpoint) {
|
||||
processed_elements.push(ProcessedElement::Node(processed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,13 +234,33 @@ pub fn parse_osm_data(
|
||||
}
|
||||
}
|
||||
|
||||
// Clip the way to bbox to reduce node count dramatically
|
||||
let tags = element.tags.clone().unwrap_or_default();
|
||||
|
||||
// Store unclipped way for relation assembly (clipping happens after ring merging)
|
||||
ways_map.insert(
|
||||
element.id,
|
||||
ProcessedWay {
|
||||
id: element.id,
|
||||
tags: tags.clone(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Clip way nodes for standalone way processing (not relations)
|
||||
let clipped_nodes = clip_way_to_bbox(&nodes, &xzbbox);
|
||||
|
||||
// Skip ways that are completely outside the bbox (empty after clipping)
|
||||
if clipped_nodes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let processed: ProcessedWay = ProcessedWay {
|
||||
id: element.id,
|
||||
tags: element.tags.clone().unwrap_or_default(),
|
||||
nodes,
|
||||
tags: tags.clone(),
|
||||
nodes: clipped_nodes.clone(),
|
||||
};
|
||||
|
||||
ways_map.insert(element.id, processed.clone());
|
||||
processed_elements.push(ProcessedElement::Way(processed));
|
||||
}
|
||||
|
||||
@@ -247,6 +275,9 @@ pub fn parse_osm_data(
|
||||
continue;
|
||||
};
|
||||
|
||||
// Water relations require unclipped ways for ring merging in water_areas.rs
|
||||
let is_water_relation = is_water_element(tags);
|
||||
|
||||
let members: Vec<ProcessedMember> = element
|
||||
.members
|
||||
.iter()
|
||||
@@ -271,7 +302,26 @@ pub fn parse_osm_data(
|
||||
}
|
||||
};
|
||||
|
||||
Some(ProcessedMember { role, way })
|
||||
// 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);
|
||||
if clipped_nodes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
ProcessedWay {
|
||||
id: way.id,
|
||||
tags: way.tags,
|
||||
nodes: clipped_nodes,
|
||||
}
|
||||
};
|
||||
|
||||
Some(ProcessedMember {
|
||||
role,
|
||||
way: final_way,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -289,6 +339,30 @@ pub fn parse_osm_data(
|
||||
(processed_elements, xzbbox)
|
||||
}
|
||||
|
||||
/// Returns true if tags indicate a water element handled by water_areas.rs.
|
||||
fn is_water_element(tags: &HashMap<String, String>) -> bool {
|
||||
// Check for explicit water tag
|
||||
if tags.contains_key("water") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for natural=water or natural=bay
|
||||
if let Some(natural_val) = tags.get("natural") {
|
||||
if natural_val == "water" || natural_val == "bay" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for waterway=dock (also handled as water area)
|
||||
if let Some(waterway_val) = tags.get("waterway") {
|
||||
if waterway_val == "dock" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
const PRIORITY_ORDER: [&str; 6] = [
|
||||
"entrance", "building", "highway", "waterway", "water", "barrier",
|
||||
];
|
||||
|
||||
@@ -56,3 +56,21 @@ pub fn emit_gui_error(message: &str) {
|
||||
};
|
||||
emit_gui_progress_update(0.0, &format!("Error! {truncated_message}"));
|
||||
}
|
||||
|
||||
/// Emits an event when the world map preview is ready
|
||||
pub fn emit_map_preview_ready() {
|
||||
if let Some(window) = get_main_window() {
|
||||
if let Err(e) = window.emit("map-preview-ready", ()) {
|
||||
eprintln!("Failed to emit map-preview-ready event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits an event to open the generated mcworld file
|
||||
pub fn emit_open_mcworld_file(path: &str) {
|
||||
if let Some(window) = get_main_window() {
|
||||
if let Err(e) = window.emit("open-mcworld-file", path) {
|
||||
eprintln!("Failed to emit open-mcworld-file event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1030
src/world_editor.rs
1030
src/world_editor.rs
File diff suppressed because it is too large
Load Diff
1070
src/world_editor/bedrock.rs
Normal file
1070
src/world_editor/bedrock.rs
Normal file
File diff suppressed because it is too large
Load Diff
312
src/world_editor/common.rs
Normal file
312
src/world_editor/common.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
//! Common data structures for world modification.
|
||||
//!
|
||||
//! This module contains the internal data structures used to track block changes
|
||||
//! before they are written to either Java or Bedrock format.
|
||||
|
||||
use crate::block_definitions::*;
|
||||
use fastnbt::{LongArray, Value};
|
||||
use fnv::FnvHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Chunk structure for Java Edition NBT format
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Chunk {
|
||||
pub sections: Vec<Section>,
|
||||
pub x_pos: i32,
|
||||
pub z_pos: i32,
|
||||
#[serde(default)]
|
||||
pub is_light_on: u8,
|
||||
#[serde(flatten)]
|
||||
pub other: FnvHashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Section within a chunk (16x16x16 blocks)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct Section {
|
||||
pub block_states: Blockstates,
|
||||
#[serde(rename = "Y")]
|
||||
pub y: i8,
|
||||
#[serde(flatten)]
|
||||
pub other: FnvHashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Block states within a section
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct Blockstates {
|
||||
pub palette: Vec<PaletteItem>,
|
||||
pub data: Option<LongArray>,
|
||||
#[serde(flatten)]
|
||||
pub other: FnvHashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Palette item for block state encoding
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct PaletteItem {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "Properties")]
|
||||
pub properties: Option<Value>,
|
||||
}
|
||||
|
||||
/// A section being modified (16x16x16 blocks)
|
||||
pub(crate) struct SectionToModify {
|
||||
pub blocks: [Block; 4096],
|
||||
/// Store properties for blocks that have them, indexed by the same index as blocks array
|
||||
pub properties: FnvHashMap<usize, Value>,
|
||||
}
|
||||
|
||||
impl SectionToModify {
|
||||
#[inline]
|
||||
pub fn get_block(&self, x: u8, y: u8, z: u8) -> Option<Block> {
|
||||
let b = self.blocks[Self::index(x, y, z)];
|
||||
if b == AIR {
|
||||
return None;
|
||||
}
|
||||
Some(b)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block(&mut self, x: u8, y: u8, z: u8, block: Block) {
|
||||
self.blocks[Self::index(x, y, z)] = block;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block_with_properties(
|
||||
&mut self,
|
||||
x: u8,
|
||||
y: u8,
|
||||
z: u8,
|
||||
block_with_props: BlockWithProperties,
|
||||
) {
|
||||
let index = Self::index(x, y, z);
|
||||
self.blocks[index] = block_with_props.block;
|
||||
|
||||
// Store properties if they exist
|
||||
if let Some(props) = block_with_props.properties {
|
||||
self.properties.insert(index, props);
|
||||
} else {
|
||||
// Remove any existing properties for this position
|
||||
self.properties.remove(&index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate index from coordinates (YZX order)
|
||||
#[inline(always)]
|
||||
pub fn index(x: u8, y: u8, z: u8) -> usize {
|
||||
usize::from(y) % 16 * 256 + usize::from(z) * 16 + usize::from(x)
|
||||
}
|
||||
|
||||
/// Convert to Java Edition section format
|
||||
pub fn to_section(&self, y: i8) -> Section {
|
||||
// Create a map of unique block+properties combinations to palette indices
|
||||
let mut unique_blocks: Vec<(Block, Option<Value>)> = Vec::new();
|
||||
let mut palette_lookup: FnvHashMap<(Block, Option<String>), usize> = FnvHashMap::default();
|
||||
|
||||
// Build unique block combinations and lookup table
|
||||
for (i, &block) in self.blocks.iter().enumerate() {
|
||||
let properties = self.properties.get(&i).cloned();
|
||||
|
||||
// Create a key for the lookup (block + properties hash)
|
||||
let props_key = properties.as_ref().map(|p| format!("{p:?}"));
|
||||
let lookup_key = (block, props_key);
|
||||
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = palette_lookup.entry(lookup_key) {
|
||||
let palette_index = unique_blocks.len();
|
||||
e.insert(palette_index);
|
||||
unique_blocks.push((block, properties));
|
||||
}
|
||||
}
|
||||
|
||||
let mut bits_per_block = 4; // minimum allowed
|
||||
while (1 << bits_per_block) < unique_blocks.len() {
|
||||
bits_per_block += 1;
|
||||
}
|
||||
|
||||
let mut data = vec![];
|
||||
let mut cur = 0;
|
||||
let mut cur_idx = 0;
|
||||
|
||||
for (i, &block) in self.blocks.iter().enumerate() {
|
||||
let properties = self.properties.get(&i).cloned();
|
||||
let props_key = properties.as_ref().map(|p| format!("{p:?}"));
|
||||
let lookup_key = (block, props_key);
|
||||
let p = palette_lookup[&lookup_key] as i64;
|
||||
|
||||
if cur_idx + bits_per_block > 64 {
|
||||
data.push(cur);
|
||||
cur = 0;
|
||||
cur_idx = 0;
|
||||
}
|
||||
|
||||
cur |= p << cur_idx;
|
||||
cur_idx += bits_per_block;
|
||||
}
|
||||
|
||||
if cur_idx > 0 {
|
||||
data.push(cur);
|
||||
}
|
||||
|
||||
let palette = unique_blocks
|
||||
.iter()
|
||||
.map(|(block, stored_props)| PaletteItem {
|
||||
name: block.name().to_string(),
|
||||
properties: stored_props.clone().or_else(|| block.properties()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Section {
|
||||
block_states: Blockstates {
|
||||
palette,
|
||||
data: Some(LongArray::new(data)),
|
||||
other: FnvHashMap::default(),
|
||||
},
|
||||
y,
|
||||
other: FnvHashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SectionToModify {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blocks: [AIR; 4096],
|
||||
properties: FnvHashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A chunk being modified (16x384x16 blocks, divided into sections)
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ChunkToModify {
|
||||
pub sections: FnvHashMap<i8, SectionToModify>,
|
||||
pub other: FnvHashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl ChunkToModify {
|
||||
#[inline]
|
||||
pub fn get_block(&self, x: u8, y: i32, z: u8) -> Option<Block> {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
let section = self.sections.get(§ion_idx)?;
|
||||
section.get_block(x, (y & 15).try_into().unwrap(), z)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block(&mut self, x: u8, y: i32, z: u8, block: Block) {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
let section = self.sections.entry(section_idx).or_default();
|
||||
section.set_block(x, (y & 15).try_into().unwrap(), z, block);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block_with_properties(
|
||||
&mut self,
|
||||
x: u8,
|
||||
y: i32,
|
||||
z: u8,
|
||||
block_with_props: BlockWithProperties,
|
||||
) {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
let section = self.sections.entry(section_idx).or_default();
|
||||
section.set_block_with_properties(x, (y & 15).try_into().unwrap(), z, block_with_props);
|
||||
}
|
||||
|
||||
pub fn sections(&self) -> impl Iterator<Item = Section> + '_ {
|
||||
self.sections.iter().map(|(y, s)| s.to_section(*y))
|
||||
}
|
||||
}
|
||||
|
||||
/// A region being modified (32x32 chunks)
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RegionToModify {
|
||||
pub chunks: FnvHashMap<(i32, i32), ChunkToModify>,
|
||||
}
|
||||
|
||||
impl RegionToModify {
|
||||
#[inline]
|
||||
pub fn get_or_create_chunk(&mut self, x: i32, z: i32) -> &mut ChunkToModify {
|
||||
self.chunks.entry((x, z)).or_default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_chunk(&self, x: i32, z: i32) -> Option<&ChunkToModify> {
|
||||
self.chunks.get(&(x, z))
|
||||
}
|
||||
}
|
||||
|
||||
/// The entire world being modified
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WorldToModify {
|
||||
pub regions: FnvHashMap<(i32, i32), RegionToModify>,
|
||||
}
|
||||
|
||||
impl WorldToModify {
|
||||
#[inline]
|
||||
pub fn get_or_create_region(&mut self, x: i32, z: i32) -> &mut RegionToModify {
|
||||
self.regions.entry((x, z)).or_default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_region(&self, x: i32, z: i32) -> Option<&RegionToModify> {
|
||||
self.regions.get(&(x, z))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_block(&self, x: i32, y: i32, z: i32) -> Option<Block> {
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let region: &RegionToModify = self.get_region(region_x, region_z)?;
|
||||
let chunk: &ChunkToModify = region.get_chunk(chunk_x & 31, chunk_z & 31)?;
|
||||
|
||||
chunk.get_block(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
(z & 15).try_into().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block(&mut self, x: i32, y: i32, z: i32, block: Block) {
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let region: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
|
||||
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
chunk.set_block(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
(z & 15).try_into().unwrap(),
|
||||
block,
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block_with_properties(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
block_with_props: BlockWithProperties,
|
||||
) {
|
||||
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: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
|
||||
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
chunk.set_block_with_properties(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
(z & 15).try_into().unwrap(),
|
||||
block_with_props,
|
||||
);
|
||||
}
|
||||
}
|
||||
321
src/world_editor/java.rs
Normal file
321
src/world_editor/java.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
//! Java Edition Anvil format world saving.
|
||||
//!
|
||||
//! This module handles saving worlds in the Java Edition Anvil (.mca) format.
|
||||
|
||||
use super::common::{Chunk, ChunkToModify, Section};
|
||||
use super::WorldEditor;
|
||||
use crate::block_definitions::GRASS_BLOCK;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use colored::Colorize;
|
||||
use fastanvil::Region;
|
||||
use fastnbt::Value;
|
||||
use fnv::FnvHashMap;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use rayon::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
#[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 out_path = self
|
||||
.world_dir
|
||||
.join(format!("region/r.{}.{}.mca", region_x, region_z));
|
||||
|
||||
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
|
||||
pub(super) 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
// 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.
|
||||
pub(super) fn save_java(&mut self) {
|
||||
println!("{} Saving world...", "[7/7]".bold());
|
||||
emit_gui_progress_update(90.0, "Saving world...");
|
||||
|
||||
// Save metadata with error handling
|
||||
if let Err(e) = self.save_metadata() {
|
||||
eprintln!("Failed to save world metadata: {}", e);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, "Failed to save world metadata.");
|
||||
// Continue with world saving even if metadata fails
|
||||
}
|
||||
|
||||
let total_regions = self.world.regions.len() as u64;
|
||||
let save_pb = ProgressBar::new(total_regions);
|
||||
save_pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template(
|
||||
"{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} regions ({eta})",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
|
||||
let regions_processed = AtomicU64::new(0);
|
||||
|
||||
self.world
|
||||
.regions
|
||||
.par_iter()
|
||||
.for_each(|((region_x, region_z), region_to_modify)| {
|
||||
let mut region = self.create_region(*region_x, *region_z);
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
|
||||
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: FnvHashMap::default(),
|
||||
}
|
||||
};
|
||||
|
||||
// Update sections while preserving existing data
|
||||
let new_sections: Vec<Section> = chunk_to_modify.sections().collect();
|
||||
for new_section in new_sections {
|
||||
if let Some(existing_section) =
|
||||
chunk.sections.iter_mut().find(|s| s.y == new_section.y)
|
||||
{
|
||||
// Merge block states
|
||||
existing_section.block_states.palette =
|
||||
new_section.block_states.palette;
|
||||
existing_section.block_states.data = new_section.block_states.data;
|
||||
} else {
|
||||
// Add new section if it doesn't exist
|
||||
chunk.sections.push(new_section);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve existing block entities and merge with new ones
|
||||
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities")
|
||||
{
|
||||
if let (Value::List(existing), Value::List(new)) =
|
||||
(existing_entities, new_entities)
|
||||
{
|
||||
// Remove old entities that are replaced by new ones
|
||||
existing.retain(|e| {
|
||||
if let Value::Compound(map) = e {
|
||||
let (x, y, z) = get_entity_coords(map);
|
||||
!new.iter().any(|new_e| {
|
||||
if let Value::Compound(new_map) = new_e {
|
||||
let (nx, ny, nz) = get_entity_coords(new_map);
|
||||
x == nx && y == ny && z == nz
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Add new entities
|
||||
existing.extend(new.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no existing entities, just add the new ones
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities")
|
||||
{
|
||||
chunk
|
||||
.other
|
||||
.insert("block_entities".to_string(), new_entities.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Update chunk coordinates and flags
|
||||
chunk.x_pos = chunk_x + (region_x * 32);
|
||||
chunk.z_pos = chunk_z + (region_z * 32);
|
||||
|
||||
// Create Level wrapper and save
|
||||
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
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress
|
||||
let regions_done = regions_processed.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
|
||||
// Update progress at regular intervals (every ~1% or at least every 10 regions)
|
||||
// This ensures progress is visible even with many regions
|
||||
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;
|
||||
emit_gui_progress_update(progress, "Saving world...");
|
||||
}
|
||||
|
||||
save_pb.inc(1);
|
||||
});
|
||||
|
||||
save_pb.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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(),
|
||||
),
|
||||
)]);
|
||||
|
||||
// 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(),
|
||||
),
|
||||
),
|
||||
])),
|
||||
)])
|
||||
}
|
||||
587
src/world_editor/mod.rs
Normal file
587
src/world_editor/mod.rs
Normal file
@@ -0,0 +1,587 @@
|
||||
//! World editor module for generating Minecraft worlds.
|
||||
//!
|
||||
//! This module provides the `WorldEditor` struct which handles block placement
|
||||
//! and world saving in both Java Edition (Anvil) and Bedrock Edition (.mcworld) formats.
|
||||
//!
|
||||
//! # Module Structure
|
||||
//!
|
||||
//! - `common` - Shared data structures for world modification
|
||||
//! - `java` - Java Edition Anvil format saving
|
||||
//! - `bedrock` - Bedrock Edition .mcworld format saving (behind `bedrock` feature)
|
||||
|
||||
mod common;
|
||||
mod java;
|
||||
|
||||
#[cfg(feature = "bedrock")]
|
||||
pub mod bedrock;
|
||||
|
||||
// Re-export common types used internally
|
||||
pub(crate) use common::WorldToModify;
|
||||
|
||||
#[cfg(feature = "bedrock")]
|
||||
pub(crate) use bedrock::{BedrockSaveError, BedrockWriter};
|
||||
|
||||
use crate::block_definitions::*;
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use colored::Colorize;
|
||||
use fastnbt::Value;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
|
||||
/// World format to generate
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum WorldFormat {
|
||||
/// Java Edition Anvil format (.mca region files)
|
||||
JavaAnvil,
|
||||
/// Bedrock Edition .mcworld format
|
||||
BedrockMcWorld,
|
||||
}
|
||||
|
||||
/// Metadata saved with the world
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct WorldMetadata {
|
||||
pub min_mc_x: i32,
|
||||
pub max_mc_x: i32,
|
||||
pub min_mc_z: i32,
|
||||
pub max_mc_z: i32,
|
||||
|
||||
pub min_geo_lat: f64,
|
||||
pub max_geo_lat: f64,
|
||||
pub min_geo_lon: f64,
|
||||
pub max_geo_lon: f64,
|
||||
}
|
||||
|
||||
/// The main world editor struct for placing blocks and saving worlds.
|
||||
///
|
||||
/// The lifetime `'a` is tied to the `XZBBox` reference, which defines
|
||||
/// the world boundaries and must outlive the WorldEditor instance.
|
||||
pub struct WorldEditor<'a> {
|
||||
world_dir: PathBuf,
|
||||
world: WorldToModify,
|
||||
xzbbox: &'a XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Option<Box<Ground>>,
|
||||
format: WorldFormat,
|
||||
/// Optional level name for Bedrock worlds (e.g., "Arnis World: New York City")
|
||||
bedrock_level_name: Option<String>,
|
||||
/// Optional spawn point for Bedrock worlds (x, z coordinates)
|
||||
bedrock_spawn_point: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
impl<'a> WorldEditor<'a> {
|
||||
/// Creates a new WorldEditor with Java Anvil format (default).
|
||||
///
|
||||
/// This is the default constructor used by CLI mode.
|
||||
#[allow(dead_code)]
|
||||
pub fn new(world_dir: PathBuf, xzbbox: &'a XZBBox, llbbox: LLBBox) -> Self {
|
||||
Self {
|
||||
world_dir,
|
||||
world: WorldToModify::default(),
|
||||
xzbbox,
|
||||
llbbox,
|
||||
ground: None,
|
||||
format: WorldFormat::JavaAnvil,
|
||||
bedrock_level_name: None,
|
||||
bedrock_spawn_point: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new WorldEditor with a specific format and optional level name.
|
||||
///
|
||||
/// Used by GUI mode to support both Java and Bedrock formats.
|
||||
#[allow(dead_code)]
|
||||
pub fn new_with_format_and_name(
|
||||
world_dir: PathBuf,
|
||||
xzbbox: &'a XZBBox,
|
||||
llbbox: LLBBox,
|
||||
format: WorldFormat,
|
||||
bedrock_level_name: Option<String>,
|
||||
bedrock_spawn_point: Option<(i32, i32)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
world_dir,
|
||||
world: WorldToModify::default(),
|
||||
xzbbox,
|
||||
llbbox,
|
||||
ground: None,
|
||||
format,
|
||||
bedrock_level_name,
|
||||
bedrock_spawn_point,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the ground reference for elevation-based block placement
|
||||
pub fn set_ground(&mut self, ground: &Ground) {
|
||||
self.ground = Some(Box::new(ground.clone()));
|
||||
}
|
||||
|
||||
/// Gets a reference to the ground data if available
|
||||
pub fn get_ground(&self) -> Option<&Ground> {
|
||||
self.ground.as_ref().map(|g| g.as_ref())
|
||||
}
|
||||
|
||||
/// Returns the current world format
|
||||
#[allow(dead_code)]
|
||||
pub fn format(&self) -> WorldFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
/// Calculate the absolute Y position from a ground-relative offset
|
||||
#[inline(always)]
|
||||
pub fn get_absolute_y(&self, x: i32, y_offset: i32, z: i32) -> i32 {
|
||||
if let Some(ground) = &self.ground {
|
||||
ground.level(XZPoint::new(
|
||||
x - self.xzbbox.min_x(),
|
||||
z - self.xzbbox.min_z(),
|
||||
)) + y_offset
|
||||
} else {
|
||||
y_offset // If no ground reference, use y_offset as absolute Y
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the minimum world coordinates
|
||||
pub fn get_min_coords(&self) -> (i32, i32) {
|
||||
(self.xzbbox.min_x(), self.xzbbox.min_z())
|
||||
}
|
||||
|
||||
/// Returns the maximum world coordinates
|
||||
pub fn get_max_coords(&self) -> (i32, i32) {
|
||||
(self.xzbbox.max_x(), self.xzbbox.max_z())
|
||||
}
|
||||
|
||||
/// Checks if there's a block at the given coordinates
|
||||
#[allow(unused)]
|
||||
#[inline]
|
||||
pub fn block_at(&self, x: i32, y: i32, z: i32) -> bool {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
self.world.get_block(x, absolute_y, z).is_some()
|
||||
}
|
||||
|
||||
/// Sets a sign at the given coordinates
|
||||
#[allow(clippy::too_many_arguments, dead_code)]
|
||||
pub fn set_sign(
|
||||
&mut self,
|
||||
line1: String,
|
||||
line2: String,
|
||||
line3: String,
|
||||
line4: String,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
_rotation: i8,
|
||||
) {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
let chunk_x = x >> 4;
|
||||
let chunk_z = z >> 4;
|
||||
let region_x = chunk_x >> 5;
|
||||
let region_z = chunk_z >> 5;
|
||||
|
||||
let mut block_entities = HashMap::new();
|
||||
|
||||
let messages = vec![
|
||||
Value::String(format!("\"{line1}\"")),
|
||||
Value::String(format!("\"{line2}\"")),
|
||||
Value::String(format!("\"{line3}\"")),
|
||||
Value::String(format!("\"{line4}\"")),
|
||||
];
|
||||
|
||||
let mut text_data = HashMap::new();
|
||||
text_data.insert("messages".to_string(), Value::List(messages));
|
||||
text_data.insert("color".to_string(), Value::String("black".to_string()));
|
||||
text_data.insert("has_glowing_text".to_string(), Value::Byte(0));
|
||||
|
||||
block_entities.insert("front_text".to_string(), Value::Compound(text_data));
|
||||
block_entities.insert(
|
||||
"id".to_string(),
|
||||
Value::String("minecraft:sign".to_string()),
|
||||
);
|
||||
block_entities.insert("is_waxed".to_string(), Value::Byte(0));
|
||||
block_entities.insert("keepPacked".to_string(), Value::Byte(0));
|
||||
block_entities.insert("x".to_string(), Value::Int(x));
|
||||
block_entities.insert("y".to_string(), Value::Int(absolute_y));
|
||||
block_entities.insert("z".to_string(), Value::Int(z));
|
||||
|
||||
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);
|
||||
|
||||
if let Some(chunk_data) = chunk.other.get_mut("block_entities") {
|
||||
if let Value::List(entities) = chunk_data {
|
||||
entities.push(Value::Compound(block_entities));
|
||||
}
|
||||
} else {
|
||||
chunk.other.insert(
|
||||
"block_entities".to_string(),
|
||||
Value::List(vec![Value::Compound(block_entities)]),
|
||||
);
|
||||
}
|
||||
|
||||
self.set_block(SIGN, x, 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.
|
||||
#[inline]
|
||||
pub fn set_block(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
override_whitelist: Option<&[Block]>,
|
||||
override_blacklist: Option<&[Block]>,
|
||||
) {
|
||||
// Check if coordinates are within bounds
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the absolute Y coordinate based on ground level
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
|
||||
let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
|
||||
// Check against whitelist and blacklist
|
||||
if let Some(whitelist) = override_whitelist {
|
||||
whitelist
|
||||
.iter()
|
||||
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
|
||||
} else if let Some(blacklist) = override_blacklist {
|
||||
!blacklist
|
||||
.iter()
|
||||
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_insert {
|
||||
self.world.set_block(x, absolute_y, z, block);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a block of the specified type at the given coordinates with absolute Y value.
|
||||
#[inline]
|
||||
pub fn set_block_absolute(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
override_whitelist: Option<&[Block]>,
|
||||
override_blacklist: Option<&[Block]>,
|
||||
) {
|
||||
// Check if coordinates are within bounds
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
|
||||
// Check against whitelist and blacklist
|
||||
if let Some(whitelist) = override_whitelist {
|
||||
whitelist
|
||||
.iter()
|
||||
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
|
||||
} else if let Some(blacklist) = override_blacklist {
|
||||
!blacklist
|
||||
.iter()
|
||||
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_insert {
|
||||
self.world.set_block(x, absolute_y, z, block);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a block with properties at the given coordinates with absolute Y value.
|
||||
#[inline]
|
||||
pub fn set_block_with_properties_absolute(
|
||||
&mut self,
|
||||
block_with_props: BlockWithProperties,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
override_whitelist: Option<&[Block]>,
|
||||
override_blacklist: Option<&[Block]>,
|
||||
) {
|
||||
// Check if coordinates are within bounds
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
|
||||
// Check against whitelist and blacklist
|
||||
if let Some(whitelist) = override_whitelist {
|
||||
whitelist
|
||||
.iter()
|
||||
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
|
||||
} else if let Some(blacklist) = override_blacklist {
|
||||
!blacklist
|
||||
.iter()
|
||||
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_insert {
|
||||
self.world
|
||||
.set_block_with_properties(x, absolute_y, z, block_with_props);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills a cuboid area with the specified block between two coordinates.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline]
|
||||
pub fn fill_blocks(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x1: i32,
|
||||
y1: i32,
|
||||
z1: i32,
|
||||
x2: i32,
|
||||
y2: i32,
|
||||
z2: i32,
|
||||
override_whitelist: Option<&[Block]>,
|
||||
override_blacklist: Option<&[Block]>,
|
||||
) {
|
||||
let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
|
||||
let (min_y, max_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
|
||||
let (min_z, max_z) = if z1 < z2 { (z1, z2) } else { (z2, z1) };
|
||||
|
||||
for x in min_x..=max_x {
|
||||
for y_offset in min_y..=max_y {
|
||||
for z in min_z..=max_z {
|
||||
self.set_block(
|
||||
block,
|
||||
x,
|
||||
y_offset,
|
||||
z,
|
||||
override_whitelist,
|
||||
override_blacklist,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills a cuboid area with the specified block between two coordinates using absolute Y values.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline]
|
||||
pub fn fill_blocks_absolute(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x1: i32,
|
||||
y1_absolute: i32,
|
||||
z1: i32,
|
||||
x2: i32,
|
||||
y2_absolute: i32,
|
||||
z2: i32,
|
||||
override_whitelist: Option<&[Block]>,
|
||||
override_blacklist: Option<&[Block]>,
|
||||
) {
|
||||
let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
|
||||
let (min_y, max_y) = if y1_absolute < y2_absolute {
|
||||
(y1_absolute, y2_absolute)
|
||||
} else {
|
||||
(y2_absolute, y1_absolute)
|
||||
};
|
||||
let (min_z, max_z) = if z1 < z2 { (z1, z2) } else { (z2, z1) };
|
||||
|
||||
for x in min_x..=max_x {
|
||||
for absolute_y in min_y..=max_y {
|
||||
for z in min_z..=max_z {
|
||||
self.set_block_absolute(
|
||||
block,
|
||||
x,
|
||||
absolute_y,
|
||||
z,
|
||||
override_whitelist,
|
||||
override_blacklist,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for a block at the given coordinates.
|
||||
#[inline]
|
||||
pub fn check_for_block(&self, x: i32, y: i32, z: i32, whitelist: Option<&[Block]>) -> bool {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
|
||||
// Retrieve the chunk modification map
|
||||
if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
|
||||
if let Some(whitelist) = whitelist {
|
||||
if whitelist
|
||||
.iter()
|
||||
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
|
||||
{
|
||||
return true; // Block is in the list
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Checks for a block at the given coordinates with absolute Y value.
|
||||
#[allow(unused)]
|
||||
pub fn check_for_block_absolute(
|
||||
&self,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
whitelist: Option<&[Block]>,
|
||||
blacklist: Option<&[Block]>,
|
||||
) -> bool {
|
||||
// Retrieve the chunk modification map
|
||||
if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
|
||||
// Check against whitelist and blacklist
|
||||
if let Some(whitelist) = whitelist {
|
||||
if whitelist
|
||||
.iter()
|
||||
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
|
||||
{
|
||||
return true; // Block is in whitelist
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if let Some(blacklist) = blacklist {
|
||||
if blacklist
|
||||
.iter()
|
||||
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
|
||||
{
|
||||
return true; // Block is in blacklist
|
||||
}
|
||||
}
|
||||
return whitelist.is_none() && blacklist.is_none();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Checks if a block exists at the given coordinates with absolute Y value.
|
||||
///
|
||||
/// Unlike `check_for_block_absolute`, this doesn't filter by block type.
|
||||
#[allow(unused)]
|
||||
pub fn block_at_absolute(&self, x: i32, absolute_y: i32, z: i32) -> bool {
|
||||
self.world.get_block(x, absolute_y, z).is_some()
|
||||
}
|
||||
|
||||
/// Saves all changes made to the world by writing to the appropriate format.
|
||||
pub fn save(&mut self) {
|
||||
println!(
|
||||
"Generating world for: {}",
|
||||
match self.format {
|
||||
WorldFormat::JavaAnvil => "Java Edition (Anvil)",
|
||||
WorldFormat::BedrockMcWorld => "Bedrock Edition (.mcworld)",
|
||||
}
|
||||
);
|
||||
|
||||
match self.format {
|
||||
WorldFormat::JavaAnvil => self.save_java(),
|
||||
WorldFormat::BedrockMcWorld => self.save_bedrock(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn save_bedrock(&mut self) {
|
||||
println!("{} Saving Bedrock world...", "[7/7]".bold());
|
||||
emit_gui_progress_update(90.0, "Saving Bedrock world...");
|
||||
|
||||
#[cfg(feature = "bedrock")]
|
||||
{
|
||||
if let Err(error) = self.save_bedrock_internal() {
|
||||
eprintln!("Failed to save Bedrock world: {error}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Error,
|
||||
&format!("Failed to save Bedrock world: {error}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "bedrock"))]
|
||||
{
|
||||
eprintln!(
|
||||
"Bedrock output requested but the 'bedrock' feature is not enabled at build time."
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Error,
|
||||
"Bedrock output requested but the 'bedrock' feature is not enabled at build time.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bedrock")]
|
||||
fn save_bedrock_internal(&mut self) -> Result<(), BedrockSaveError> {
|
||||
// Use the stored level name if available, otherwise extract from path
|
||||
let level_name = self.bedrock_level_name.clone().unwrap_or_else(|| {
|
||||
self.world_dir
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Arnis World")
|
||||
.to_string()
|
||||
});
|
||||
|
||||
BedrockWriter::new(
|
||||
self.world_dir.clone(),
|
||||
level_name,
|
||||
self.bedrock_spawn_point,
|
||||
self.ground.clone(),
|
||||
)
|
||||
.write_world(&self.world, self.xzbbox, &self.llbbox)
|
||||
}
|
||||
|
||||
/// Saves world metadata to a JSON file
|
||||
pub(crate) fn save_metadata(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let metadata_path = self.world_dir.join("metadata.json");
|
||||
|
||||
let mut file = File::create(&metadata_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to create metadata file at {}: {}",
|
||||
metadata_path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
let metadata = WorldMetadata {
|
||||
min_mc_x: self.xzbbox.min_x(),
|
||||
max_mc_x: self.xzbbox.max_x(),
|
||||
min_mc_z: self.xzbbox.min_z(),
|
||||
max_mc_z: self.xzbbox.max_z(),
|
||||
|
||||
min_geo_lat: self.llbbox.min().lat(),
|
||||
max_geo_lat: self.llbbox.max().lat(),
|
||||
min_geo_lon: self.llbbox.min().lng(),
|
||||
max_geo_lon: self.llbbox.max().lng(),
|
||||
};
|
||||
|
||||
let contents = serde_json::to_string(&metadata)
|
||||
.map_err(|e| format!("Failed to serialize metadata to JSON: {}", e))?;
|
||||
|
||||
write!(&mut file, "{}", contents)
|
||||
.map_err(|e| format!("Failed to write metadata to file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Arnis",
|
||||
"version": "2.3.1",
|
||||
"version": "2.4.0",
|
||||
"identifier": "com.louisdev.arnis",
|
||||
"build": {
|
||||
"frontendDist": "src/gui"
|
||||
@@ -16,7 +16,7 @@
|
||||
"minWidth": 1000,
|
||||
"minHeight": 650,
|
||||
"resizable": true,
|
||||
"transparent": true,
|
||||
"transparent": false,
|
||||
"center": true,
|
||||
"theme": "Dark",
|
||||
"additionalBrowserArgs": "--disable-features=VizDisplayCompositor"
|
||||
|
||||
Reference in New Issue
Block a user