Compare commits

...

55 Commits

Author SHA1 Message Date
Louis Erbkamm
5d97391820 Merge pull request #664 from louis-e/single-bbox 2025-12-07 20:32:41 +01:00
louis-e
bef3cfb090 Allow only one bbox selection at a time 2025-12-07 19:37:49 +01:00
Louis Erbkamm
5a898944f7 Merge pull request #663 from louis-e/fix-world-lock-during-map-preview
Fix world lock held during map preview generation
2025-12-07 19:24:40 +01:00
louis-e
9fdd960009 Fix world lock held during map preview generation 2025-12-07 18:18:12 +01:00
Louis Erbkamm
58e4a337d9 Merge pull request #661 from louis-e/disable-transparent
Disable transparent flag
2025-12-07 15:06:33 +01:00
louis-e
236a7e5af9 Disable transparent flag 2025-12-07 15:04:02 +01:00
Louis Erbkamm
9173e5b4de Merge pull request #654 from louis-e/prepare-v2.4.0
Prepare release v2.4.0
2025-12-05 18:01:58 +01:00
louis-e
1fd02d8005 Prepare release v2.4.0 2025-12-05 17:49:58 +01:00
Louis Erbkamm
438b2beceb Merge pull request #653 from louis-e/bedrock-support
Bedrock support
2025-12-05 17:34:03 +01:00
louis-e
a62e181c16 Only modify spawn position in level.dat for Java 2025-12-05 17:26:34 +01:00
louis-e
12abba3bc8 Remove telemetry line 2025-12-05 17:25:48 +01:00
louis-e
a8e31700d8 Remove comment 2025-12-05 16:56:17 +01:00
louis-e
7a109cce0b Mock emit_open_mcworld_file 2025-12-05 16:51:30 +01:00
louis-e
86543714af Mention Bedrock Edition in README 2025-12-05 16:51:00 +01:00
louis-e
b84a565210 Restore format selection earlier in call chain 2025-12-05 16:50:44 +01:00
louis-e
93becaae7f Include world_icon via include_bytes 2025-12-05 16:39:36 +01:00
louis-e
06e377ce29 Emit done msg after snowman lock is released 2025-12-05 01:00:39 +01:00
louis-e
e22380bdd3 Fix progress bar updates on Java gen with many regions 2025-12-05 00:45:13 +01:00
louis-e
35cac44209 Release snowman lock before map preview generation 2025-12-05 00:43:51 +01:00
louis-e
61af45d2f4 Skip map preview for large areas 2025-12-05 00:39:39 +01:00
louis-e
393f1f9bd8 Increase map loading failure count 2025-12-05 00:39:20 +01:00
louis-e
e6f8466177 Adjust area size thresholds 2025-12-05 00:38:52 +01:00
louis-e
02d3a32a03 Scale gaussian blur correctly with area size 2025-12-05 00:31:51 +01:00
louis-e
f00304ff3a Fix spawn_y spawn in Bedrock 2025-12-04 23:52:44 +01:00
louis-e
a93b908104 Fix bedrock progress bar 2025-12-04 22:57:58 +01:00
louis-e
7cbc4fa263 Fix incorrectly mapped Bedrock blocks 2025-12-04 22:24:56 +01:00
louis-e
7e7f7ed476 Support spawn position marker on Bedrock 2025-12-04 19:00:14 +01:00
louis-e
3c0ba60657 Adjust time in benchmark 2025-12-04 18:59:51 +01:00
louis-e
fb438c4a0f Preserver block properties in bedrock 2025-12-04 18:38:50 +01:00
louis-e
5015c8b9b4 Fix linter issues 2025-12-04 18:15:21 +01:00
louis-e
af0ace422f Add bedrock_use_java localization 2025-12-04 18:04:59 +01:00
louis-e
0bb39b7d9e Simplify comments 2025-12-04 17:32:53 +01:00
louis-e
5b5e93b89a Refactor world_editor into modular directory structure 2025-12-04 17:26:12 +01:00
louis-e
958dc2107e Remove console log line 2025-12-04 17:25:14 +01:00
louis-e
562a3bca66 Clean up temp directory after packaging mcworld 2025-12-04 16:52:07 +01:00
louis-e
f1b37fbbb6 Add Bedrock world icon asset 2025-12-04 16:46:50 +01:00
louis-e
b34cbf4307 Add bedrock-rs license credit 2025-12-04 16:46:22 +01:00
louis-e
a03318bb98 Add format toggle logic and mcworld file opening 2025-12-04 16:45:59 +01:00
louis-e
8bb779d6cc Add format toggle button styles 2025-12-04 16:45:47 +01:00
louis-e
6d164102ad Add Java/Bedrock format toggle UI 2025-12-04 16:45:35 +01:00
louis-e
127a0e5e68 Add GUI format toggle and show_in_folder command 2025-12-04 16:44:46 +01:00
louis-e
4a326c3dad Add emit_open_mcworld_file event 2025-12-04 16:44:37 +01:00
louis-e
d4fd9b9cd3 Add GenerationOptions and format-aware world generation 2025-12-04 16:44:19 +01:00
louis-e
ee0521f232 Add Bedrock world format support with LevelDB storage 2025-12-04 16:43:59 +01:00
louis-e
8b3a41b131 Add Java to Bedrock block mapping 2025-12-04 16:43:49 +01:00
louis-e
02594b1cae Add bedrock_block_map module import 2025-12-04 16:43:38 +01:00
louis-e
06ba4db97e Add bedrockrs and dependencies for Bedrock support 2025-12-04 16:43:27 +01:00
louis-e
59d31cfbb8 Add *.mcworld to gitignore 2025-12-04 16:43:13 +01:00
Louis Erbkamm
94388e4164 Merge pull request #651 from louis-e/fix-2x-sprite
fix: correct disabled icon positions for 7-sprite 2x sheet
2025-12-01 21:49:41 +01:00
louis-e
f8c9fd8f4c fix: correct disabled icon positions for 7-sprite 2x sheet 2025-12-01 21:42:45 +01:00
Louis Erbkamm
2ee2d48f6a Merge pull request #650 from louis-e/mock-emit-mappreview
Mock map preview emit for CLI builds
2025-12-01 18:50:04 +01:00
louis-e
56c2f2e5cd Mock mep preview emit for CLI builds 2025-12-01 18:49:40 +01:00
Louis Erbkamm
9d34bc8e92 Merge pull request #649 from louis-e/benchmark-datetime
Add date time in benchmark comment
2025-12-01 18:46:45 +01:00
louis-e
c95b78fdcd Add date time to benchmark comment 2025-12-01 18:46:09 +01:00
louis-e
6e52e08b8a Remove telemetry line 2025-12-01 18:45:55 +01:00
40 changed files with 3982 additions and 1207 deletions

