Compare commits

..

5 Commits

Author SHA1 Message Date
louis-e
996e8756d0 Mock emit_map_preview_ready 2025-12-01 18:36:29 +01:00
louis-e
0c5bd51ba4 Mock emit_map_preview_ready 2025-12-01 18:32:26 +01:00
louis-e
7965dc3737 Fix CLI build issue 2025-12-01 18:07:52 +01:00
Louis Erbkamm
c54187b43a Merge branch 'main' into benchmark-map-preview 2025-12-01 18:04:14 +01:00
louis-e
beb7b73d11 Include map preview in CI benchmark 2025-12-01 18:02:59 +01:00
55 changed files with 1609 additions and 4896 deletions

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
- 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 --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
/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
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,6 +57,25 @@ 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: |
@@ -65,7 +84,7 @@ jobs:
seconds=$((duration % 60))
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
baseline_time=30
baseline_time=69
diff=$((duration - baseline_time))
abs_diff=${diff#-}
@@ -87,20 +106,27 @@ 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 run finished in **${minutes}m ${seconds}s**"
echo "🧠 Peak memory usage: **${peak_mem} MB**${mem_annotation}"
echo "## ⏱️ Benchmark Results"
echo ""
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 "| 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 ""
echo "${verdict}"
echo ""
echo "📅 **Last benchmark:** ${benchmark_time}"
echo "---"
echo ""
echo "📅 **Last benchmark:** ${benchmark_time} | 📥 [Download generated world map](${run_url}#artifacts)"
echo ""
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
echo "EOF"
@@ -112,4 +138,4 @@ jobs:
message: ${{ steps.comment_body.outputs.summary }}
comment-tag: benchmark-report
env:
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}

View File

@@ -87,7 +87,7 @@ jobs:
shell: powershell
- name: Upload artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-${{ matrix.target }}-build
path: target/release/${{ matrix.asset_name }}
@@ -97,13 +97,13 @@ jobs:
runs-on: macos-latest
steps:
- name: Download macOS Intel build
uses: actions/download-artifact@v7
uses: actions/download-artifact@v5
with:
name: macos-13-x86_64-apple-darwin-build
path: ./intel
- name: Download macOS ARM64 build
uses: actions/download-artifact@v7
uses: actions/download-artifact@v5
with:
name: macos-latest-aarch64-apple-darwin-build
path: ./arm64
@@ -114,7 +114,7 @@ jobs:
chmod +x arnis-mac-universal
- name: Upload universal binary
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: macos-universal-build
path: arnis-mac-universal
@@ -127,19 +127,19 @@ jobs:
uses: actions/checkout@v6
- name: Download Windows build artifact
uses: actions/download-artifact@v7
uses: actions/download-artifact@v5
with:
name: windows-latest-x86_64-pc-windows-msvc-build
path: ./builds/windows
- name: Download Linux build artifact
uses: actions/download-artifact@v7
uses: actions/download-artifact@v5
with:
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
path: ./builds/linux
- name: Download macOS universal build artifact
uses: actions/download-artifact@v7
uses: actions/download-artifact@v5
with:
name: macos-universal-build
path: ./builds/macos

1
.gitignore vendored
View File

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