View File

@@ -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
@@ -43,7 +43,7 @@ jobs:
- name: Run benchmark command with memory tracking
id: benchmark
run: |
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --generate-map --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
peak_kb=$(cat peak_mem_kb.txt)
peak_mb=$((peak_kb / 1024))
@@ -57,25 +57,6 @@ jobs:
duration=$((end_time - start_time))
echo "duration=$duration" >> $GITHUB_OUTPUT
- name: Check for map preview
id: map_check
run: |
if [ -f "./world/arnis_world_map.png" ]; then
echo "Map preview generated successfully"
echo "map_exists=true" >> $GITHUB_OUTPUT
else
echo "Map preview not found"
echo "map_exists=false" >> $GITHUB_OUTPUT
fi
- name: Upload map preview as artifact
if: steps.map_check.outputs.map_exists == 'true'
uses: actions/upload-artifact@v4
with:
name: world-map-preview
path: ./world/arnis_world_map.png
retention-days: 60
- name: Format duration and generate summary
id: comment_body
run: |
@@ -84,7 +65,7 @@ jobs:
seconds=$((duration % 60))
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
baseline_time=69
baseline_time=30
diff=$((duration - baseline_time))
abs_diff=${diff#-}
@@ -106,27 +87,20 @@ jobs:
mem_annotation=" (↗ ${mem_percent}% more)"
fi
# Get current timestamp
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
{
echo "summary<<EOF"
echo "## ⏱️ Benchmark Results"
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
echo "🧠 Peak memory usage: **${peak_mem} MB**${mem_annotation}"
echo ""
echo "| Metric | Value |"
echo "|--------|-------|"
echo "| Duration | **${minutes}m ${seconds}s** |"
echo "| Peak Memory | **${peak_mem} MB**${mem_annotation} |"
echo "| Baseline | **${baseline_time}s** |"
echo "| Delta | **${diff}s** |"
echo "| Commit | [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}) |"
echo "📈 Compared against baseline: **${baseline_time}s**"
echo "🧮 Delta: **${diff}s**"
echo "🔢 Commit: [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA})"
echo ""
echo "${verdict}"
echo ""
echo "---"
echo ""
echo "📅 **Last benchmark:** ${benchmark_time} | 📥 [Download generated world map](${run_url}#artifacts)"
echo "📅 **Last benchmark:** ${benchmark_time}"
echo ""
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
echo "EOF"
@@ -138,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 }}

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/wiki
*.mcworld
# Environment files
.env

314
Cargo.lock generated
View File

@@ -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,9 +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",
@@ -198,6 +201,7 @@ dependencies = [
"indicatif",
"itertools 0.14.0",
"log",
"nbtx",
"once_cell",
"rand 0.8.5",
"rayon",
@@ -212,7 +216,9 @@ dependencies = [
"tauri-plugin-shell",
"tempfile",
"tokio",
"vek",
"windows",
"zip",
]
[[package]]
@@ -298,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",
@@ -344,7 +350,7 @@ dependencies = [
"async-signal",
"async-task",
"blocking",
"cfg-if",
"cfg-if 1.0.0",
"event-listener",
"futures-lite",
"rustix 0.38.42",
@@ -371,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",
@@ -467,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"
@@ -637,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"
@@ -767,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"
@@ -863,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"
@@ -960,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]]
@@ -1296,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]]
@@ -1352,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"
@@ -1362,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"
@@ -1470,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",
@@ -1825,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",
]
@@ -1836,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]]
@@ -1847,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",
@@ -2036,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",
]
@@ -2550,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"
@@ -2655,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",
@@ -2717,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"
@@ -2752,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"
@@ -2798,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",
]
@@ -2924,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",
]
@@ -3014,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"
@@ -3057,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",
@@ -3483,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",
@@ -3593,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",
@@ -3612,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"
@@ -3813,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",
@@ -4137,7 +4316,7 @@ dependencies = [
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"cfg-if 1.0.0",
"interpolate_name",
"itertools 0.12.1",
"libc",
@@ -4343,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",
@@ -4428,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",
@@ -4441,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",
@@ -4492,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"
@@ -4612,6 +4805,12 @@ dependencies = [
"serde_core",
]
[[package]]
name = "seq-macro"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
[[package]]
name = "serde"
version = "1.0.228"
@@ -4794,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",
]
@@ -4845,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"
@@ -4866,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"
@@ -5813,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",
]
@@ -5992,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"
@@ -6076,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",
@@ -6102,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",
@@ -6762,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",
]
@@ -6872,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"
@@ -7029,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"

View File

@@ -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,13 +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 }
@@ -46,6 +48,11 @@ 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"] }

View File

@@ -2,7 +2,7 @@
# Arnis [![CI Build Status](https://github.com/louis-e/arnis/actions/workflows/ci-build.yml/badge.svg)](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) [![Download here](https://img.shields.io/badge/Download-here-green)](https://github.com/louis-e/arnis/releases) [![Discord](https://img.shields.io/discord/1326192999738249267?label=Discord&color=%237289da)](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!

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