330
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 1.0.0",
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
@@ -182,12 +182,9 @@ dependencies = [
[[package]]
name = "arnis"
version = "2.4.0"
version = "2.3.1"
dependencies = [
"base64 0.22.1",
"bedrockrs_level",
"bedrockrs_shared",
"byteorder",
"clap",
"colored",
"dirs",
@@ -201,10 +198,8 @@ dependencies = [
"indicatif",
"itertools 0.14.0",
"log",
"nbtx",
"once_cell",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rayon",
"reqwest",
"rfd",
@@ -217,9 +212,7 @@ dependencies = [
"tauri-plugin-shell",
"tempfile",
"tokio",
"vek",
"windows",
"zip",
]
[[package]]
@@ -305,7 +298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
dependencies = [
"async-lock",
"cfg-if 1.0.0",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
@@ -351,7 +344,7 @@ dependencies = [
"async-signal",
"async-task",
"blocking",
"cfg-if 1.0.0",
"cfg-if",
"event-listener",
"futures-lite",
"rustix 0.38.42",
@@ -378,7 +371,7 @@ dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if 1.0.0",
"cfg-if",
"futures-core",
"futures-io",
"rustix 0.38.42",
@@ -474,73 +467,6 @@ 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"
@@ -711,9 +637,9 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.19.0"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "byteorder"
@@ -841,12 +767,6 @@ 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"
@@ -943,16 +863,6 @@ 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"
@@ -1050,28 +960,13 @@ 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 1.0.0",
"cfg-if",
]
[[package]]
@@ -1401,7 +1296,7 @@ version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -1457,17 +1352,6 @@ 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"
@@ -1478,16 +1362,6 @@ 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"
@@ -1596,7 +1470,7 @@ version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
@@ -1951,7 +1825,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
@@ -1962,11 +1836,9 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -1975,7 +1847,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
@@ -2164,7 +2036,7 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"crunchy",
]
@@ -2678,12 +2550,6 @@ 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"
@@ -2789,7 +2655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if 1.0.0",
"cfg-if",
"combine",
"jni-sys",
"log",
@@ -2851,21 +2717,6 @@ 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"
@@ -2901,15 +2752,6 @@ 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"
@@ -2956,7 +2798,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"winapi",
]
@@ -3082,7 +2924,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"rayon",
]
@@ -3172,18 +3014,6 @@ 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"
@@ -3227,7 +3057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
@@ -3653,7 +3483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
@@ -3763,7 +3593,7 @@ version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
@@ -3782,16 +3612,6 @@ 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"
@@ -3993,7 +3813,7 @@ version = "3.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
@@ -4317,7 +4137,7 @@ dependencies = [
"av1-grain",
"bitstream-io",
"built",
"cfg-if 1.0.0",
"cfg-if",
"interpolate_name",
"itertools 0.12.1",
"libc",
@@ -4488,9 +4308,9 @@ dependencies = [
[[package]]
name = "rfd"
version = "0.16.0"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
dependencies = [
"ashpd",
"block2 0.6.1",
@@ -4507,7 +4327,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -4523,7 +4343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if 1.0.0",
"cfg-if",
"getrandom 0.2.15",
"libc",
"untrusted",
@@ -4608,7 +4428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags 2.6.0",
"errno 0.3.10",
"errno",
"libc",
"linux-raw-sys 0.4.14",
"windows-sys 0.59.0",
@@ -4621,7 +4441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.6.0",
"errno 0.3.10",
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.59.0",
@@ -4672,20 +4492,6 @@ 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"
@@ -4806,12 +4612,6 @@ 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"
@@ -4994,7 +4794,7 @@ version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"cpufeatures",
"digest",
]
@@ -5045,18 +4845,6 @@ 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"
@@ -5078,12 +4866,6 @@ 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"
@@ -6031,7 +5813,7 @@ version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"static_assertions",
]
@@ -6210,31 +5992,12 @@ 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"
@@ -6313,7 +6076,7 @@ version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
@@ -6339,7 +6102,7 @@ version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -6766,15 +6529,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -7008,7 +6762,7 @@ version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"windows-sys 0.59.0",
]
@@ -7118,12 +6872,6 @@ 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"
@@ -7281,18 +7029,6 @@ 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.4.0"
version = "2.3.1"
edition = "2021"
description = "Arnis - Generate real life cities in Minecraft"
homepage = "https://github.com/louis-e/arnis"
@@ -14,15 +14,13 @@ overflow-checks = true
[features]
default = ["gui"]
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build"]
[build-dependencies]
tauri-build = {version = "2", optional = true}
[dependencies]
base64 = "0.22.1"
byteorder = { version = "1.5", optional = true }
clap = { version = "4.5", features = ["derive", "env"] }
colored = "3.0.0"
dirs = {version = "6.0.0", optional = true }
@@ -38,10 +36,9 @@ itertools = "0.14.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.8.5"
rand_chacha = "0.3"
rayon = "1.10.0"
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
rfd = { version = "0.16.0", optional = true }
rfd = { version = "0.15.4", optional = true }
semver = "1.0.27"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@@ -49,11 +46,6 @@ 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 (1.17+) and Bedrock Edition worlds that reflect real-world geography, topography, and architecture.
Arnis creates complex and accurate Minecraft Java 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.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -59,6 +59,10 @@ pub struct Args {
#[arg(long, value_parser = parse_duration)]
pub timeout: Option<Duration>,
/// Generate a top-down map preview image after world generation (optional)
#[arg(long)]
pub generate_map: bool,
/// Spawn point coordinates (lat, lng)
#[arg(skip)]
pub spawn_point: Option<(f64, f64)>,

View File

@@ -1,849 +0,0 @@
//! 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

@@ -3,30 +3,18 @@ use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
use crate::coordinate_system::cartesian::XZBBox;
use crate::coordinate_system::geographic::LLBBox;
use crate::element_processing::*;
use crate::floodfill_cache::FloodFillCache;
use crate::ground::Ground;
use crate::map_renderer;
use crate::osm_parser::ProcessedElement;
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use crate::world_editor::{WorldEditor, WorldFormat};
use crate::world_editor::WorldEditor;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::PathBuf;
use std::sync::Arc;
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,
@@ -34,59 +22,21 @@ pub fn generate_world(
ground: Ground,
args: &Args,
) -> Result<(), String> {
// Default to Java format when called from CLI
let options = GenerationOptions {
path: args.path.clone(),
format: WorldFormat::JavaAnvil,
level_name: None,
spawn_point: None,
};
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options).map(|_| ())
}
/// Generate world with explicit format options (used by GUI for Bedrock support)
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;
// Create editor with appropriate format
let mut editor: WorldEditor = WorldEditor::new_with_format_and_name(
options.path,
&xzbbox,
llbbox,
options.format,
options.level_name.clone(),
options.spawn_point,
);
let ground = Arc::new(ground);
let mut editor: WorldEditor = WorldEditor::new(args.path.clone(), &xzbbox, llbbox);
println!("{} Processing data...", "[4/7]".bold());
// Build highway connectivity map once before processing
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
// Set ground reference in the editor to enable elevation-aware block placement
editor.set_ground(Arc::clone(&ground));
editor.set_ground(&ground);
println!("{} Processing terrain...", "[5/7]".bold());
emit_gui_progress_update(25.0, "Processing terrain...");
// Run both precomputations concurrently using rayon::join
// This overlaps highway connectivity map building with flood fill computation
let timeout_ref = args.timeout.as_ref();
let (highway_connectivity, mut flood_fill_cache) = rayon::join(
|| highways::build_highway_connectivity_map(&elements),
|| FloodFillCache::precompute(&elements, timeout_ref),
);
println!("Pre-computed {} flood fills", flood_fill_cache.way_count());
// Process data
let elements_count: usize = elements.len();
let mut elements = elements; // Take ownership for consuming
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
process_pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
@@ -97,8 +47,7 @@ pub fn generate_world_with_options(
let mut current_progress_prcs: f64 = 25.0;
let mut last_emitted_progress: f64 = current_progress_prcs;
// Process elements by draining in insertion order
for element in elements.drain(..) {
for element in &elements {
process_pb.inc(1);
current_progress_prcs += progress_increment_prcs;
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
@@ -116,28 +65,22 @@ pub fn generate_world_with_options(
process_pb.set_message("");
}
match &element {
match element {
ProcessedElement::Way(way) => {
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
buildings::generate_buildings(&mut editor, way, args, None, &flood_fill_cache);
buildings::generate_buildings(&mut editor, way, args, None);
} else if way.tags.contains_key("highway") {
highways::generate_highways(
&mut editor,
&element,
args,
&highway_connectivity,
&flood_fill_cache,
);
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
} else if way.tags.contains_key("landuse") {
landuse::generate_landuse(&mut editor, way, args, &flood_fill_cache);
landuse::generate_landuse(&mut editor, way, args);
} else if way.tags.contains_key("natural") {
natural::generate_natural(&mut editor, &element, args, &flood_fill_cache);
natural::generate_natural(&mut editor, element, args);
} else if way.tags.contains_key("amenity") {
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
amenities::generate_amenities(&mut editor, element, args);
} else if way.tags.contains_key("leisure") {
leisure::generate_leisure(&mut editor, way, args, &flood_fill_cache);
leisure::generate_leisure(&mut editor, way, args);
} else if way.tags.contains_key("barrier") {
barriers::generate_barriers(&mut editor, &element);
barriers::generate_barriers(&mut editor, element);
} else if let Some(val) = way.tags.get("waterway") {
if val == "dock" {
// docks count as water areas
@@ -157,10 +100,8 @@ pub fn generate_world_with_options(
} else if way.tags.get("service") == Some(&"siding".to_string()) {
highways::generate_siding(&mut editor, way);
} else if way.tags.contains_key("man_made") {
man_made::generate_man_made(&mut editor, &element, args);
man_made::generate_man_made(&mut editor, element, args);
}
// Release flood fill cache entry for this way
flood_fill_cache.remove_way(way.id);
}
ProcessedElement::Node(node) => {
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
@@ -168,19 +109,13 @@ pub fn generate_world_with_options(
} else if node.tags.contains_key("natural")
&& node.tags.get("natural") == Some(&"tree".to_string())
{
natural::generate_natural(&mut editor, &element, args, &flood_fill_cache);
natural::generate_natural(&mut editor, element, args);
} else if node.tags.contains_key("amenity") {
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
amenities::generate_amenities(&mut editor, element, args);
} else if node.tags.contains_key("barrier") {
barriers::generate_barrier_nodes(&mut editor, node);
} else if node.tags.contains_key("highway") {
highways::generate_highways(
&mut editor,
&element,
args,
&highway_connectivity,
&flood_fill_cache,
);
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
} else if node.tags.contains_key("tourism") {
tourisms::generate_tourisms(&mut editor, node);
} else if node.tags.contains_key("man_made") {
@@ -189,12 +124,7 @@ pub fn generate_world_with_options(
}
ProcessedElement::Relation(rel) => {
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
buildings::generate_building_from_relation(
&mut editor,
rel,
args,
&flood_fill_cache,
);
buildings::generate_building_from_relation(&mut editor, rel, args);
} else if rel.tags.contains_key("water")
|| rel
.tags
@@ -204,43 +134,24 @@ pub fn generate_world_with_options(
{
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
} else if rel.tags.contains_key("natural") {
natural::generate_natural_from_relation(
&mut editor,
rel,
args,
&flood_fill_cache,
);
natural::generate_natural_from_relation(&mut editor, rel, args);
} else if rel.tags.contains_key("landuse") {
landuse::generate_landuse_from_relation(
&mut editor,
rel,
args,
&flood_fill_cache,
);
landuse::generate_landuse_from_relation(&mut editor, rel, args);
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
leisure::generate_leisure_from_relation(
&mut editor,
rel,
args,
&flood_fill_cache,
);
leisure::generate_leisure_from_relation(&mut editor, rel, args);
} else if rel.tags.contains_key("man_made") {
man_made::generate_man_made(&mut editor, &element, args);
man_made::generate_man_made(
&mut editor,
&ProcessedElement::Relation(rel.clone()),
args,
);
}
// Release flood fill cache entries for all ways in this relation
let way_ids: Vec<u64> = rel.members.iter().map(|m| m.way.id).collect();
flood_fill_cache.remove_relation_ways(&way_ids);
}
}
// Element is dropped here, freeing its memory immediately
}
process_pb.finish();
// Drop remaining caches
drop(highway_connectivity);
drop(flood_fill_cache);
// Generate ground layer
let total_blocks: u64 = xzbbox.bounding_rect().total_blocks();
let desired_updates: u64 = 1500;
@@ -266,61 +177,44 @@ pub fn generate_world_with_options(
let groundlayer_block = GRASS_BLOCK;
// Process ground generation chunk-by-chunk for better cache locality.
// This keeps the same region/chunk HashMap entries hot in CPU cache,
// rather than jumping between regions on every Z iteration.
let min_chunk_x = xzbbox.min_x() >> 4;
let max_chunk_x = xzbbox.max_x() >> 4;
let min_chunk_z = xzbbox.min_z() >> 4;
let max_chunk_z = xzbbox.max_z() >> 4;
for x in xzbbox.min_x()..=xzbbox.max_x() {
for z in xzbbox.min_z()..=xzbbox.max_z() {
// Add default dirt and grass layer if there isn't a stone layer already
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
editor.set_block(groundlayer_block, x, 0, z, None, None);
editor.set_block(DIRT, x, -1, z, None, None);
editor.set_block(DIRT, x, -2, z, None, None);
}
for chunk_x in min_chunk_x..=max_chunk_x {
for chunk_z in min_chunk_z..=max_chunk_z {
// Calculate the block range for this chunk, clamped to bbox
let chunk_min_x = (chunk_x << 4).max(xzbbox.min_x());
let chunk_max_x = ((chunk_x << 4) + 15).min(xzbbox.max_x());
let chunk_min_z = (chunk_z << 4).max(xzbbox.min_z());
let chunk_max_z = ((chunk_z << 4) + 15).min(xzbbox.max_z());
// Fill underground with stone
if args.fillground {
// Fill from bedrock+1 to 3 blocks below ground with stone
editor.fill_blocks_absolute(
STONE,
x,
MIN_Y + 1,
z,
x,
editor.get_absolute_y(x, -3, z),
z,
None,
None,
);
}
// Generate a bedrock level at MIN_Y
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
for x in chunk_min_x..=chunk_max_x {
for z in chunk_min_z..=chunk_max_z {
// Add default dirt and grass layer if there isn't a stone layer already
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
editor.set_block(groundlayer_block, x, 0, z, None, None);
editor.set_block(DIRT, x, -1, z, None, None);
editor.set_block(DIRT, x, -2, z, None, None);
}
block_counter += 1;
// Use manual % check since is_multiple_of() is unstable on stable Rust
#[allow(clippy::manual_is_multiple_of)]
if block_counter % batch_size == 0 {
ground_pb.inc(batch_size);
}
// Fill underground with stone
if args.fillground {
// Fill from bedrock+1 to 3 blocks below ground with stone
editor.fill_blocks_absolute(
STONE,
x,
MIN_Y + 1,
z,
x,
editor.get_absolute_y(x, -3, z),
z,
None,
None,
);
}
// Generate a bedrock level at MIN_Y
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
block_counter += 1;
#[allow(clippy::manual_is_multiple_of)]
if block_counter % batch_size == 0 {
ground_pb.inc(batch_size);
}
gui_progress_grnd += progress_increment_grnd;
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
emit_gui_progress_update(gui_progress_grnd, "");
last_emitted_progress = gui_progress_grnd;
}
}
gui_progress_grnd += progress_increment_grnd;
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
emit_gui_progress_update(gui_progress_grnd, "");
last_emitted_progress = gui_progress_grnd;
}
}
}
@@ -343,109 +237,83 @@ pub fn generate_world_with_options(
// 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 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 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.as_ref(),
) {
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
eprintln!("Warning: {}", warning_msg);
#[cfg(feature = "gui")]
send_log(LogLevel::Warning, &warning_msg);
}
if let Err(e) = update_player_spawn_y_after_generation(
&args.path,
Some(*spawn_coords),
bbox_string,
args.scale,
&ground,
) {
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);
}
}
// 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);
emit_gui_progress_update(100.0, "Done! World generation completed.");
println!("{}", "Done! World generation completed.".green().bold());
// Generate top-down map preview:
// - Always for GUI mode (non-blocking, runs in background)
// - Only when --generate-map flag is set for CLI mode (blocking, waits for completion)
#[cfg(feature = "gui")]
let should_generate_map = true;
#[cfg(not(feature = "gui"))]
let should_generate_map = args.generate_map;
if should_generate_map {
let world_path = args.path.clone();
let bounds = (
xzbbox.min_x(),
xzbbox.max_x(),
xzbbox.min_z(),
xzbbox.max_z(),
);
let map_thread = 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)
}));
match result {
Ok(Ok(_path)) => {
// Notify the GUI that the map preview is ready
emit_map_preview_ready();
}
Ok(Err(e)) => {
eprintln!("Warning: Failed to generate map preview: {}", e);
}
Err(_) => {
eprintln!("Warning: Map preview generation panicked unexpectedly");
}
}
});
// In CLI mode, wait for map generation to complete before exiting
// In GUI mode, let it run in background to keep UI responsive
#[cfg(not(feature = "gui"))]
{
let _ = map_thread.join();
}
// In GUI mode, we don't join, let the thread run in background
#[cfg(feature = "gui")]
drop(map_thread);
}
Ok(output_path)
}
/// Information needed to generate a map preview after world generation is complete
#[derive(Clone)]
pub struct MapPreviewInfo {
pub world_path: PathBuf,
pub min_x: i32,
pub max_x: i32,
pub min_z: i32,
pub max_z: i32,
pub world_area: i64,
}
impl MapPreviewInfo {
/// Create MapPreviewInfo from world bounds
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
Self {
world_path,
min_x: xzbbox.min_x(),
max_x: xzbbox.max_x(),
min_z: xzbbox.min_z(),
max_z: xzbbox.max_z(),
world_area: world_width * world_height,
}
}
}
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
/// Start map preview generation in a background thread.
/// This should be called AFTER the world generation is complete, the session lock is released,
/// and the GUI has been notified of 100% completion.
///
/// For Java worlds only, and only if the world area is within limits.
pub fn start_map_preview_generation(info: MapPreviewInfo) {
if info.world_area > MAX_MAP_PREVIEW_AREA {
return;
}
std::thread::spawn(move || {
// Use catch_unwind to prevent any panic from affecting the application
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
map_renderer::render_world_map(
&info.world_path,
info.min_x,
info.max_x,
info.min_z,
info.max_z,
)
}));
match result {
Ok(Ok(_path)) => {
// Notify the GUI that the map preview is ready
emit_map_preview_ready();
}
Ok(Err(e)) => {
eprintln!("Warning: Failed to generate map preview: {}", e);
}
Err(_) => {
eprintln!("Warning: Map preview generation panicked unexpectedly");
}
}
});
Ok(())
}

View File

@@ -1,127 +0,0 @@
//! Deterministic random number generation for consistent element processing.
//!
//! This module provides seeded RNG that ensures the same element always produces
//! the same random values, regardless of processing order. This is essential for
//! region-by-region streaming where the same element may be processed multiple times
//! (once for each region it touches).
//!
//! # Example
//! ```ignore
//! let mut rng = element_rng(element_id);
//! let color = rng.gen_bool(0.5); // Always same result for same element_id
//! ```
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
/// Creates a deterministic RNG seeded from an element ID.
///
/// The same element ID will always produce the same sequence of random values,
/// ensuring consistent results when an element is processed multiple times
/// (e.g., once per region it touches during streaming).
///
/// # Arguments
/// * `element_id` - The unique OSM element ID (way ID, node ID, or relation ID)
///
/// # Returns
/// A seeded ChaCha8Rng that will produce deterministic random values
#[inline]
pub fn element_rng(element_id: u64) -> ChaCha8Rng {
ChaCha8Rng::seed_from_u64(element_id)
}
/// Creates a deterministic RNG seeded from an element ID with an additional salt.
///
/// Use this when you need multiple independent random sequences for the same element.
/// For example, one sequence for wall colors and another for roof style.
///
/// # Arguments
/// * `element_id` - The unique OSM element ID
/// * `salt` - Additional value to create a different sequence (e.g., use different
/// salt values for different purposes within the same element)
#[inline]
#[allow(dead_code)]
pub fn element_rng_salted(element_id: u64, salt: u64) -> ChaCha8Rng {
// Combine element_id and salt using XOR and bit rotation to avoid collisions
let combined = element_id ^ salt.rotate_left(32);
ChaCha8Rng::seed_from_u64(combined)
}
/// Creates a deterministic RNG seeded from coordinates.
///
/// Use this for per-block randomness that needs to be consistent regardless
/// of processing order (e.g., random flower placement within a natural area).
///
/// # Arguments
/// * `x` - X coordinate
/// * `z` - Z coordinate
/// * `element_id` - The element ID for additional uniqueness
#[inline]
pub fn coord_rng(x: i32, z: i32, element_id: u64) -> ChaCha8Rng {
// Combine coordinates and element_id into a seed.
// Cast through u32 to handle negative coordinates consistently.
let coord_part = ((x as u32 as i64) << 32) | (z as u32 as i64);
let seed = (coord_part as u64) ^ element_id;
ChaCha8Rng::seed_from_u64(seed)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::Rng;
#[test]
fn test_element_rng_deterministic() {
let mut rng1 = element_rng(12345);
let mut rng2 = element_rng(12345);
// Same seed should produce same sequence
for _ in 0..100 {
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
}
}
#[test]
fn test_different_elements_different_values() {
let mut rng1 = element_rng(12345);
let mut rng2 = element_rng(12346);
// Different seeds should (almost certainly) produce different values
let v1: u64 = rng1.gen();
let v2: u64 = rng2.gen();
assert_ne!(v1, v2);
}
#[test]
fn test_salted_rng_different_from_base() {
let mut rng1 = element_rng(12345);
let mut rng2 = element_rng_salted(12345, 1);
let v1: u64 = rng1.gen();
let v2: u64 = rng2.gen();
assert_ne!(v1, v2);
}
#[test]
fn test_coord_rng_deterministic() {
let mut rng1 = coord_rng(100, 200, 12345);
let mut rng2 = coord_rng(100, 200, 12345);
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
}
#[test]
fn test_coord_rng_negative_coordinates() {
// Negative coordinates are common in Minecraft worlds
let mut rng1 = coord_rng(-100, -200, 12345);
let mut rng2 = coord_rng(-100, -200, 12345);
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
// Ensure different negative coords produce different seeds
let mut rng3 = coord_rng(-100, -200, 12345);
let mut rng4 = coord_rng(-101, -200, 12345);
assert_ne!(rng3.gen::<u64>(), rng4.gen::<u64>());
}
}

View File