849
src/bedrock_block_map.rs Normal file
View 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))
));
}
}

View File

@@ -6,15 +6,25 @@ 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,
@@ -22,7 +32,35 @@ 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());
@@ -237,53 +275,102 @@ 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());
// 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;
}
// Generate top-down map preview silently in background after completion
let world_path = args.path.clone();
let bounds = (
xzbbox.min_x(),
xzbbox.max_x(),
xzbbox.min_z(),
xzbbox.max_z(),
);
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(&world_path, bounds.0, bounds.1, bounds.2, bounds.3)
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
crate::progress::emit_map_preview_ready();
emit_map_preview_ready();
}
Ok(Err(e)) => {
eprintln!("Warning: Failed to generate map preview: {}", e);
@@ -293,6 +380,4 @@ pub fn generate_world(
}
}
});
Ok(())
}

View File

@@ -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}",

View File

@@ -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...");
@@ -101,7 +114,8 @@ pub fn run_gui() {
gui_start_generation,
gui_get_version,
gui_check_for_updates,
gui_get_world_map_data
gui_get_world_map_data,
gui_show_in_folder
])
.setup(|app| {
let app_handle = app.handle();
@@ -721,6 +735,57 @@ struct WorldMapData {
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)]
@@ -738,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;
@@ -749,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) {
@@ -803,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,
@@ -839,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(());
}
@@ -876,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) => {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -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;
}
/* ================================================================== */

View File

@@ -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
View File

@@ -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">

11
src/gui/js/bbox.js vendored
View File

@@ -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;
@@ -899,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({

View File

@@ -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>.

91
src/gui/js/main.js vendored
View File

@@ -24,6 +24,7 @@ window.addEventListener("DOMContentLoaded", async () => {
handleBboxInput();
const localization = await getLocalization();
await applyLocalization(localization);
updateFormatToggleUI(selectedWorldFormat);
initFooter();
await checkForUpdates();
});
@@ -220,6 +221,17 @@ function setupProgressListener() {
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() {
@@ -248,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);
@@ -350,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'
@@ -539,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
@@ -678,7 +759,8 @@ 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";
@@ -735,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.");

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "تضاريس فقط",
"interior": "توليد الداخلية",
"roof": "توليد السقف",
"fillground": "ملء الأرض"
"fillground": "ملء الأرض",
"bedrock_use_java": "استخدم Java لاختيار العوالم"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "지형만",
"interior": "내부 생성",
"roof": "지붕 생성",
"fillground": "지면 채우기"
"fillground": "지면 채우기",
"bedrock_use_java": "Java로 세계 선택"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Только Рельеф",
"interior": "Генерация Интерьера",
"roof": "Генерация Крыши",
"fillground": "Заполнить Землю"
"fillground": "Заполнить Землю",
"bedrock_use_java": "Используйте Java для миров"
}

View File

@@ -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"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Тільки рельєф",
"interior": "Генерація інтер'єру",
"roof": "Генерація даху",
"fillground": "Заповнити землю"
"fillground": "Заповнити землю",
"bedrock_use_java": "Використовуй Java для світів"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "仅地形",
"interior": "内部生成",
"roof": "屋顶生成",
"fillground": "填充地面"
"fillground": "填充地面",
"bedrock_use_java": "使用Java选择世界"
}

View File

@@ -1,6 +1,8 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod args;
#[cfg(feature = "bedrock")]
mod bedrock_block_map;
mod block_definitions;
mod bresenham;
mod clipping;
@@ -37,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
}

View File

@@ -65,3 +65,12 @@ pub fn emit_map_preview_ready() {
}
}
}
/// 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);
}
}
}

View File

File diff suppressed because it is too large Load Diff

1070
src/world_editor/bedrock.rs Normal file
View File

File diff suppressed because it is too large Load Diff

312
src/world_editor/common.rs Normal file
View 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(&section_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
View 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 &region_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) = &section.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
View 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(())
}
}

View File

@@ -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"