@@ -2,19 +2,11 @@ use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::coordinate_system::cartesian::XZPoint;
use crate::deterministic_rng::element_rng;
use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::ProcessedElement;
use crate::world_editor::WorldEditor;
use rand::Rng;
pub fn generate_amenities(
editor: &mut WorldEditor,
element: &ProcessedElement,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags().get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
@@ -50,14 +42,18 @@ pub fn generate_amenities(
let ground_block: Block = OAK_PLANKS;
let roof_block: Block = STONE_BLOCK_SLAB;
// Use pre-computed flood fill from cache
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
let polygon_coords: Vec<(i32, i32)> = element
.nodes()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
if floor_area.is_empty() {
if polygon_coords.is_empty() {
return;
}
let floor_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Fill the floor area
for (x, z) in floor_area.iter() {
editor.set_block(ground_block, *x, 0, *z, None, None);
@@ -84,10 +80,8 @@ pub fn generate_amenities(
"bench" => {
// Place a bench
if let Some(pt) = first_node {
// Use deterministic RNG for consistent bench orientation across region boundaries
let mut rng = element_rng(element.id());
// 50% chance to 90 degrees rotate the bench
if rng.gen_bool(0.5) {
// 50% chance to 90 degrees rotate the bench using if
if rand::random::<bool>() {
editor.set_block(SMOOTH_STONE, pt.x, 1, pt.z, None, None);
editor.set_block(OAK_LOG, pt.x + 1, 1, pt.z, None, None);
editor.set_block(OAK_LOG, pt.x - 1, 1, pt.z, None, None);
@@ -101,9 +95,12 @@ pub fn generate_amenities(
"shelter" => {
let roof_block: Block = STONE_BRICK_SLAB;
// Use pre-computed flood fill from cache
let polygon_coords: Vec<(i32, i32)> = element
.nodes()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let roof_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Place fences and roof slabs at each corner node directly
for node in element.nodes() {

View File

@@ -3,9 +3,8 @@ use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::colors::color_text_to_rgb_tuple;
use crate::coordinate_system::cartesian::XZPoint;
use crate::deterministic_rng::element_rng;
use crate::element_processing::subprocessor::buildings_interior::generate_building_interior;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
@@ -29,7 +28,6 @@ pub fn generate_buildings(
element: &ProcessedWay,
args: &Args,
relation_levels: Option<i32>,
flood_fill_cache: &FloodFillCache,
) {
// Get min_level first so we can use it both for start_level and building height calculations
let min_level = if let Some(min_level_str) = element.tags.get("building:min_level") {
@@ -45,9 +43,10 @@ pub fn generate_buildings(
let scale_factor = args.scale;
let min_level_offset = multiply_scale(min_level * 4, scale_factor);
// Use pre-computed flood fill from cache
// Cache floodfill result: compute once and reuse throughout
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
let cached_floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
flood_fill_area(&polygon_coords, args.timeout.as_ref());
let cached_footprint_size = cached_floor_area.len();
// Use fixed starting Y coordinate based on maximum ground level when terrain is enabled
@@ -122,8 +121,7 @@ pub fn generate_buildings(
let mut processed_points: HashSet<(i32, i32)> = HashSet::new();
let mut building_height: i32 = ((6.0 * scale_factor) as i32).max(3); // Default building height with scale and minimum
let mut is_tall_building = false;
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(element.id);
let mut rng = rand::thread_rng();
let use_vertical_windows = rng.gen_bool(0.7);
let use_accent_roof_line = rng.gen_bool(0.25);
@@ -388,7 +386,7 @@ pub fn generate_buildings(
building_height = ((23.0 * scale_factor) as i32).max(3);
}
} else if building_type == "bridge" {
generate_bridge(editor, element, flood_fill_cache, args.timeout.as_ref());
generate_bridge(editor, element, args.timeout.as_ref());
return;
}
}
@@ -1486,7 +1484,6 @@ pub fn generate_building_from_relation(
editor: &mut WorldEditor,
relation: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
// Extract levels from relation tags
let relation_levels = relation
@@ -1498,13 +1495,7 @@ pub fn generate_building_from_relation(
// Process the outer way to create the building walls
for member in &relation.members {
if member.role == ProcessedMemberRole::Outer {
generate_buildings(
editor,
&member.way,
args,
Some(relation_levels),
flood_fill_cache,
);
generate_buildings(editor, &member.way, args, Some(relation_levels));
}
}
@@ -1528,29 +1519,28 @@ pub fn generate_building_from_relation(
fn generate_bridge(
editor: &mut WorldEditor,
element: &ProcessedWay,
flood_fill_cache: &FloodFillCache,
floodfill_timeout: Option<&Duration>,
) {
let floor_block: Block = STONE;
let railing_block: Block = STONE_BRICKS;
// Calculate bridge level based on the "level" tag (computed once, used throughout)
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
if let Ok(level) = level_str.parse::<i32>() {
(level * 3) + 1
} else {
1 // Default elevation
}
} else {
1 // Default elevation
};
// Process the nodes to create bridge pathways and railings
let mut previous_node: Option<(i32, i32)> = None;
for node in &element.nodes {
let x: i32 = node.x;
let z: i32 = node.z;
// Calculate bridge level based on the "level" tag
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
if let Ok(level) = level_str.parse::<i32>() {
(level * 3) + 1
} else {
1 // Default elevation
}
} else {
1 // Default elevation
};
// Create bridge path using Bresenham's line
if let Some(prev) = previous_node {
let bridge_points: Vec<(i32, i32, i32)> =
@@ -1566,8 +1556,21 @@ fn generate_bridge(
previous_node = Some((x, z));
}
// Flood fill the area between the bridge path nodes (uses cache)
let bridge_area: Vec<(i32, i32)> = flood_fill_cache.get_or_compute(element, floodfill_timeout);
// Flood fill the area between the bridge path nodes
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
let bridge_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
// Calculate bridge level based on the "level" tag
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
if let Ok(level) = level_str.parse::<i32>() {
(level * 3) + 1
} else {
1 // Default elevation
}
} else {
1 // Default elevation
};
// Place floor blocks
for (x, z) in bridge_area {

View File

@@ -2,10 +2,9 @@ use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::coordinate_system::cartesian::XZPoint;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedElement, ProcessedWay};
use crate::world_editor::WorldEditor;
use rayon::prelude::*;
use std::collections::HashMap;
/// Type alias for highway connectivity map
@@ -17,53 +16,44 @@ pub fn generate_highways(
element: &ProcessedElement,
args: &Args,
highway_connectivity: &HighwayConnectivityMap,
flood_fill_cache: &FloodFillCache,
) {
generate_highways_internal(
editor,
element,
args,
highway_connectivity,
flood_fill_cache,
);
generate_highways_internal(editor, element, args, highway_connectivity);
}
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
/// Uses parallel processing for better performance on large element sets.
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
// Parallel map phase: extract connectivity data from each highway element
let partial_maps: Vec<Vec<((i32, i32), i32)>> = elements
.par_iter()
.filter_map(|element| {
if let ProcessedElement::Way(way) = element {
if way.tags.contains_key("highway") && !way.nodes.is_empty() {
let layer_value = way
.tags
.get("layer")
.and_then(|layer| layer.parse::<i32>().ok())
.unwrap_or(0);
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
// Treat negative layers as ground level (0) for connectivity
let layer_value = if layer_value < 0 { 0 } else { layer_value };
for element in elements {
if let ProcessedElement::Way(way) = element {
if way.tags.contains_key("highway") {
let layer_value = way
.tags
.get("layer")
.and_then(|layer| layer.parse::<i32>().ok())
.unwrap_or(0);
// Treat negative layers as ground level (0) for connectivity
let layer_value = if layer_value < 0 { 0 } else { layer_value };
// Add connectivity for start and end nodes
if !way.nodes.is_empty() {
let start_node = &way.nodes[0];
let end_node = &way.nodes[way.nodes.len() - 1];
let start_coord = (start_node.x, start_node.z);
let end_coord = (end_node.x, end_node.z);
return Some(vec![(start_coord, layer_value), (end_coord, layer_value)]);
connectivity_map
.entry(start_coord)
.or_default()
.push(layer_value);
connectivity_map
.entry(end_coord)
.or_default()
.push(layer_value);
}
}
None
})
.collect();
// Sequential reduce phase: merge all partial results into final map
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
for entries in partial_maps {
for (coord, layer) in entries {
connectivity_map.entry(coord).or_default().push(layer);
}
}
@@ -76,7 +66,6 @@ fn generate_highways_internal(
element: &ProcessedElement,
args: &Args,
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>, // Maps node coordinates to list of layers that connect to this node
flood_fill_cache: &FloodFillCache,
) {
if let Some(highway_type) = element.tags().get("highway") {
if highway_type == "street_lamp" {
@@ -148,9 +137,14 @@ fn generate_highways_internal(
};
}
// Fill the area using flood fill cache
// Fill the area using flood fill or by iterating through the nodes
let polygon_coords: Vec<(i32, i32)> = way
.nodes
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in filled_area {
editor.set_block(surface_block, x, 0, z, None, None);

View File

@@ -1,18 +1,12 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
pub fn generate_landuse(
editor: &mut WorldEditor,
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
// Determine block type based on landuse tag
let binding: String = "".to_string();
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
@@ -50,12 +44,11 @@ pub fn generate_landuse(
_ => GRASS_BLOCK,
};
// Get the area of the landuse element using cache
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
// Get the area of the landuse element
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(element.id);
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
for (x, z) in floor_area {
if landuse_tag == "traffic_island" {
@@ -282,13 +275,12 @@ pub fn generate_landuse_from_relation(
editor: &mut WorldEditor,
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
if rel.tags.contains_key("landuse") {
// Generate individual ways with their original tags
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
generate_landuse(editor, &member.way.clone(), args, flood_fill_cache);
generate_landuse(editor, &member.way.clone(), args);
}
}
@@ -310,7 +302,7 @@ pub fn generate_landuse_from_relation(
};
// Generate landuse area from combined way
generate_landuse(editor, &combined_way, args, flood_fill_cache);
generate_landuse(editor, &combined_way, args);
}
}
}

View File

@@ -1,19 +1,13 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
pub fn generate_leisure(
editor: &mut WorldEditor,
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
if let Some(leisure_type) = element.tags.get("leisure") {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
@@ -80,13 +74,15 @@ pub fn generate_leisure(
previous_node = Some((node.x, node.z));
}
// Flood-fill the interior of the leisure area using cache
// Flood-fill the interior of the leisure area
if corner_addup != (0, 0, 0) {
let polygon_coords: Vec<(i32, i32)> = element
.nodes
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(element.id);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in filled_area {
editor.set_block(block_type, x, 0, z, Some(&[GRASS_BLOCK]), None);
@@ -95,6 +91,7 @@ pub fn generate_leisure(
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
&& editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK]))
{
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let random_choice: i32 = rng.gen_range(0..1000);
match random_choice {
@@ -126,6 +123,7 @@ pub fn generate_leisure(
// Add playground or recreation ground features
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let random_choice: i32 = rng.gen_range(0..5000);
match random_choice {
@@ -178,13 +176,12 @@ pub fn generate_leisure_from_relation(
editor: &mut WorldEditor,
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
if rel.tags.get("leisure") == Some(&"park".to_string()) {
// First generate individual ways with their original tags
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
generate_leisure(editor, &member.way, args, flood_fill_cache);
generate_leisure(editor, &member.way, args);
}
}
@@ -204,6 +201,6 @@ pub fn generate_leisure_from_relation(
};
// Generate leisure area from combined way
generate_leisure(editor, &combined_way, args, flood_fill_cache);
generate_leisure(editor, &combined_way, args);
}
}

View File

@@ -1,19 +1,13 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
pub fn generate_natural(
editor: &mut WorldEditor,
element: &ProcessedElement,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
if let Some(natural_type) = element.tags().get("natural") {
if natural_type == "tree" {
if let ProcessedElement::Node(node) = element {
@@ -75,13 +69,17 @@ pub fn generate_natural(
previous_node = Some((x, z));
}
// If there are natural nodes, flood-fill the area using cache
// If there are natural nodes, flood-fill the area
if corner_addup != (0, 0, 0) {
let polygon_coords: Vec<(i32, i32)> = way
.nodes
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(way.id);
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
for (x, z) in filled_area {
editor.set_block(block_type, x, 0, z, None, None);
@@ -450,18 +448,12 @@ pub fn generate_natural_from_relation(
editor: &mut WorldEditor,
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
if rel.tags.contains_key("natural") {
// Generate individual ways with their original tags
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
generate_natural(
editor,
&ProcessedElement::Way((*member.way).clone()),
args,
flood_fill_cache,
);
generate_natural(editor, &ProcessedElement::Way(member.way.clone()), args);
}
}
@@ -483,12 +475,7 @@ pub fn generate_natural_from_relation(
};
// Generate natural area from combined way
generate_natural(
editor,
&ProcessedElement::Way(combined_way),
args,
flood_fill_cache,
);
generate_natural(editor, &ProcessedElement::Way(combined_way), args);
}
}
}

View File

@@ -1,5 +1,4 @@
use crate::block_definitions::*;
use crate::deterministic_rng::coord_rng;
use crate::world_editor::WorldEditor;
use rand::Rng;
@@ -116,9 +115,7 @@ impl Tree<'_> {
blacklist.extend(Self::get_functional_blocks());
blacklist.push(WATER);
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
// The element_id of 0 is used as a salt for tree-specific randomness
let mut rng = coord_rng(x, z, 0);
let mut rng = rand::thread_rng();
let tree = Self::get_tree(match rng.gen_range(1..=3) {
1 => TreeType::Oak,

View File

@@ -1,5 +1,6 @@
use geo::orient::{Direction, Orient};
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
use std::time::Instant;
use crate::clipping::clip_water_ring_to_bbox;
use crate::{
@@ -14,13 +15,15 @@ pub fn generate_water_area_from_way(
element: &ProcessedWay,
_xzbbox: &XZBBox,
) {
let start_time = Instant::now();
let outers = [element.nodes.clone()];
if !verify_closed_rings(&outers) {
println!("Skipping way {} due to invalid polygon", element.id);
return;
}
generate_water_areas(editor, &outers, &[]);
generate_water_areas(editor, &outers, &[], start_time);
}
pub fn generate_water_areas_from_relation(
@@ -28,6 +31,8 @@ pub fn generate_water_areas_from_relation(
element: &ProcessedRelation,
xzbbox: &XZBBox,
) {
let start_time = Instant::now();
// Check if this is a water relation (either with water tag or natural=water)
let is_water = element.tags.contains_key("water")
|| element
@@ -118,13 +123,14 @@ pub fn generate_water_areas_from_relation(
return;
}
generate_water_areas(editor, &outers, &inners);
generate_water_areas(editor, &outers, &inners, start_time);
}
fn generate_water_areas(
editor: &mut WorldEditor,
outers: &[Vec<ProcessedNode>],
inners: &[Vec<ProcessedNode>],
start_time: Instant,
) {
// Calculate polygon bounding box to limit fill area
let mut poly_min_x = i32::MAX;
@@ -163,7 +169,9 @@ fn generate_water_areas(
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
.collect();
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
inverse_floodfill(
min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor, start_time,
);
}
/// Merges way segments that share endpoints into closed rings.
@@ -300,6 +308,7 @@ fn inverse_floodfill(
outers: Vec<Vec<XZPoint>>,
inners: Vec<Vec<XZPoint>>,
editor: &mut WorldEditor,
start_time: Instant,
) {
// Convert to geo Polygons with normalized winding order
let inners: Vec<_> = inners
@@ -332,7 +341,14 @@ fn inverse_floodfill(
})
.collect();
inverse_floodfill_recursive((min_x, min_z), (max_x, max_z), &outers, &inners, editor);
inverse_floodfill_recursive(
(min_x, min_z),
(max_x, max_z),
&outers,
&inners,
editor,
start_time,
);
}
fn inverse_floodfill_recursive(
@@ -341,11 +357,12 @@ fn inverse_floodfill_recursive(
outers: &[Polygon],
inners: &[Polygon],
editor: &mut WorldEditor,
start_time: Instant,
) {
// Check if we've exceeded 40 seconds
// if start_time.elapsed().as_secs() > 40 {
// println!("Water area generation exceeded 40 seconds, continuing anyway");
// }
// Check if we've exceeded 25 seconds
if start_time.elapsed().as_secs() > 25 {
println!("Water area generation exceeded 25 seconds, continuing anyway");
}
const ITERATIVE_THRES: i64 = 10_000;
@@ -400,6 +417,7 @@ fn inverse_floodfill_recursive(
&outers_intersects,
&inners_intersects,
editor,
start_time,
);
}
}

View File

@@ -2,8 +2,7 @@ use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance}
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use image::Rgb;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::path::Path;
/// Maximum Y coordinate in Minecraft (build height limit)
const MAX_Y: i32 = 319;
@@ -18,8 +17,6 @@ const TERRARIUM_OFFSET: f64 = 32768.0;
const MIN_ZOOM: u8 = 10;
/// Maximum zoom level for terrain tiles
const MAX_ZOOM: u8 = 15;
/// Maximum concurrent tile downloads to be respectful to AWS
const MAX_CONCURRENT_DOWNLOADS: usize = 8;
/// Holds processed elevation data and metadata
#[derive(Clone)]
@@ -32,11 +29,6 @@ pub struct ElevationData {
pub(crate) height: usize,
}
/// RGB image buffer type for elevation tiles
type TileImage = image::ImageBuffer<Rgb<u8>, Vec<u8>>;
/// Result type for tile download operations: ((tile_x, tile_y), image) or error
type TileDownloadResult = Result<((u32, u32), TileImage), String>;
/// Calculates appropriate zoom level for the given bounding box
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
@@ -61,103 +53,21 @@ fn download_tile(
tile_y: u32,
zoom: u8,
tile_path: &Path,
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, Box<dyn std::error::Error>> {
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
let url: String = AWS_TERRARIUM_URL
.replace("{z}", &zoom.to_string())
.replace("{x}", &tile_x.to_string())
.replace("{y}", &tile_y.to_string());
let response = client.get(&url).send().map_err(|e| e.to_string())?;
response.error_for_status_ref().map_err(|e| e.to_string())?;
let bytes = response.bytes().map_err(|e| e.to_string())?;
std::fs::write(tile_path, &bytes).map_err(|e| e.to_string())?;
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
let response: reqwest::blocking::Response = client.get(&url).send()?;
response.error_for_status_ref()?;
let bytes = response.bytes()?;
std::fs::write(tile_path, &bytes)?;
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
Ok(img.to_rgb8())
}
/// Fetches a tile from cache or downloads it if not available
/// Note: In parallel execution, multiple threads may attempt to download the same tile
/// if it's missing or corrupted. This is harmless (just wastes some bandwidth) as
/// file writes are atomic at the OS level.
fn fetch_or_load_tile(
client: &reqwest::blocking::Client,
tile_x: u32,
tile_y: u32,
zoom: u8,
tile_path: &Path,
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
if tile_path.exists() {
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
let file_size = std::fs::metadata(tile_path).map(|m| m.len()).unwrap_or(0);
if file_size < 1000 {
eprintln!(
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
tile_path.display(),
file_size
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Cached tile appears too small, refetching.",
);
// Remove the potentially corrupted file
if let Err(e) = std::fs::remove_file(tile_path) {
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to remove corrupted tile file during refetching.",
);
}
// Re-download the tile
return download_tile(client, tile_x, tile_y, zoom, tile_path);
}
// Try to load cached tile, but handle corruption gracefully
match image::open(tile_path) {
Ok(img) => {
println!(
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
tile_path.display()
);
Ok(img.to_rgb8())
}
Err(e) => {
eprintln!(
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
tile_path.display(),
e
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Cached tile is corrupted or invalid. Re-downloading...",
);
// Remove the corrupted file
if let Err(e) = std::fs::remove_file(tile_path) {
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to remove corrupted tile file during re-download.",
);
}
// Re-download the tile
download_tile(client, tile_x, tile_y, zoom, tile_path)
}
}
} else {
// Download the tile for the first time
download_tile(client, tile_x, tile_y, zoom, tile_path)
}
}
pub fn fetch_elevation_data(
bbox: &LLBBox,
scale: f64,
@@ -181,68 +91,106 @@ pub fn fetch_elevation_data(
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
let mut extreme_values_found = Vec::new(); // Track extreme values for debugging
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let tile_cache_dir = Path::new("./arnis-tile-cache");
if !tile_cache_dir.exists() {
std::fs::create_dir_all(&tile_cache_dir)?;
std::fs::create_dir_all(tile_cache_dir)?;
}
// Create a shared HTTP client for connection pooling
let client = reqwest::blocking::Client::new();
// Fetch and process each tile
for (tile_x, tile_y) in &tiles {
// Check if tile is already cached
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
// Download tiles in parallel with limited concurrency to be respectful to AWS
let num_tiles = tiles.len();
println!(
"Downloading {num_tiles} elevation tiles (up to {MAX_CONCURRENT_DOWNLOADS} concurrent)..."
);
let rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
let file_size = match std::fs::metadata(&tile_path) {
Ok(metadata) => metadata.len(),
Err(_) => 0,
};
// Use a custom thread pool to limit concurrent downloads
let thread_pool = rayon::ThreadPoolBuilder::new()
.num_threads(MAX_CONCURRENT_DOWNLOADS)
.build()
.map_err(|e| format!("Failed to create thread pool: {e}"))?;
let downloaded_tiles: Vec<TileDownloadResult> = thread_pool.install(|| {
tiles
.par_iter()
.map(|(tile_x, tile_y)| {
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
let rgb_img = fetch_or_load_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?;
Ok(((*tile_x, *tile_y), rgb_img))
})
.collect()
});
// Check for any download errors
let mut successful_tiles = Vec::new();
for result in downloaded_tiles {
match result {
Ok(tile_data) => successful_tiles.push(tile_data),
Err(e) => {
eprintln!("Warning: Failed to download tile: {e}");
if file_size < 1000 {
eprintln!(
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
tile_path.display(),
file_size
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
&format!("Failed to download elevation tile: {e}"),
"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) {
eprintln!(
"Warning: Failed to remove corrupted tile file: {}",
remove_err
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to remove corrupted tile file during refetching.",
);
}
// Re-download the tile
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
} else {
println!(
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
tile_path.display()
);
// Try to load cached tile, but handle corruption gracefully
match image::open(&tile_path) {
Ok(img) => img.to_rgb8(),
Err(e) => {
eprintln!(
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
tile_path.display(),
e
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Cached tile is corrupted or invalid. Re-downloading...",
);
// Remove the corrupted file
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
eprintln!(
"Warning: Failed to remove corrupted tile file: {}",
remove_err
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to remove corrupted tile file during re-download.",
);
}
// Re-download the tile
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
}
}
}
}
}
} else {
// Download the tile for the first time
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
};
println!("Processing {} elevation tiles...", successful_tiles.len());
// Process tiles sequentially (writes to shared height_grid)
for ((tile_x, tile_y), rgb_img) in successful_tiles {
// Only process pixels that fall within the requested bbox
for (y, row) in rgb_img.rows().enumerate() {
for (x, pixel) in row.enumerate() {
// Convert tile pixel coordinates back to geographic coordinates
let pixel_lng = ((tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
let pixel_lng = ((*tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
* 360.0
- 180.0;
let pixel_lat_rad = std::f64::consts::PI
* (1.0
- 2.0 * (tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
- 2.0 * (*tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
let pixel_lat = pixel_lat_rad.sinh().atan().to_degrees();
// Skip pixels outside the requested bounding box
@@ -303,26 +251,25 @@ pub fn fetch_elevation_data(
filter_elevation_outliers(&mut height_grid);
// Calculate blur sigma based on grid resolution
// 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;
// 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
let grid_size: f64 = (grid_width.min(grid_height) as f64).max(1.0);
// 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
);
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)
};
/* eprintln!(
"Grid: {}x{}, Blur sigma: {:.2}",
@@ -332,9 +279,6 @@ pub fn fetch_elevation_data(
// Continue with the existing blur and conversion to Minecraft heights...
let blurred_heights: Vec<Vec<f64>> = apply_gaussian_blur(&height_grid, sigma);
// Release raw height grid
drop(height_grid);
let mut mc_heights: Vec<Vec<i32>> = Vec::with_capacity(blurred_heights.len());
// Find min/max in raw data

View File

@@ -1,189 +0,0 @@
//! Pre-computed flood fill cache for parallel polygon filling.
//!
//! This module provides a way to pre-compute all flood fill operations in parallel
//! before the main element processing loop, then retrieve cached results during
//! sequential processing.
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedElement, ProcessedWay};
use fnv::FnvHashMap;
use rayon::prelude::*;
use std::time::Duration;
/// A cache of pre-computed flood fill results, keyed by element ID.
pub struct FloodFillCache {
/// Cached results: element_id -> filled coordinates
way_cache: FnvHashMap<u64, Vec<(i32, i32)>>,
}
impl FloodFillCache {
/// Creates an empty cache.
pub fn new() -> Self {
Self {
way_cache: FnvHashMap::default(),
}
}
/// Pre-computes flood fills for all elements that need them.
///
/// This runs in parallel using Rayon, taking advantage of multiple CPU cores.
pub fn precompute(elements: &[ProcessedElement], timeout: Option<&Duration>) -> Self {
// Collect all ways that need flood fill
let ways_needing_fill: Vec<&ProcessedWay> = elements
.iter()
.filter_map(|el| match el {
ProcessedElement::Way(way) => {
if Self::way_needs_flood_fill(way) {
Some(way)
} else {
None
}
}
_ => None,
})
.collect();
// Compute all way flood fills in parallel
let way_results: Vec<(u64, Vec<(i32, i32)>)> = ways_needing_fill
.par_iter()
.map(|way| {
let polygon_coords: Vec<(i32, i32)> =
way.nodes.iter().map(|n| (n.x, n.z)).collect();
let filled = flood_fill_area(&polygon_coords, timeout);
(way.id, filled)
})
.collect();
// Build the cache
let mut cache = Self::new();
for (id, filled) in way_results {
cache.way_cache.insert(id, filled);
}
cache
}
/// Gets cached flood fill result for a way, or computes it if not cached.
///
/// Note: Combined ways created from relations (e.g., in `generate_natural_from_relation`)
/// will miss the cache and fall back to on-demand computation. This is by design,
/// these synthetic ways don't exist in the original element list and have relation IDs
/// rather than way IDs. The individual member ways are still cached.
pub fn get_or_compute(
&self,
way: &ProcessedWay,
timeout: Option<&Duration>,
) -> Vec<(i32, i32)> {
if let Some(cached) = self.way_cache.get(&way.id) {
// Clone is intentional: each result is typically accessed once during
// sequential processing, so the cost is acceptable vs Arc complexity
cached.clone()
} else {
// Fallback: compute on demand for synthetic/combined ways from relations
let polygon_coords: Vec<(i32, i32)> = way.nodes.iter().map(|n| (n.x, n.z)).collect();
flood_fill_area(&polygon_coords, timeout)
}
}
/// Gets cached flood fill result for a ProcessedElement (Way only).
/// For Nodes/Relations, returns empty vec.
pub fn get_or_compute_element(
&self,
element: &ProcessedElement,
timeout: Option<&Duration>,
) -> Vec<(i32, i32)> {
match element {
ProcessedElement::Way(way) => self.get_or_compute(way, timeout),
_ => Vec::new(),
}
}
/// Determines if a way element needs flood fill based on its tags.
///
/// This checks for tag presence (not specific values) because:
/// - Only some values within each tag type actually use flood fill
/// - But caching extra results is harmless (small memory overhead)
/// - And avoids duplicating value-checking logic from processors
///
/// Covered cases:
/// - building/building:part -> buildings::generate_buildings (includes bridge)
/// - landuse -> landuse::generate_landuse
/// - leisure -> leisure::generate_leisure
/// - amenity -> amenities::generate_amenities
/// - natural (except tree) -> natural::generate_natural
/// - highway with area=yes -> highways::generate_highways (area fill)
fn way_needs_flood_fill(way: &ProcessedWay) -> bool {
way.tags.contains_key("building")
|| way.tags.contains_key("building:part")
|| way.tags.contains_key("landuse")
|| way.tags.contains_key("leisure")
|| way.tags.contains_key("amenity")
|| way
.tags
.get("natural")
.map(|v| v != "tree")
.unwrap_or(false)
// Highway areas (like pedestrian plazas) use flood fill when area=yes
|| (way.tags.contains_key("highway")
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false))
}
/// Returns the number of cached way entries.
pub fn way_count(&self) -> usize {
self.way_cache.len()
}
/// Removes a way's cached flood fill result, freeing memory.
///
/// Call this after processing an element to release its cached data.
pub fn remove_way(&mut self, way_id: u64) {
self.way_cache.remove(&way_id);
}
/// Removes all cached flood fill results for ways in a relation.
///
/// Relations contain multiple ways, so we need to remove all of them.
pub fn remove_relation_ways(&mut self, way_ids: &[u64]) {
for &id in way_ids {
self.way_cache.remove(&id);
}
}
}
impl Default for FloodFillCache {
fn default() -> Self {
Self::new()
}
}
/// Configures the global Rayon thread pool with a CPU usage cap.
///
/// Call this once at startup before any parallel operations.
///
/// # Arguments
/// * `cpu_fraction` - Fraction of available cores to use (e.g., 0.9 for 90%).
/// Values are clamped to the range [0.1, 1.0].
pub fn configure_rayon_thread_pool(cpu_fraction: f64) {
// Clamp cpu_fraction to valid range
let cpu_fraction = cpu_fraction.clamp(0.1, 1.0);
let available_cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
let target_threads = ((available_cores as f64) * cpu_fraction).floor() as usize;
let target_threads = target_threads.max(1); // At least 1 thread
// Only configure if we haven't already (this can only be called once)
match rayon::ThreadPoolBuilder::new()
.num_threads(target_threads)
.build_global()
{
Ok(()) => {
// Successfully configured (silent to avoid cluttering output)
}
Err(_) => {
// Thread pool already configured
}
}
}

View File

@@ -2,16 +2,14 @@ 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::{self, GenerationOptions};
use crate::data_processing;
use crate::ground::{self, Ground};
use crate::map_transformation;
use crate::osm_parser;
use crate::progress::{self, emit_gui_progress_update};
use crate::progress;
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;
@@ -62,21 +60,7 @@ 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() {
// Configure thread pool with 90% CPU cap to keep system responsive
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
// Launch the UI
println!("Launching UI...");
@@ -117,8 +101,7 @@ pub fn run_gui() {
gui_start_generation,
gui_get_version,
gui_check_for_updates,
gui_get_world_map_data,
gui_show_in_folder
gui_get_world_map_data
])
.setup(|app| {
let app_handle = app.handle();
@@ -738,57 +721,6 @@ 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)]
@@ -797,6 +729,7 @@ fn gui_start_generation(
selected_world: String,
world_scale: f64,
ground_level: i32,
floodfill_timeout: u64,
terrain_enabled: bool,
skip_osm_objects: bool,
interior_enabled: bool,
@@ -805,7 +738,6 @@ 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;
@@ -817,9 +749,7 @@ fn gui_start_generation(
telemetry::send_generation_click();
// If spawn point was chosen and the world is new, check and set the spawn point
// Only update player position for Java worlds - Bedrock worlds don't have a pre-existing
// level.dat to modify (the spawn point will be set when the .mcworld is created)
if is_new_world && spawn_point.is_some() && world_format != "bedrock" {
if is_new_world && spawn_point.is_some() {
// Verify the spawn point is within bounds
if let Some(coords) = spawn_point {
let llbbox = match LLBBox::from_str(&bbox_text) {
@@ -850,52 +780,16 @@ fn gui_start_generation(
tauri::async_runtime::spawn(async move {
if let Err(e) = tokio::task::spawn_blocking(move || {
// Acquire session lock for the world directory before starting generation
let world_path = PathBuf::from(&selected_world);
// Determine world format from UI selection first (needed for session lock decision)
let world_format = if world_format == "bedrock" {
WorldFormat::BedrockMcWorld
} else {
WorldFormat::JavaAnvil
};
// Check available disk space before starting generation (minimum 3GB required)
const MIN_DISK_SPACE_BYTES: u64 = 3 * 1024 * 1024 * 1024; // 3 GB
let check_path = if world_format == WorldFormat::JavaAnvil {
world_path.clone()
} else {
// For Bedrock, check current directory where .mcworld will be created
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
};
match fs2::available_space(&check_path) {
Ok(available) if available < MIN_DISK_SPACE_BYTES => {
let error_msg = "Not enough disk space available.".to_string();
let _session_lock = match SessionLock::acquire(&world_path) {
Ok(lock) => lock,
Err(e) => {
let error_msg = format!("Failed to acquire session lock: {e}");
eprintln!("{error_msg}");
emit_gui_error(&error_msg);
return Err(error_msg);
}
Err(e) => {
// Log warning but don't block generation if we can't check space
eprintln!("Warning: Could not check disk space: {e}");
}
_ => {} // Sufficient space available
}
// Acquire session lock for Java worlds only
// Session lock prevents Minecraft from having the world open during generation
// Bedrock worlds are generated as .mcworld files and don't need this lock
let _session_lock: Option<SessionLock> = if world_format == WorldFormat::JavaAnvil {
match SessionLock::acquire(&world_path) {
Ok(lock) => Some(lock),
Err(e) => {
let error_msg = format!("Failed to acquire session lock: {e}");
eprintln!("{error_msg}");
emit_gui_error(&error_msg);
return Err(error_msg);
}
}
} else {
None
};
// Parse the bounding box from the text with proper error handling
@@ -909,66 +803,19 @@ fn gui_start_generation(
}
};
// 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
}
// 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)
} else {
None
world_path
};
// 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
// Create an Args instance with the chosen bounding box and world directory path
let args: Args = Args {
bbox,
file: None,
save_json_file: None,
path: if world_format == WorldFormat::JavaAnvil {
generation_path
} else {
world_path
},
path: updated_world_path,
downloader: "requests".to_string(),
scale: world_scale,
ground_level,
@@ -977,7 +824,8 @@ fn gui_start_generation(
roof: roof_enabled,
fillground: fillground_enabled,
debug: false,
timeout: Some(std::time::Duration::from_secs(40)),
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
generate_map: true,
spawn_point,
};
@@ -992,29 +840,14 @@ 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_with_options(
let _ = data_processing::generate_world(
parsed_elements,
xzbbox.clone(),
xzbbox,
args.bbox,
ground,
&args,
generation_options.clone(),
);
// 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);
}
// Session lock will be automatically released when _session_lock goes out of scope
return Ok(());
}
@@ -1045,29 +878,14 @@ fn gui_start_generation(
&mut ground,
);
let _ = data_processing::generate_world_with_options(
let _ = data_processing::generate_world(
parsed_elements,
xzbbox.clone(),
xzbbox,
args.bbox,
ground,
&args,
generation_options.clone(),
);
// 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);
}
// Session lock will be automatically released when _session_lock goes out of scope
Ok(())
}
Err(e) => {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -158,13 +158,12 @@
background-position: -182px -2px;
}
/* Disabled states reuse same sprites; opacity indicates disabled */
.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled {
background-position: -152px -2px;
background-position: -212px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
background-position: -182px -2px;
background-position: -242px -2px;
}
/* ================================================================== */

View File

@@ -222,68 +222,6 @@ 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;

37
src/gui/index.html vendored
View File

@@ -37,26 +37,15 @@
<div class="controls-content">
<h2 data-localize="select_world">Select World</h2>
<!-- World Selection Container -->
<div class="world-selection-container">
<div class="tooltip" style="width: 100%;">
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
<span id="choose_world">Choose World</span>
<br>
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
No world selected
</span>
</button>
</div>
<!-- World Format Toggle -->
<div class="format-toggle-container">
<button type="button" id="format-java" class="format-toggle-btn format-active" onclick="setWorldFormat('java')">
Java
</button>
<button type="button" id="format-bedrock" class="format-toggle-btn" onclick="setWorldFormat('bedrock')">
Bedrock
</button>
</div>
<!-- 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>
</div>
<div class="button-container">
@@ -151,6 +140,14 @@
</div>
</div>
<!-- Floodfill Timeout Input -->
<div class="settings-row">
<label for="floodfill-timeout" data-localize="floodfill_timeout">Floodfill Timeout (sec)</label>
<div class="settings-control">
<input type="number" id="floodfill-timeout" name="floodfill-timeout" min="0" step="1" value="20" placeholder="Seconds">
</div>
</div>
<!-- Map Theme Selector -->
<div class="settings-row">
<label for="tile-theme-select" data-localize="map_theme">Map Theme</label>

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 >= 6 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
if (failureCount >= 3 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
console.log('HTTPS tile loading failed, attempting HTTP fallback for', themeKey);
this._httpFallbackAttempted = true;
@@ -899,15 +899,6 @@ $(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,10 +24,6 @@ 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>.

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

@@ -24,7 +24,6 @@ window.addEventListener("DOMContentLoaded", async () => {
handleBboxInput();
const localization = await getLocalization();
await applyLocalization(localization);
updateFormatToggleUI(selectedWorldFormat);
initFooter();
await checkForUpdates();
});
@@ -91,6 +90,7 @@ async function applyLocalization(localization) {
"h2[data-localize='customization_settings']": "customization_settings",
"label[data-localize='world_scale']": "world_scale",
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
"label[data-localize='floodfill_timeout']": "floodfill_timeout",
// DEPRECATED: Ground level localization removed
// "label[data-localize='ground_level']": "ground_level",
"label[data-localize='language']": "language",
@@ -109,6 +109,7 @@ async function applyLocalization(localization) {
// Placeholder strings
"input[id='bbox-coords']": "placeholder_bbox",
"input[id='floodfill-timeout']": "placeholder_floodfill",
// DEPRECATED: Ground level placeholder removed
// "input[id='ground-level']": "placeholder_ground"
};
@@ -219,17 +220,6 @@ 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() {
@@ -258,9 +248,6 @@ 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);
@@ -363,72 +350,6 @@ 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'
@@ -618,8 +539,8 @@ function normalizeLongitude(lon) {
return ((lon + 180) % 360 + 360) % 360 - 180;
}
const threshold1 = 44000000.00; // Yellow warning threshold (~6.2km x 7km)
const threshold2 = 85000000.00; // Red error threshold (~8.7km x 9.8km)
const threshold1 = 30000000.00;
const threshold2 = 45000000.00;
let selectedBBox = "";
let mapSelectedBBox = ""; // Tracks bbox from map selection
let customBBoxValid = false; // Tracks if custom input is valid
@@ -757,8 +678,7 @@ async function startGeneration() {
return;
}
// Only require world selection for Java format (Bedrock generates a new .mcworld file)
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
if (!worldPath || worldPath === "") {
const selectedWorld = document.getElementById('selected-world');
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
selectedWorld.style.color = "#fa7878";
@@ -789,12 +709,14 @@ async function startGeneration() {
var roof = document.getElementById("roof-toggle").checked;
var fill_ground = document.getElementById("fillground-toggle").checked;
var scale = parseFloat(document.getElementById("scale-value-slider").value);
var floodfill_timeout = parseInt(document.getElementById("floodfill-timeout").value, 10);
// var ground_level = parseInt(document.getElementById("ground-level").value, 10);
// DEPRECATED: Ground level input removed from UI
var ground_level = -62;
// Validate ground_level
ground_level = isNaN(ground_level) || ground_level < -62 ? -62 : ground_level;
// Validate floodfill_timeout and ground_level
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
// Get telemetry consent (defaults to false if not set)
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
@@ -805,6 +727,7 @@ async function startGeneration() {
selectedWorld: worldPath,
worldScale: scale,
groundLevel: ground_level,
floodfillTimeout: floodfill_timeout,
terrainEnabled: terrain,
skipOsmObjects: skipOsmObjects,
interiorEnabled: interior,
@@ -812,8 +735,7 @@ async function startGeneration() {
fillgroundEnabled: fill_ground,
isNewWorld: isNewWorld,
spawnPoint: spawnPoint,
telemetryConsent: telemetryConsent || false,
worldFormat: selectedWorldFormat
telemetryConsent: telemetryConsent || false
});
console.log("Generation process started.");

View File

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

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Nur Terrain",
"interior": "Innenraum Generierung",
"roof": "Dach Generierung",
"fillground": "Boden füllen",
"bedrock_use_java": "Java für Weltauswahl nutzen"
"fillground": "Boden füllen"
}

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Terrain only",
"interior": "Interior Generation",
"roof": "Roof Generation",
"fillground": "Fill Ground",
"bedrock_use_java": "Use Java to select worlds"
"fillground": "Fill Ground"
}

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Solo Terreno",
"interior": "Generación Interior",
"roof": "Generación de Tejado",
"fillground": "Rellenar Suelo",
"bedrock_use_java": "Usa Java para elegir mundos"
"fillground": "Rellenar Suelo"
}

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Vain maasto",
"interior": "Sisätilan luonti",
"roof": "Katon luonti",
"fillground": "Täytä maa",
"bedrock_use_java": "Käytä Javaa maailmojen valintaan"
"fillground": "Täytä maa"
}

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Terrain uniquement",
"interior": "Génération d'intérieur",
"roof": "Génération de toit",
"fillground": "Remplir le sol",
"bedrock_use_java": "Utilisez Java pour les mondes"
"fillground": "Remplir le sol"
}

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Csak terep",
"interior": "Belső generálás",
"roof": "Tető generálás",
"fillground": "Talaj feltöltése",
"bedrock_use_java": "Java világválasztáshoz"
"fillground": "Talaj feltöltése"
}

View File

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

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Tik reljefas",
"interior": "Interjero generavimas",
"roof": "Stogo generavimas",
"fillground": "Užpildyti pagrindą",
"bedrock_use_java": "Naudok Java pasauliams"
"fillground": "Užpildyti pagrindą"
}

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Tikai reljefs",
"interior": "Interjera ģenerēšana",
"roof": "Jumta ģenerēšana",
"fillground": "Aizpildīt zemi",
"bedrock_use_java": "Izmanto Java pasaulēm"
"fillground": "Aizpildīt zemi"
}

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Tylko teren",
"interior": "Generowanie wnętrza",
"roof": "Generowanie dachu",
"fillground": "Wypełnij podłoże",
"bedrock_use_java": "Użyj Java do wyboru światów"
"fillground": "Wypełnij podłoże"
}

View File

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

View File

@@ -44,6 +44,5 @@
"mode_terrain_only": "Endast terräng",
"interior": "Interiörgenerering",
"roof": "Takgenerering",
"fillground": "Fyll mark",
"bedrock_use_java": "Använd Java för världar"
"fillground": "Fyll mark"
}

View File

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

View File

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

View File

@@ -1,19 +1,15 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod args;
#[cfg(feature = "bedrock")]
mod bedrock_block_map;
mod block_definitions;
mod bresenham;
mod clipping;
mod colors;
mod coordinate_system;
mod data_processing;
mod deterministic_rng;
mod element_processing;
mod elevation_data;
mod floodfill;
mod floodfill_cache;
mod ground;
mod map_renderer;
mod map_transformation;
@@ -42,7 +38,6 @@ 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
}
@@ -51,9 +46,6 @@ mod progress {
use windows::Win32::System::Console::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS};
fn run_cli() {
// Configure thread pool with 90% CPU cap to keep system responsive
floodfill_cache::configure_rayon_thread_pool(0.9);
let version: &str = env!("CARGO_PKG_VERSION");
let repository: &str = env!("CARGO_PKG_REPOSITORY");
println!(

View File

@@ -5,8 +5,8 @@ use crate::coordinate_system::transformation::CoordTransformer;
use crate::progress::emit_gui_progress_update;
use colored::Colorize;
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
// Raw data from OSM
@@ -29,18 +29,9 @@ struct OsmElement {
pub members: Vec<OsmMember>,
}
#[derive(Debug, Deserialize)]
pub struct OsmData {
elements: Vec<OsmElement>,
#[serde(default)]
pub remark: Option<String>,
}
impl OsmData {
/// Returns true if there are no elements in the OSM data
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
#[derive(Deserialize)]
struct OsmData {
pub elements: Vec<OsmElement>,
}
struct SplitOsmData {
@@ -77,6 +68,11 @@ impl SplitOsmData {
}
}
fn parse_raw_osm_data(json_data: Value) -> Result<SplitOsmData, serde_json::Error> {
let osm_data: OsmData = serde_json::from_value(json_data)?;
Ok(SplitOsmData::from_raw_osm_data(osm_data))
}
// End raw data
// Normalized data that we can use
@@ -116,7 +112,7 @@ pub enum ProcessedMemberRole {
#[derive(Debug, Clone, PartialEq)]
pub struct ProcessedMember {
pub role: ProcessedMemberRole,
pub way: Arc<ProcessedWay>,
pub way: ProcessedWay,
}
#[derive(Debug, Clone, PartialEq)]
@@ -168,7 +164,7 @@ impl ProcessedElement {
}
pub fn parse_osm_data(
osm_data: OsmData,
json_data: Value,
bbox: LLBBox,
scale: f64,
debug: bool,
@@ -178,7 +174,7 @@ pub fn parse_osm_data(
emit_gui_progress_update(5.0, "Parsing data...");
// Deserialize the JSON data into the OSMData structure
let data = SplitOsmData::from_raw_osm_data(osm_data);
let data = parse_raw_osm_data(json_data).expect("Failed to parse OSM data");
let (coord_transformer, xzbbox) = CoordTransformer::llbbox_to_xzbbox(&bbox, scale)
.unwrap_or_else(|e| {
@@ -193,7 +189,7 @@ pub fn parse_osm_data(
}
let mut nodes_map: HashMap<u64, ProcessedNode> = HashMap::new();
let mut ways_map: HashMap<u64, Arc<ProcessedWay>> = HashMap::new();
let mut ways_map: HashMap<u64, ProcessedWay> = HashMap::new();
let mut processed_elements: Vec<ProcessedElement> = Vec::new();
@@ -242,15 +238,17 @@ pub fn parse_osm_data(
let tags = element.tags.clone().unwrap_or_default();
// Store unclipped way for relation assembly (clipping happens after ring merging)
let way = Arc::new(ProcessedWay {
id: element.id,
tags,
nodes,
});
ways_map.insert(element.id, Arc::clone(&way));
ways_map.insert(
element.id,
ProcessedWay {
id: element.id,
tags: tags.clone(),
nodes: nodes.clone(),
},
);
// Clip way nodes for standalone way processing (not relations)
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
let clipped_nodes = clip_way_to_bbox(&nodes, &xzbbox);
// Skip ways that are completely outside the bbox (empty after clipping)
if clipped_nodes.is_empty() {
@@ -259,8 +257,8 @@ pub fn parse_osm_data(
let processed: ProcessedWay = ProcessedWay {
id: element.id,
tags: way.tags.clone(),
nodes: clipped_nodes,
tags: tags.clone(),
nodes: clipped_nodes.clone(),
};
processed_elements.push(ProcessedElement::Way(processed));
@@ -296,8 +294,8 @@ pub fn parse_osm_data(
};
// Check if the way exists in ways_map
let way = match ways_map.get(&mem.r#ref) {
Some(w) => Arc::clone(w),
let way: ProcessedWay = match ways_map.get(&mem.r#ref) {
Some(w) => w.clone(),
None => {
// Way was likely filtered out because it was completely outside the bbox
return None;
@@ -313,11 +311,11 @@ pub fn parse_osm_data(
if clipped_nodes.is_empty() {
return None;
}
Arc::new(ProcessedWay {
ProcessedWay {
id: way.id,
tags: way.tags.clone(),
tags: way.tags,
nodes: clipped_nodes,
})
}
};
Some(ProcessedMember {
@@ -338,9 +336,6 @@ pub fn parse_osm_data(
emit_gui_progress_update(15.0, "");
drop(nodes_map);
drop(ways_map);
(processed_elements, xzbbox)
}

View File

@@ -65,12 +65,3 @@ 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

@@ -1,14 +1,12 @@
use crate::coordinate_system::geographic::LLBBox;
use crate::osm_parser::OsmData;
use crate::progress::{emit_gui_error, emit_gui_progress_update, is_running_with_gui};
use colored::Colorize;
use rand::seq::SliceRandom;
use reqwest::blocking::Client;
use reqwest::blocking::ClientBuilder;
use serde::Deserialize;
use serde_json::Value;
use std::fs::File;
use std::io::{self, BufReader, Cursor, Write};
use std::io::{self, BufReader, Write};
use std::process::Command;
use std::time::Duration;
@@ -81,14 +79,13 @@ fn download_with_wget(url: &str, query: &str) -> io::Result<String> {
}
}
pub fn fetch_data_from_file(file: &str) -> Result<OsmData, Box<dyn std::error::Error>> {
pub fn fetch_data_from_file(file: &str) -> Result<Value, Box<dyn std::error::Error>> {
println!("{} Loading data from file...", "[1/7]".bold());
emit_gui_progress_update(1.0, "Loading data from file...");
let file: File = File::open(file)?;
let reader: BufReader<File> = BufReader::new(file);
let mut deserializer = serde_json::Deserializer::from_reader(reader);
let data: OsmData = OsmData::deserialize(&mut deserializer)?;
let data: Value = serde_json::from_reader(reader)?;
Ok(data)
}
@@ -98,7 +95,7 @@ pub fn fetch_data_from_overpass(
debug: bool,
download_method: &str,
save_file: Option<&str>,
) -> Result<OsmData, Box<dyn std::error::Error>> {
) -> Result<Value, Box<dyn std::error::Error>> {
println!("{} Fetching data...", "[1/7]".bold());
emit_gui_progress_update(1.0, "Fetching data...");
@@ -185,12 +182,14 @@ pub fn fetch_data_from_overpass(
println!("API response saved to: {save_file}");
}
let mut deserializer =
serde_json::Deserializer::from_reader(Cursor::new(response.as_bytes()));
let data: OsmData = OsmData::deserialize(&mut deserializer)?;
let data: Value = serde_json::from_str(&response)?;
if data.is_empty() {
if let Some(remark) = data.remark.as_deref() {
if data["elements"]
.as_array()
.map_or(0, |elements: &Vec<Value>| elements.len())
== 0
{
if let Some(remark) = data["remark"].as_str() {
// Check if the remark mentions memory or other runtime errors
if remark.contains("runtime error") && remark.contains("out of memory") {
eprintln!("{}", "Error! The query ran out of memory on the Overpass API server. Try using a smaller area.".red().bold());
@@ -212,7 +211,7 @@ pub fn fetch_data_from_overpass(
}
if debug {
println!("Additional debug information: {data:?}");
println!("Additional debug information: {data}");
}
if !is_running_with_gui() {

View File

@@ -7,8 +7,9 @@ use crate::retrieve_data;
// this is copied from main.rs
pub fn generate_example(llbbox: LLBBox) -> (XZBBox, Vec<ProcessedElement>) {
// Fetch data
let raw_data = retrieve_data::fetch_data_from_overpass(llbbox, false, "requests", None)
.expect("Failed to fetch data");
let raw_data: serde_json::Value =
retrieve_data::fetch_data_from_overpass(llbbox, false, "requests", None)
.expect("Failed to fetch data");
// Parse raw data
let (mut parsed_elements, xzbbox) = osm_parser::parse_osm_data(raw_data, llbbox, 1.0, false);

1030
src/world_editor.rs Normal file
View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,323 +0,0 @@
//! 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::*;
/// Minimum Y coordinate in Minecraft (1.18+)
const MIN_Y: i32 = -64;
/// Maximum Y coordinate in Minecraft (1.18+)
const MAX_Y: i32 = 319;
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> {
// Clamp Y to valid Minecraft range to prevent TryFromIntError
let y = y.clamp(MIN_Y, MAX_Y);
let section_idx: i8 = (y >> 4) as i8;
let section = self.sections.get(&section_idx)?;
section.get_block(x, (y & 15) as u8, z)
}
#[inline]
pub fn set_block(&mut self, x: u8, y: i32, z: u8, block: Block) {
// Clamp Y to valid Minecraft range to prevent TryFromIntError
let y = y.clamp(MIN_Y, MAX_Y);
let section_idx: i8 = (y >> 4) as i8;
let section = self.sections.entry(section_idx).or_default();
section.set_block(x, (y & 15) as u8, z, block);
}
#[inline]
pub fn set_block_with_properties(
&mut self,
x: u8,
y: i32,
z: u8,
block_with_props: BlockWithProperties,
) {
// Clamp Y to valid Minecraft range to prevent TryFromIntError
let y = y.clamp(MIN_Y, MAX_Y);
let section_idx: i8 = (y >> 4) as i8;
let section = self.sections.entry(section_idx).or_default();
section.set_block_with_properties(x, (y & 15) as u8, 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,
);
}
}

View File

@@ -1,348 +0,0 @@
//! Java Edition Anvil format world saving.
//!
//! This module handles saving worlds in the Java Edition Anvil (.mca) format.
//! Supports streaming mode for memory-efficient saving of large worlds.
use super::common::{Chunk, ChunkToModify, RegionToModify, 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::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
impl<'a> WorldEditor<'a> {
/// Saves the world in Java Edition Anvil format.
///
/// Uses parallel processing: saves multiple regions concurrently for faster I/O,
/// while still releasing memory after each region is processed.
pub(super) fn save_java(&mut self) {
println!("{} Saving world...", "[7/7]".bold());
emit_gui_progress_update(90.0, "Saving world...");
// 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;
// Early return if no regions to save (prevents division by zero)
if total_regions == 0 {
return;
}
let save_pb = ProgressBar::new(total_regions);
save_pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} regions ({eta})",
)
.unwrap()
.progress_chars("█▓░"),
);
// Ensure region directory exists before parallel processing
let region_dir = self.world_dir.join("region");
std::fs::create_dir_all(&region_dir).expect("Failed to create region directory");
// Drain all regions from memory into a Vec for parallel processing
let regions_to_save: Vec<((i32, i32), super::common::RegionToModify)> =
self.world.regions.drain().collect();
// Track progress atomically across threads
let regions_processed = AtomicU64::new(0);
let world_dir = self.world_dir.clone();
// Process regions in parallel, each region file is independent
regions_to_save
.into_par_iter()
.for_each(|((region_x, region_z), region_to_modify)| {
// Save this region (creates its own file handle)
save_region_to_file(&world_dir, region_x, region_z, &region_to_modify);
// Update progress atomically
let processed = regions_processed.fetch_add(1, Ordering::Relaxed) + 1;
save_pb.inc(1);
// Emit GUI progress update periodically
let update_interval = (total_regions / 10).max(1);
if processed.is_multiple_of(update_interval) || processed == total_regions {
let progress = 90.0 + (processed as f64 / total_regions as f64) * 9.0;
emit_gui_progress_update(progress, "Saving world...");
}
// Region memory is freed when region_to_modify goes out of scope here
});
save_pb.finish();
}
}
/// Saves a single region to a file (thread-safe, for parallel processing).
///
/// This is a standalone function that can be called from parallel threads
/// since it only needs the world directory path, not a reference to WorldEditor.
fn save_region_to_file(
world_dir: &Path,
region_x: i32,
region_z: i32,
region_to_modify: &RegionToModify,
) {
// Create region file
let region_dir = world_dir.join("region");
let out_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
let mut region_file: File = File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&out_path)
.expect("Failed to open region file");
region_file
.write_all(REGION_TEMPLATE)
.expect("Could not write region template");
let mut region = Region::from_stream(region_file).expect("Failed to load region");
let mut ser_buffer = Vec::with_capacity(8192);
// First pass: write modified chunks
for (&(chunk_x, chunk_z), chunk_to_modify) in &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 (create base chunks for empty slots)
for chunk_x in 0..32 {
for chunk_z in 0..32 {
let abs_chunk_x = chunk_x + (region_x * 32);
let abs_chunk_z = chunk_z + (region_z * 32);
// Check if chunk exists in our modifications
let chunk_exists = region_to_modify.chunks.contains_key(&(chunk_x, chunk_z));
// If chunk doesn't exist, create it with base layer
if !chunk_exists {
let (ser_buffer, _) = create_base_chunk(abs_chunk_x, abs_chunk_z);
region
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
.unwrap();
}
}
}
}
/// Helper function to create a base chunk with grass blocks at Y -62 (standalone version)
fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
let mut chunk = ChunkToModify::default();
// Fill the bottom layer with grass blocks at Y -62
for x in 0..16 {
for z in 0..16 {
chunk.set_block(x, -62, z, GRASS_BLOCK);
}
}
// 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)
}
/// 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(),
),
),
])),
)])
}

View File

@@ -1,588 +0,0 @@
//! 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;
use std::sync::Arc;
#[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<Arc<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: Arc<Ground>) {
self.ground = Some(ground);
}
/// Gets a reference to the ground data if available
pub fn get_ground(&self) -> Option<&Ground> {
self.ground.as_deref()
}
/// 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.4.0",
"version": "2.3.1",
"identifier": "com.louisdev.arnis",
"build": {
"frontendDist": "src/gui"
@@ -16,7 +16,7 @@
"minWidth": 1000,
"minHeight": 650,
"resizable": true,
"transparent": false,
"transparent": true,
"center": true,
"theme": "Dark",
"additionalBrowserArgs": "--disable-features=VizDisplayCompositor"