mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-29 08:23:18 -05:00
Compare commits
5 Commits
parallel-p
...
benchmark-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
996e8756d0 | ||
|
|
0c5bd51ba4 | ||
|
|
7965dc3737 | ||
|
|
c54187b43a | ||
|
|
beb7b73d11 |
46
.github/workflows/pr-benchmark.yml
vendored
46
.github/workflows/pr-benchmark.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Rust
|
- name: Set up Rust
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Run benchmark command with memory tracking
|
- name: Run benchmark command with memory tracking
|
||||||
id: benchmark
|
id: benchmark
|
||||||
run: |
|
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
|
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
|
||||||
peak_kb=$(cat peak_mem_kb.txt)
|
peak_kb=$(cat peak_mem_kb.txt)
|
||||||
peak_mb=$((peak_kb / 1024))
|
peak_mb=$((peak_kb / 1024))
|
||||||
@@ -57,6 +57,25 @@ jobs:
|
|||||||
duration=$((end_time - start_time))
|
duration=$((end_time - start_time))
|
||||||
echo "duration=$duration" >> $GITHUB_OUTPUT
|
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
|
- name: Format duration and generate summary
|
||||||
id: comment_body
|
id: comment_body
|
||||||
run: |
|
run: |
|
||||||
@@ -65,7 +84,7 @@ jobs:
|
|||||||
seconds=$((duration % 60))
|
seconds=$((duration % 60))
|
||||||
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
||||||
|
|
||||||
baseline_time=30
|
baseline_time=69
|
||||||
diff=$((duration - baseline_time))
|
diff=$((duration - baseline_time))
|
||||||
abs_diff=${diff#-}
|
abs_diff=${diff#-}
|
||||||
|
|
||||||
@@ -87,20 +106,27 @@ jobs:
|
|||||||
mem_annotation=" (↗ ${mem_percent}% more)"
|
mem_annotation=" (↗ ${mem_percent}% more)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Get current timestamp
|
||||||
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
|
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 "summary<<EOF"
|
||||||
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
|
echo "## ⏱️ Benchmark Results"
|
||||||
echo "🧠 Peak memory usage: **${peak_mem} MB**${mem_annotation}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "📈 Compared against baseline: **${baseline_time}s**"
|
echo "| Metric | Value |"
|
||||||
echo "🧮 Delta: **${diff}s**"
|
echo "|--------|-------|"
|
||||||
echo "🔢 Commit: [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA})"
|
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 ""
|
||||||
echo "${verdict}"
|
echo "${verdict}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📅 **Last benchmark:** ${benchmark_time}"
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
echo "📅 **Last benchmark:** ${benchmark_time} | 📥 [Download generated world map](${run_url}#artifacts)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
|
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
|
||||||
echo "EOF"
|
echo "EOF"
|
||||||
@@ -112,4 +138,4 @@ jobs:
|
|||||||
message: ${{ steps.comment_body.outputs.summary }}
|
message: ${{ steps.comment_body.outputs.summary }}
|
||||||
comment-tag: benchmark-report
|
comment-tag: benchmark-report
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
|
||||||
|
|||||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
binary_name: arnis
|
binary_name: arnis
|
||||||
asset_name: arnis-linux
|
asset_name: arnis-linux
|
||||||
- os: macos-15-intel # Intel runner for x86_64 builds
|
- os: macos-13 # Intel runner for x86_64 builds
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
binary_name: arnis
|
binary_name: arnis
|
||||||
asset_name: arnis-mac-intel
|
asset_name: arnis-mac-intel
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
shell: powershell
|
shell: powershell
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-${{ matrix.target }}-build
|
name: ${{ matrix.os }}-${{ matrix.target }}-build
|
||||||
path: target/release/${{ matrix.asset_name }}
|
path: target/release/${{ matrix.asset_name }}
|
||||||
@@ -97,13 +97,13 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download macOS Intel build
|
- name: Download macOS Intel build
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: macos-15-intel-x86_64-apple-darwin-build
|
name: macos-13-x86_64-apple-darwin-build
|
||||||
path: ./intel
|
path: ./intel
|
||||||
|
|
||||||
- name: Download macOS ARM64 build
|
- name: Download macOS ARM64 build
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: macos-latest-aarch64-apple-darwin-build
|
name: macos-latest-aarch64-apple-darwin-build
|
||||||
path: ./arm64
|
path: ./arm64
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
chmod +x arnis-mac-universal
|
chmod +x arnis-mac-universal
|
||||||
|
|
||||||
- name: Upload universal binary
|
- name: Upload universal binary
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-universal-build
|
name: macos-universal-build
|
||||||
path: arnis-mac-universal
|
path: arnis-mac-universal
|
||||||
@@ -127,19 +127,19 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download Windows build artifact
|
- name: Download Windows build artifact
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: windows-latest-x86_64-pc-windows-msvc-build
|
name: windows-latest-x86_64-pc-windows-msvc-build
|
||||||
path: ./builds/windows
|
path: ./builds/windows
|
||||||
|
|
||||||
- name: Download Linux build artifact
|
- name: Download Linux build artifact
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
|
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
|
||||||
path: ./builds/linux
|
path: ./builds/linux
|
||||||
|
|
||||||
- name: Download macOS universal build artifact
|
- name: Download macOS universal build artifact
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: macos-universal-build
|
name: macos-universal-build
|
||||||
path: ./builds/macos
|
path: ./builds/macos
|
||||||
@@ -157,4 +157,4 @@ jobs:
|
|||||||
builds/linux/arnis-linux
|
builds/linux/arnis-linux
|
||||||
builds/macos/arnis-mac-universal
|
builds/macos/arnis-mac-universal
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
/wiki
|
/wiki
|
||||||
*.mcworld
|
|
||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
|||||||
330
Cargo.lock
generated
330
Cargo.lock
generated
@@ -25,7 +25,7 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
@@ -182,12 +182,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arnis"
|
name = "arnis"
|
||||||
version = "2.4.1"
|
version = "2.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bedrockrs_level",
|
|
||||||
"bedrockrs_shared",
|
|
||||||
"byteorder",
|
|
||||||
"clap",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -201,10 +198,8 @@ dependencies = [
|
|||||||
"indicatif",
|
"indicatif",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
"nbtx",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rand_chacha 0.3.1",
|
|
||||||
"rayon",
|
"rayon",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rfd",
|
"rfd",
|
||||||
@@ -217,9 +212,7 @@ dependencies = [
|
|||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"vek",
|
|
||||||
"windows",
|
"windows",
|
||||||
"zip",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -305,7 +298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
|
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
@@ -351,7 +344,7 @@ dependencies = [
|
|||||||
"async-signal",
|
"async-signal",
|
||||||
"async-task",
|
"async-task",
|
||||||
"blocking",
|
"blocking",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"event-listener",
|
"event-listener",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"rustix 0.38.42",
|
"rustix 0.38.42",
|
||||||
@@ -378,7 +371,7 @@ dependencies = [
|
|||||||
"async-io",
|
"async-io",
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"rustix 0.38.42",
|
"rustix 0.38.42",
|
||||||
@@ -474,73 +467,6 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
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]]
|
[[package]]
|
||||||
name = "bit_field"
|
name = "bit_field"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -711,9 +637,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.19.0"
|
version = "1.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
|
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -841,12 +767,6 @@ dependencies = [
|
|||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -943,16 +863,6 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -1050,28 +960,13 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1401,7 +1296,7 @@ version = "0.8.35"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1457,17 +1352,6 @@ dependencies = [
|
|||||||
"typeid",
|
"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]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
@@ -1478,16 +1362,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.3.1"
|
version = "5.3.1"
|
||||||
@@ -1596,7 +1470,7 @@ version = "0.2.25"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"libredox",
|
"libredox",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
@@ -1951,7 +1825,7 @@ version = "0.1.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
"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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1975,7 +1847,7 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasi 0.14.2+wasi-0.2.4",
|
"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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
|
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2678,12 +2550,6 @@ dependencies = [
|
|||||||
"cfb",
|
"cfb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "integer-encoding"
|
|
||||||
version = "3.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interpolate_name"
|
name = "interpolate_name"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -2789,7 +2655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cesu8",
|
"cesu8",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"combine",
|
"combine",
|
||||||
"jni-sys",
|
"jni-sys",
|
||||||
"log",
|
"log",
|
||||||
@@ -2851,21 +2717,6 @@ dependencies = [
|
|||||||
"serde_json",
|
"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]]
|
[[package]]
|
||||||
name = "keyboard-types"
|
name = "keyboard-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -2901,15 +2752,6 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
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]]
|
[[package]]
|
||||||
name = "libappindicator"
|
name = "libappindicator"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2956,7 +2798,7 @@ version = "0.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3082,7 +2924,7 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"rayon",
|
"rayon",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3172,18 +3014,6 @@ dependencies = [
|
|||||||
"tempfile",
|
"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]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -3227,7 +3057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
@@ -3653,7 +3483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"foreign-types 0.3.2",
|
"foreign-types 0.3.2",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -3763,7 +3593,7 @@ version = "0.9.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -3782,16 +3612,6 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
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]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -3993,7 +3813,7 @@ version = "3.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
|
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -4317,7 +4137,7 @@ dependencies = [
|
|||||||
"av1-grain",
|
"av1-grain",
|
||||||
"bitstream-io",
|
"bitstream-io",
|
||||||
"built",
|
"built",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"interpolate_name",
|
"interpolate_name",
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -4488,9 +4308,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfd"
|
name = "rfd"
|
||||||
version = "0.16.0"
|
version = "0.15.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"block2 0.6.1",
|
"block2 0.6.1",
|
||||||
@@ -4507,7 +4327,7 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4523,7 +4343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
"libc",
|
"libc",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
@@ -4608,7 +4428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"errno 0.3.10",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.14",
|
"linux-raw-sys 0.4.14",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
@@ -4621,7 +4441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"errno 0.3.10",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.9.4",
|
"linux-raw-sys 0.9.4",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
@@ -4672,20 +4492,6 @@ version = "1.0.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
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]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -4806,12 +4612,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "seq-macro"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -4994,7 +4794,7 @@ version = "0.10.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
@@ -5045,18 +4845,6 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
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]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@@ -5078,12 +4866,6 @@ version = "1.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "snap"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -6031,7 +5813,7 @@ version = "1.6.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
|
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6210,31 +5992,12 @@ version = "1.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "varint-rs"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
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]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -6313,7 +6076,7 @@ version = "0.2.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
@@ -6339,7 +6102,7 @@ version = "0.4.50"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -6766,15 +6529,6 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -7008,7 +6762,7 @@ version = "0.55.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
|
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7118,12 +6872,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
@@ -7281,18 +7029,6 @@ dependencies = [
|
|||||||
"syn 2.0.95",
|
"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]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|||||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "arnis"
|
name = "arnis"
|
||||||
version = "2.4.1"
|
version = "2.3.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Arnis - Generate real life cities in Minecraft"
|
description = "Arnis - Generate real life cities in Minecraft"
|
||||||
homepage = "https://github.com/louis-e/arnis"
|
homepage = "https://github.com/louis-e/arnis"
|
||||||
@@ -14,15 +14,13 @@ overflow-checks = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gui"]
|
default = ["gui"]
|
||||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build"]
|
||||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = {version = "2", optional = true}
|
tauri-build = {version = "2", optional = true}
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
byteorder = { version = "1.5", optional = true }
|
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
dirs = {version = "6.0.0", optional = true }
|
dirs = {version = "6.0.0", optional = true }
|
||||||
@@ -38,10 +36,9 @@ itertools = "0.14.0"
|
|||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rand_chacha = "0.3"
|
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
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"
|
semver = "1.0.27"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@@ -49,11 +46,6 @@ tauri = { version = "2", optional = true }
|
|||||||
tauri-plugin-log = { version = "2.6.0", optional = true }
|
tauri-plugin-log = { version = "2.6.0", optional = true }
|
||||||
tauri-plugin-shell = { version = "2", optional = true }
|
tauri-plugin-shell = { version = "2", optional = true }
|
||||||
tokio = { version = "1.48.0", features = ["full"], 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]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Arnis [](https://github.com/louis-e/arnis/actions) [<img alt="GitHub Release" src="https://img.shields.io/github/v/release/louis-e/arnis" />](https://github.com/louis-e/arnis/releases) [<img alt="GitHub Downloads (all assets, all releases" src="https://img.shields.io/github/downloads/louis-e/arnis/total" />](https://github.com/louis-e/arnis/releases) [](https://github.com/louis-e/arnis/releases) [](https://discord.gg/mA2g69Fhxq)
|
# Arnis [](https://github.com/louis-e/arnis/actions) [<img alt="GitHub Release" src="https://img.shields.io/github/v/release/louis-e/arnis" />](https://github.com/louis-e/arnis/releases) [<img alt="GitHub Downloads (all assets, all releases" src="https://img.shields.io/github/downloads/louis-e/arnis/total" />](https://github.com/louis-e/arnis/releases) [](https://github.com/louis-e/arnis/releases) [](https://discord.gg/mA2g69Fhxq)
|
||||||
|
|
||||||
Arnis creates complex and accurate Minecraft Java Edition (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.
|
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!
|
Generate your hometown, big cities, and natural landscapes with ease!
|
||||||
@@ -63,8 +63,6 @@ Arnis has been recognized in various academic and press publications after gaini
|
|||||||
|
|
||||||
[XDA Developers: Hometown Minecraft Map: Arnis](https://www.xda-developers.com/hometown-minecraft-map-arnis/)
|
[XDA Developers: Hometown Minecraft Map: Arnis](https://www.xda-developers.com/hometown-minecraft-map-arnis/)
|
||||||
|
|
||||||
Free to use assets, including screenshots and logos, can be found [here](https://drive.google.com/file/d/1T1IsZSyT8oa6qAO_40hVF5KR8eEVCJjo/view?usp=sharing).
|
|
||||||
|
|
||||||
## :copyright: License Information
|
## :copyright: License Information
|
||||||
Copyright (c) 2022-2025 Louis Erbkamm (louis-e)
|
Copyright (c) 2022-2025 Louis Erbkamm (louis-e)
|
||||||
|
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Analyze performance data from Windows Performance Monitor CSV exports."""
|
|
||||||
|
|
||||||
import csv
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def parse_pdh_csv(filepath):
|
|
||||||
"""Parse a Windows Performance Monitor CSV file."""
|
|
||||||
data = []
|
|
||||||
|
|
||||||
with open(filepath, 'r', encoding='utf-8-sig', errors='replace') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
header = next(reader)
|
|
||||||
|
|
||||||
# Clean up column names - extract the metric name
|
|
||||||
clean_cols = []
|
|
||||||
for col in header:
|
|
||||||
if 'Verfügbare MB' in col or 'Verf' in col:
|
|
||||||
clean_cols.append('available_mb')
|
|
||||||
elif 'Zugesicherte' in col:
|
|
||||||
clean_cols.append('committed_pct')
|
|
||||||
elif 'Bytes geschrieben' in col:
|
|
||||||
clean_cols.append('disk_write_bytes_sec')
|
|
||||||
elif 'Arbeitsseiten' in col and 'arnis-windows' not in col:
|
|
||||||
clean_cols.append('working_set')
|
|
||||||
elif 'Arbeitsseiten' in col and 'arnis-windows' in col:
|
|
||||||
clean_cols.append('gui_working_set')
|
|
||||||
elif 'Private Bytes' in col and 'arnis-windows' not in col:
|
|
||||||
clean_cols.append('private_bytes')
|
|
||||||
elif 'Private Bytes' in col and 'arnis-windows' in col:
|
|
||||||
clean_cols.append('gui_private_bytes')
|
|
||||||
elif 'Prozessorzeit' in col and 'arnis-windows' not in col and 'Prozessorinformationen' not in col:
|
|
||||||
clean_cols.append('cpu_pct')
|
|
||||||
elif 'Prozessorzeit' in col and 'arnis-windows' in col:
|
|
||||||
clean_cols.append('gui_cpu_pct')
|
|
||||||
elif 'Threadanzahl' in col and 'arnis-windows' not in col:
|
|
||||||
clean_cols.append('thread_count')
|
|
||||||
elif 'Threadanzahl' in col and 'arnis-windows' in col:
|
|
||||||
clean_cols.append('gui_thread_count')
|
|
||||||
elif 'PDH-CSV' in col:
|
|
||||||
clean_cols.append('timestamp')
|
|
||||||
else:
|
|
||||||
clean_cols.append(col[:30]) # truncate long names
|
|
||||||
|
|
||||||
for row in reader:
|
|
||||||
if not row or not row[0].strip():
|
|
||||||
continue
|
|
||||||
entry = {}
|
|
||||||
for i, val in enumerate(row):
|
|
||||||
if i >= len(clean_cols):
|
|
||||||
break
|
|
||||||
col_name = clean_cols[i]
|
|
||||||
if col_name == 'timestamp':
|
|
||||||
try:
|
|
||||||
entry[col_name] = datetime.strptime(val.strip(), '%m/%d/%Y %H:%M:%S.%f')
|
|
||||||
except:
|
|
||||||
entry[col_name] = val
|
|
||||||
elif val.strip() == '' or val.strip() == ' ':
|
|
||||||
entry[col_name] = None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
entry[col_name] = float(val)
|
|
||||||
except:
|
|
||||||
entry[col_name] = val
|
|
||||||
data.append(entry)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_run(data, name):
|
|
||||||
"""Analyze a single run's data."""
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f" {name}")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
# Time range
|
|
||||||
timestamps = [d.get('timestamp') for d in data if isinstance(d.get('timestamp'), datetime)]
|
|
||||||
if timestamps:
|
|
||||||
duration = (timestamps[-1] - timestamps[0]).total_seconds()
|
|
||||||
print(f"Duration: {duration:.1f}s ({duration/60:.1f} min)")
|
|
||||||
|
|
||||||
# Memory usage (working set) - prefer 'working_set' (arnis backend) over gui_working_set
|
|
||||||
working_sets = [d.get('working_set') for d in data if d.get('working_set') is not None]
|
|
||||||
gui_ws = [d.get('gui_working_set') for d in data if d.get('gui_working_set') is not None]
|
|
||||||
|
|
||||||
# Use GUI working set if backend working set not available (before scenario)
|
|
||||||
if working_sets:
|
|
||||||
max_ws = max(working_sets) / (1024**3) # GB
|
|
||||||
avg_ws = sum(working_sets) / len(working_sets) / (1024**3)
|
|
||||||
print(f"Backend Working Set: max={max_ws:.2f} GB, avg={avg_ws:.2f} GB")
|
|
||||||
|
|
||||||
if gui_ws:
|
|
||||||
max_gui_ws = max(gui_ws) / (1024**3)
|
|
||||||
print(f"GUI Working Set: max={max_gui_ws:.2f} GB")
|
|
||||||
# For before, we only have GUI data, so use that as the main metric
|
|
||||||
if not working_sets:
|
|
||||||
working_sets = gui_ws
|
|
||||||
max_ws = max_gui_ws
|
|
||||||
|
|
||||||
# Private bytes
|
|
||||||
private = [d.get('private_bytes') for d in data if d.get('private_bytes') is not None]
|
|
||||||
if private:
|
|
||||||
max_private = max(private) / (1024**3)
|
|
||||||
avg_private = sum(private) / len(private) / (1024**3)
|
|
||||||
print(f"Private Bytes: max={max_private:.2f} GB, avg={avg_private:.2f} GB")
|
|
||||||
|
|
||||||
# Available system memory
|
|
||||||
avail = [d.get('available_mb') for d in data if d.get('available_mb') is not None]
|
|
||||||
if avail:
|
|
||||||
min_avail = min(avail) / 1024 # GB
|
|
||||||
max_avail = max(avail) / 1024
|
|
||||||
print(f"System Available Memory: min={min_avail:.2f} GB, max={max_avail:.2f} GB")
|
|
||||||
|
|
||||||
# CPU usage
|
|
||||||
cpu = [d.get('cpu_pct') for d in data if d.get('cpu_pct') is not None]
|
|
||||||
if cpu:
|
|
||||||
max_cpu = max(cpu)
|
|
||||||
avg_cpu = sum(cpu) / len(cpu)
|
|
||||||
print(f"CPU %: max={max_cpu:.1f}%, avg={avg_cpu:.1f}%")
|
|
||||||
|
|
||||||
# Thread count
|
|
||||||
threads = [d.get('thread_count') for d in data if d.get('thread_count') is not None]
|
|
||||||
if threads:
|
|
||||||
max_threads = max(threads)
|
|
||||||
print(f"Thread count: max={int(max_threads)}")
|
|
||||||
|
|
||||||
# Disk writes
|
|
||||||
disk = [d.get('disk_write_bytes_sec') for d in data if d.get('disk_write_bytes_sec') is not None]
|
|
||||||
if disk:
|
|
||||||
max_disk = max(disk) / (1024**2) # MB/s
|
|
||||||
avg_disk = sum(disk) / len(disk) / (1024**2)
|
|
||||||
print(f"Disk Write: max={max_disk:.1f} MB/s, avg={avg_disk:.1f} MB/s")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'duration': duration if timestamps else 0,
|
|
||||||
'max_working_set_gb': max(working_sets) / (1024**3) if working_sets else 0,
|
|
||||||
'max_private_bytes_gb': max(private) / (1024**3) if private else 0,
|
|
||||||
'avg_cpu': sum(cpu) / len(cpu) if cpu else 0,
|
|
||||||
'max_cpu': max(cpu) if cpu else 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Performance Analysis: BEFORE vs AFTER Parallel Processing")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
before_path = Path("arnis_before.csv")
|
|
||||||
after_path = Path("arnis_after.csv")
|
|
||||||
|
|
||||||
if before_path.exists():
|
|
||||||
before_data = parse_pdh_csv(before_path)
|
|
||||||
before_stats = analyze_run(before_data, "BEFORE (Sequential)")
|
|
||||||
else:
|
|
||||||
print("arnis_before.csv not found")
|
|
||||||
before_stats = None
|
|
||||||
|
|
||||||
if after_path.exists():
|
|
||||||
after_data = parse_pdh_csv(after_path)
|
|
||||||
after_stats = analyze_run(after_data, "AFTER (Parallel)")
|
|
||||||
else:
|
|
||||||
print("arnis_after.csv not found")
|
|
||||||
after_stats = None
|
|
||||||
|
|
||||||
# Comparison
|
|
||||||
if before_stats and after_stats:
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(" COMPARISON")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
time_diff = after_stats['duration'] - before_stats['duration']
|
|
||||||
time_ratio = after_stats['duration'] / before_stats['duration'] if before_stats['duration'] > 0 else 0
|
|
||||||
print(f"Duration: {before_stats['duration']:.1f}s -> {after_stats['duration']:.1f}s ({time_ratio:.2f}x, {time_diff:+.1f}s)")
|
|
||||||
|
|
||||||
mem_ratio = after_stats['max_working_set_gb'] / before_stats['max_working_set_gb'] if before_stats['max_working_set_gb'] > 0 else 0
|
|
||||||
print(f"Peak Memory: {before_stats['max_working_set_gb']:.2f} GB -> {after_stats['max_working_set_gb']:.2f} GB ({mem_ratio:.2f}x)")
|
|
||||||
|
|
||||||
cpu_diff = after_stats['avg_cpu'] - before_stats['avg_cpu']
|
|
||||||
print(f"Avg CPU: {before_stats['avg_cpu']:.1f}% -> {after_stats['avg_cpu']:.1f}% ({cpu_diff:+.1f}%)")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
212
arnis_after.csv
212
arnis_after.csv
@@ -1,212 +0,0 @@
|
|||||||
"(PDH-CSV 4.0) (Mitteleurop<6F>ische Zeit)(-60)","\\ROADRUNNER\Arbeitsspeicher\Verf<72>gbare MB","\\ROADRUNNER\Arbeitsspeicher\Zugesicherte verwendete Bytes (%)","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Aktuelle Warteschlangenl<6E>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Bytes geschrieben/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Mittlere Sek./Schreibvorg<72>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Schreibvorg<72>nge/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Zeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Arbeitsseiten","\\ROADRUNNER\Prozess(arnis)\Arbeitsseiten","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Bytes gelesen/s","\\ROADRUNNER\Prozess(arnis)\E/A-Bytes gelesen/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Datenbytes/s","\\ROADRUNNER\Prozess(arnis)\E/A-Datenbytes/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Schreibvorg<72>nge/s","\\ROADRUNNER\Prozess(arnis)\E/A-Schreibvorg<72>nge/s","\\ROADRUNNER\Prozess(arnis-windows)\Private Bytes","\\ROADRUNNER\Prozess(arnis)\Private Bytes","\\ROADRUNNER\Prozess(arnis-windows)\Prozessorzeit (%)","\\ROADRUNNER\Prozess(arnis)\Prozessorzeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Threadanzahl","\\ROADRUNNER\Prozess(arnis)\Threadanzahl","\\ROADRUNNER\Prozessorinformationen(0,0)\Prozessorzeit (%)"
|
|
||||||
"01/27/2026 21:49:44.151","12746","64.201036203699430871","0"," "," "," "," "," ","29315072"," "," "," "," "," "," "," ","8900608"," "," "," ","40"," "
|
|
||||||
"01/27/2026 21:49:45.152","12743","64.19952420149918737","0","116877.24273461557459","8.3499999999999996751e-005","23.231614550748048487","0.19385484677997560921"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","6.2790441242259076304"
|
|
||||||
"01/27/2026 21:49:46.171","12742","64.208226502921490919","0","142183.78738217015052","0.00030800000000000000554","4.9064082991307609305","0.15111770184909190107"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","3.4048780794156852103"
|
|
||||||
"01/27/2026 21:49:47.170","12742","64.206333752769580769","0","0","0","0","0"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","0"
|
|
||||||
"01/27/2026 21:49:48.167","12765","64.218516965447577149","0","92406.716692898364272","0.00028894000000000000846","5.013385237244920134","0.1788449123872541402"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","4.4122934340259050146"
|
|
||||||
"01/27/2026 21:49:49.168","12764","64.214459518951940709","0","150899.54270564959734","0.00011216363636363636347","10.989770721412510213","0.12326769080690583302"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","3.2131824694520805252"
|
|
||||||
"01/27/2026 21:49:50.169","12749","64.293889786185204116","0","169198.42306973855011","0.00023646999999999999075","9.9838571014526618086","0.2360349639579291392"," ","29315072"," ","223.63839907253964157"," ","223.63839907253964157"," ","0"," ","8900608"," ","0"," ","40","3.3032218318417294611"
|
|
||||||
"01/27/2026 21:49:51.167","12750","64.293280608089006023","0","57488.566498172956926","0.00017981249999999998645","8.0201683172674318456","0.14424488479661923268"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","1.2922777395397599953"
|
|
||||||
"01/27/2026 21:49:52.156","12749","64.287896073490358617","0","10873.380635370705932","0.00022130000000000001233","3.0338673647797729238","0.067124930729073398195"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","0.47280661781649024888"
|
|
||||||
"01/27/2026 21:49:53.166","12750","64.292388633892954886","0","198668.69878159218933","0.0001286857142857142811","13.858028653849903122","0.17838443271057205508"," ","29372416"," ","221.72845846159844996"," ","605.79382401115287848"," ","2.9695775686821219708"," ","8900608"," ","0"," ","40","0.98555022725796970207"
|
|
||||||
"01/27/2026 21:49:54.165","12733","64.284621822714015593","0","1296829.4491823313292","0.00012115833333333332839","36.026598437626496718","0.43636684508381734515"," ","33755136"," ","448.33100277935199074"," ","4211895.6425528964028"," ","5.0036942274481246429"," ","8019968"," ","1.5632051618485096611"," ","40","3.0812799653923916843"
|
|
||||||
"01/27/2026 21:49:55.162","12723","64.314361932760661489","0","0","0","0","0.073700618954755645063"," ","35278848"," ","503170.75050394039135"," ","508278.39024856238393"," ","6.0207934121281256878"," ","9277440"," ","3.1356628214242530106"," ","45","9.0622377543083878493"
|
|
||||||
"01/27/2026 21:49:56.153","12724","64.318321497253677421","0","1247689.9431102154776","0.00041058888888888885223","9.0778351743519287709","0.37273695420660279964"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","0.71209912904023342506"
|
|
||||||
"01/27/2026 21:49:57.169","12725","64.313622253088652769","0","1039800.1256299206289","0.0015873499999999999589","1.9678872290416073998","0.31244015098534727581"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","0.046609649890272386585"
|
|
||||||
"01/27/2026 21:49:58.170","12775","64.089679383693649584","0","0","0","0","0"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","1.5790544030494069183"
|
|
||||||
"01/27/2026 21:49:59.168","12790","64.034974543385899892","0","1050344.1493410007097","0.0025869999999999998899","1.0016862386140830132","0.25918488307926468295"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","0"
|
|
||||||
"01/27/2026 21:50:00.169","12762","64.097174248680744313","0","114080.13845965237124","6.3770000000000007776e-005","19.983208110224978782","0.31184487053797710354"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","6.3476419485044903723"
|
|
||||||
"01/27/2026 21:50:01.170","12773","64.067890998923189727","0","24040.855868055819883","0.00022316666666666665209","5.9942293553995567024","0.13379846034844156133"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","1.6371475687101066931"
|
|
||||||
"01/27/2026 21:50:02.158","12772","64.128752719640900182","0","0","0","0","0"," ","35655680"," ","226.61720206666794297"," ","339.92580310000192867"," ","1.0116839377976247771"," ","9707520"," ","0"," ","45","2.0117555548587517933"
|
|
||||||
"01/27/2026 21:50:03.170","12763","64.150530231220301403","0","76922.788000345550245","0.00013399166666666666647","11.861038445776966199","0.15892774821748678615"," ","43597824"," ","0"," ","0"," ","0"," ","18137088"," ","0"," ","45","1.160689787685253993"
|
|
||||||
"01/27/2026 21:50:04.169","12757","64.162452394180576221","0","0","0","0","0"," ","50364416"," ","0"," ","0"," ","0"," ","25014272"," ","3.1283388760824424324"," ","45","1.4574832398887460627"
|
|
||||||
"01/27/2026 21:50:05.170","12748","64.166335776486988607","0","0","0","0","0"," ","58380288"," ","0"," ","0"," ","0"," ","33206272"," ","4.6817582916311435426"," ","44","3.2442901147175629006"
|
|
||||||
"01/27/2026 21:50:06.170","12743","64.188428750305533299","0","151560.24487732132548","0.00031206666666666664457","3.0001632088785630259","0.093621591567056641758"," ","66879488"," ","0"," ","0"," ","0"," ","42012672"," ","1.5625265629515703303"," ","44","6.2484062229057890647"
|
|
||||||
"01/27/2026 21:50:07.170","12739","64.129046412214890438","0","0","0","0","0"," ","77299712"," ","0"," ","0"," ","0"," ","52727808"," ","3.1265858043199514782"," ","44","1.5125471639215404274"
|
|
||||||
"01/27/2026 21:50:08.168","12737","64.136737087773326493","0","221512.63614698767196","0.00034925714285714284327","7.0104006303752246509","0.24484697664860091693"," ","90259456"," ","0"," ","0"," ","0"," ","66203648"," ","6.2593815610837513219"," ","44","1.4147404129309038012"
|
|
||||||
"01/27/2026 21:50:09.169","12725","64.169577384407077147","0","0","0","0","0"," ","102637568"," ","0"," ","0"," ","0"," ","78856192"," ","3.1220552774622976067"," ","44","4.7759060488242743858"
|
|
||||||
"01/27/2026 21:50:10.168","12714","64.20401676190179785","0","98440.33987072094169","0.00028076666666666669691","3.0041607626562787381","0.084346626079880651639"," ","115486720"," ","0"," ","0"," ","0"," ","92315648"," ","9.3879807609982321992"," ","44","0"
|
|
||||||
"01/27/2026 21:50:11.168","12709","64.234637972953407825","0","4094.1564013724619144","0.00063150000000000000838","0.99954990267882370958","0.063123582780606313225"," ","127291392"," ","0"," ","0"," ","0"," ","104419328"," ","1.5618463672952869192"," ","44","3.1655252276922118959"
|
|
||||||
"01/27/2026 21:50:12.171","12693","64.269240494880193637","0","0","0","0","0"," ","139763712"," ","0"," ","0"," ","0"," ","117067776"," ","1.5584051346681460082"," ","44","3.3788816505749497132"
|
|
||||||
"01/27/2026 21:50:13.169","12678","64.303070694278702035","0","86179.525649920717115","8.6116666666666668293e-005","12.022813288214386773","0.103538348414056805"," ","151339008"," ","0"," ","0"," ","0"," ","129167360"," ","3.1309980529700749408"," ","44","7.6355574373827899137"
|
|
||||||
"01/27/2026 21:50:14.170","12675","64.328601086588719227","0","36319.403335506380245","0.0003498500000000000199","1.9982066095679127393","0.069903319223792420578"," ","161058816"," ","0"," ","0"," ","0"," ","139071488"," ","7.8050547582660900758"," ","44","9.4569052834805660268"
|
|
||||||
"01/27/2026 21:50:15.171","12663","64.339283146974466376","0","118633.95177779145888","0.00012265714285714286475","13.982315167775798415","0.21097823966660581019"," ","169115648"," ","0"," ","0"," ","0"," ","147320832"," ","0"," ","44","3.2452274803329350661"
|
|
||||||
"01/27/2026 21:50:16.169","12680","64.299111153068750468","0","176383.79177483185777","0.00023925714285714285291","7.0101661429404922288","0.16771888200363777033"," ","170295296"," ","0"," ","0"," ","0"," ","157147136"," ","17.212015073068570814"," ","43","4.5515527766197383386"
|
|
||||||
"01/27/2026 21:50:17.162","12513","64.808707070725205313","0","0","0","0","0"," ","316452864"," ","677.35714996334513671"," ","1016.0357249450175914"," ","3.0239158480506476145"," ","304222208"," ","100.79634149598905424"," ","43","5.5002579464959122646"
|
|
||||||
"01/27/2026 21:50:18.153","12047","66.17082400856791935","0","12386.270190436887788","0.00065830000000000000928","1.0079972485707102692","0.066365275768236281495"," ","801759232"," ","2192698.4308103630319"," ","2192924.2221940429881"," ","2.0159944971414205384"," ","816644096"," ","99.237913313622357236"," ","52","11.791482726705716289"
|
|
||||||
"01/27/2026 21:50:19.171","12056","66.073673676250891162","0","1027943.840953755076","3.7708670520231215542e-005","170.09431287206899697","0.64131760114815083984"," ","801427456"," ","0"," ","0"," ","0"," ","785248256"," ","1238.0577502390588052"," ","52","87.711305333657236361"
|
|
||||||
"01/27/2026 21:50:20.169","11683","67.189482824688198548","0","0","0","0","0"," ","1193062400"," ","0"," ","0"," ","0"," ","1207631872"," ","1199.2725857534383067"," ","52","84.344063870207534706"
|
|
||||||
"01/27/2026 21:50:21.167","12197","65.758976681567489209","0","98431.981262036904809","0.00028293333333333334396","3.0039056781627473391","0.084988666508998403359"," ","654458880"," ","1121.458119847425678"," ","1682.1871797711382897"," ","5.0065094636045781584"," ","660942848"," ","730.62167287093279811"," ","43","48.371487784280986943"
|
|
||||||
"01/27/2026 21:50:22.170","10914","69.570106284127135154","0","44927.0100614126859","0.00010571250000000000134","7.9770969569269674082","0.08433404178449116495"," ","1999941632"," ","9424.9400546092128934"," ","8602926.1387629974633"," ","944.28885227622981802"," ","2108280832"," ","1338.4426712501094698"," ","58","92.209297606227536903"
|
|
||||||
"01/27/2026 21:50:23.167","10679","70.1412165933617473","0","4277087.253197716549","0.0014359999999999999935","9.0255911611784060966","1.2959332323647190233"," ","2215051264"," ","60423.324293702367868"," ","41892262.32058378309"," ","17601.908451222596341"," ","2323849216"," ","501.36692678919808941"," ","58","76.497374223974631491"
|
|
||||||
"01/27/2026 21:50:24.169","10607","70.305211462617194229","0","9362936.4392447564751","0.0034966624999999999443","15.978143497509757154","5.5869539144657771601"," ","2273783808"," ","30410.40161163544326"," ","19318356.420252736658"," ","10537.585636607684137"," ","2381434880"," ","240.29356308843210854"," ","58","54.750532508216053884"
|
|
||||||
"01/27/2026 21:50:25.171","10556","70.377277413005302265","0","4086.1291378417158739","0.00061390000000000001425","0.99759012154338766454","0.061243817132885514098"," ","2304122880"," ","38135.875166360623552"," ","27797729.025991909206"," ","10224.301155698180082"," ","2411061248"," ","235.37568178514703732"," ","57","50.11922005215334508"
|
|
||||||
"01/27/2026 21:50:26.169","10646","70.013956299520557991","0","12642454.189482485875","0.0076883823529411768338","17.036079008123401479","13.097755286100811745"," ","2325094400"," ","9536.1957553707234183"," ","1859914.9278341671452"," ","7154.1510611172334393"," ","2431336448"," ","156.57881551257642627"," ","57","31.107005701033063616"
|
|
||||||
"01/27/2026 21:50:27.169","10639","70.032013573225597725","0","9942254.8753778282553","0.00032751532258064514937","248.13374408806345173","8.1984747810162996728"," ","2341912576"," ","14751.951301751643769"," ","9674004.2883113995194"," ","4919.6516922621294725"," ","2447740928"," ","129.76228200311840055"," ","57","17.140021232166034792"
|
|
||||||
"01/27/2026 21:50:28.169","10624","70.059763051117712962","1","0","0","0","0"," ","2350510080"," ","13887.055680213745291"," ","9623900.5747609175742"," ","4279.7089797893740979"," ","2456276992"," ","162.48695229773048254"," ","57","34.378845399659574866"
|
|
||||||
"01/27/2026 21:50:29.169","10544","70.400751864165243887","0","13819735.229708110914","0.001033360666666666627","149.95372428068696991","15.595066527935175671"," ","2350444544"," ","8909.250605263214311"," ","5178735.842119121924"," ","3445.9365839701863479"," ","2456023040"," ","148.39274474818395788"," ","57","35.958204712416474536"
|
|
||||||
"01/27/2026 21:50:30.168","10457","70.639075285438238438","0","8541511.430721545592","0.0019598948717948717554","39.043623440470035746","7.6521811037251517007"," ","2350706688"," ","3752.1923244841459564"," ","789298.88364269398153"," ","2816.1464804626207297"," ","2456023040"," ","115.75495809144926795"," ","57","60.893595239375251538"
|
|
||||||
"01/27/2026 21:50:31.169","10423","70.760374486157758156","0","8897102.315781397745","0.00084457578947368431264","94.918901290737196064","8.0169842597338760726"," ","2350759936"," ","3313.169228211416339"," ","735583.51744269696064"," ","2486.8752138173144886"," ","2456023040"," ","110.84782411029476634"," ","57","11.009493319904194664"
|
|
||||||
"01/27/2026 21:50:32.169","10472","70.549963710492008317","0","143323.28057551657548","0.00015364545454545454947","10.997182521837906677","0.16895169477013483039"," ","2350858240"," ","7158.1660778508548901"," ","4886054.1929157748818"," ","2300.410634795365695"," ","2456023040"," ","118.70903351253483038"," ","57","34.39580309083989107"
|
|
||||||
"01/27/2026 21:50:33.170","10485","70.514849915754723497","0","13130170.966100767255","0.0013322203252032521932","122.86999126224540646","16.369284307391883004"," ","2350968832"," ","3164.6514822666135842"," ","660591.02863260381855"," ","2373.4886116999600745"," ","2456023040"," ","120.18749298854261554"," ","57","11.032526158809774941"
|
|
||||||
"01/27/2026 21:50:34.170","10495","70.501317845308534515","0","10946851.342131813988","0.0013394725274725274335","91.019450856648063564","12.191989527203823229"," ","2351104000"," ","15475.307073121526628"," ","13320481.586915124208"," ","2395.5119208974956564"," ","2456023040"," ","123.46574896336281313"," ","57","14.042833000190446668"
|
|
||||||
"01/27/2026 21:50:35.170","10501","70.54300186469755829","0","11477459.974901933223","0.0006450450261780104038","190.93964397853838477","12.316214362245668568"," ","2351513600"," ","11372.405182721740857"," ","9044571.0111033897847"," ","2225.2965837498763904"," ","2456023040"," ","131.20583611556349979"," ","57","9.4054941106823441999"
|
|
||||||
"01/27/2026 21:50:36.170","10483","70.540445570494142657","0","221178.80229814600898","0.00033769999999999996852","4.9998825027611850658","0.16884581262384693034"," ","2351644672"," ","11019.741036085652013"," ","9037578.6169025041163"," ","2124.9500636735037915"," ","2456023040"," ","124.99690007687809157"," ","57","14.0646311971463156"
|
|
||||||
"01/27/2026 21:50:37.169","10492","70.53700815200269858","0","10640702.010342037305","0.000276337762237762255","429.63302129357396097","11.872376823823810099"," ","2351783936"," ","2687.9604409136418326"," ","536102.89400402549654"," ","2015.9703306852316018"," ","2456023040"," ","114.23075047237074386"," ","57","9.2409943281254740555"
|
|
||||||
"01/27/2026 21:50:38.171","10499","70.529273983680013771","0","8528471.2360408864915","0.0035371807692307693713","25.948857396956334753","9.1788181166449369641"," ","2351972352"," ","2495.0824420150324841"," ","544185.46486729301978"," ","1873.3078974648863095"," ","2456023040"," ","112.28162347051220138"," ","57","7.9917780205270076976"
|
|
||||||
"01/27/2026 21:50:39.170","10509","70.476189947332301244","0","9009261.7181815039366","0.00068547040816326532033","98.023476622651116941","6.7189840322345917301"," ","2352095232"," ","2660.6372226148159825"," ","558448.74847525975201"," ","1995.4779169611119869"," ","2456023040"," ","118.7742893421704764"," ","57","4.6672036476565814667"
|
|
||||||
"01/27/2026 21:50:40.169","10511","70.482466479410049942","0","0","0","0","0"," ","2352148480"," ","2757.217206397946029"," ","579702.91385359375272"," ","2068.9148005856682175"," ","2456023040"," ","118.97510089088557095"," ","57","9.2036309330229890691"
|
|
||||||
"01/27/2026 21:50:41.169","10518","70.45824145210400502","0","4077146.8798495628871","0.0015635999999999998424","10.993342431823286631","1.718978635097222929"," ","2352271360"," ","2842.2787160095845138"," ","583799.45105244254228"," ","2132.7084317737176207"," ","2456461312"," ","123.36706973988549407"," ","57","17.235065008139827114"
|
|
||||||
"01/27/2026 21:50:42.169","10523","70.45927484763303994","0","4434737.803733243607","0.00041770862068965519676","57.983915261906339822","2.4220562870081363549"," ","2352390144"," ","2931.1868887570585684"," ","605981.90062076773029"," ","2198.39016656779404"," ","2456461312"," ","118.7179580231295688"," ","57","7.8373746925704761424"
|
|
||||||
"01/27/2026 21:50:43.169","10519","70.462483812696873997","4","0","0","0","0"," ","2352590848"," ","7213.9232319336333603"," ","4890832.8960500871763"," ","2341.6242770322569413"," ","2456461312"," ","110.96830480141287012"," ","57","15.601570996108515388"
|
|
||||||
"01/27/2026 21:50:44.170","10523","70.457273319004400491","0","6311018.5268822452053","0.00017956057007125888407","420.66582307015306696","7.5532376106969705276"," ","2352734208"," ","7342.1673822315551661"," ","4809942.9812956592068"," ","2439.0624088224317347"," ","2456461312"," ","109.28439231033550527"," ","57","14.133691756164967757"
|
|
||||||
"01/27/2026 21:50:45.169","10527","70.461896404265871752","0","6365989.812155360356","0.00019072807486631015322","374.28820191547492868","7.1387539678202998061"," ","2352906240"," ","7601.853427138897132"," ","4920483.7725048288703"," ","2630.0251193418935145"," ","2456461312"," ","125.09679990376554315"," ","57","13.995950066161199743"
|
|
||||||
"01/27/2026 21:50:46.171","10519","70.459535920633825867","0","8449449.7441951278597","0.0034042045454545453304","21.966498892538986354","7.4777701273005678928"," ","2352979968"," ","7796.1101524065643389"," ","4952449.0199995981529"," ","2782.7560187957346898"," ","2456461312"," ","115.44776459179865924"," ","57","12.631664082228077461"
|
|
||||||
"01/27/2026 21:50:47.170","10515","70.461504806406210832","0","4098.3930517028893519","0.00060210000000000005269","1.0005842411384007207","0.060245388140994025894"," ","2353098752"," ","8144.7557228665818911"," ","5044815.6678684679791"," ","3037.7737560961845702"," ","2456461312"," ","126.6368865071342924"," ","57","17.141165339657938205"
|
|
||||||
"01/27/2026 21:50:48.170","10544","70.46187465788375448","0","5179045.8344612037763","0.00023416082949308757426","217.07132963891933741","5.0828458591174765502"," ","2353197056"," ","4557.4975937092931417"," ","921951.95341189112514"," ","3420.1238526979964263"," ","2456461312"," ","134.41614478191775106"," ","57","15.597197716927958311"
|
|
||||||
"01/27/2026 21:50:49.169","10533","70.457501749148946146","0","7572093.0642005652189","0.00013412629107981220017","426.15111318473532265","5.7159760780091710686"," ","2353258496"," ","13784.888121327821864"," ","9542735.8541338760406"," ","4195.4877199454922447"," ","2456461312"," ","132.86354617444021642"," ","57","23.409858617975231709"
|
|
||||||
"01/27/2026 21:50:50.169","10524","70.445514323759255149","0","10276717.642242111266","0.00040804894366197185396","284.22251780919282282","11.597713921542077031"," ","2353594368"," ","32421.382700516234763"," ","26776161.0564911291"," ","5874.5992237322598157"," ","2456461312"," ","153.24555828069944141"," ","57","28.068411419263540552"
|
|
||||||
"01/27/2026 21:50:51.169","10521","70.447124254528219467","0","0","0","0","0"," ","2353774592"," ","16620.970991367750685"," ","6341991.9715952128172"," ","9401.0243068206455064"," ","2456465408"," ","156.18357512549911803"," ","57","28.155555442270397748"
|
|
||||||
"01/27/2026 21:50:52.158","10514","70.521159090688726678","0","9258209.5270443074405","0.00012994450474898235808","745.01110440567424575","9.6810353437431633239"," ","2354065408"," ","14831.482936010925187"," ","5982249.1248393980786"," ","7851.4263879496220397"," ","2456465408"," ","151.63088110481089643"," ","57","30.5025128269616701"
|
|
||||||
"01/27/2026 21:50:53.170","10509","70.521322258403827732","0","11016937.085175897926","0.00033535093833780160342","368.84240837282158054","12.447882736800552905"," ","2354348032"," ","3540.0960374656870044"," ","596844.37025844678283"," ","2656.0608817410152369"," ","2456465408"," ","118.97084202036791112"," ","57","8.8405236467310803761"
|
|
||||||
"01/27/2026 21:50:54.169","10516","70.515502586615156133","0","11693554.042049037293","0.00023336273408239700767","534.16307998832041903","12.464908102389820499"," ","2354810880"," ","11659.559663565285518"," ","9007700.050825515762"," ","2600.7940224150434005"," ","2456465408"," ","115.65597266948088873"," ","57","9.3470789003012981766"
|
|
||||||
"01/27/2026 21:50:55.168","10514","70.503689225414703401","0","0","0","0","0"," ","2355269632"," ","7745.2284396858858599"," ","4854337.9436598708853"," ","2736.2602540927514383"," ","2456465408"," ","125.15336293093557174"," ","57","18.653588232773486766"
|
|
||||||
"01/27/2026 21:50:56.167","10510","70.501132907928237614","0","8521233.7350227460265","0.00012932806451612904441","620.70990591939994374","8.0276038548484951463"," ","2356051968"," ","3772.3143959746762448"," ","652540.31035295070615"," ","2830.2369419905544419"," ","2456469504"," ","126.70872186503223134"," ","57","17.09182396485544686"
|
|
||||||
"01/27/2026 21:50:57.168","10517","70.460721610686903205","0","10013080.507485311478","0.00032016716981132076593","264.84829489668317137","8.4795500189640868882"," ","2356363264"," ","4185.6024868955064449"," ","705965.62289120792411"," ","3140.2012926995416819"," ","2456469504"," ","115.55849608550279584"," ","57","15.673529883552017594"
|
|
||||||
"01/27/2026 21:50:58.169","10519","70.44155478254927516","4","4270160.7986819520593","0.0023528578947368422071","18.973060151890333458","4.4640802921923858904"," ","2356805632"," ","13085.420012124784989"," ","9217282.3807475771755"," ","3681.7722515799819121"," ","2456469504"," ","137.30469779785232731"," ","57","21.985967160311169266"
|
|
||||||
"01/27/2026 21:50:59.161","10528","70.463038601554700335","0","5220309.3795784646645","0.00029643981042654028767","212.58284937991294328","6.3018387818669596712"," ","2357317632"," ","5001.2382195350137408"," ","709248.92565060930792"," ","3247.1778367367746796"," ","2456469504"," ","132.23536504617808873"," ","57","19.714242650534718138"
|
|
||||||
"01/27/2026 21:51:00.153","10743","69.929794168549079814","0","7314739.5646521644667","0.00016994746716697935402","537.76540816444924076","9.1387627631384464877"," ","2156122112"," ","1243.0149772206407306"," ","176064.1928448950348"," ","763.76813129547485914"," ","2278170624"," ","110.34777077325159667"," ","57","16.447475288631409285"
|
|
||||||
"01/27/2026 21:51:01.170","10750","69.964277038808049269","0","5823810.7487898729742","0.00012140000000000000996","257.44239431645803506","3.1253856766094259001"," ","2158878720"," ","5989.9573883707180357"," ","4503428.0125230988488"," ","1313.742294660703692"," ","2292432896"," ","116.68559839322702487"," ","57","17.09198274805759965"
|
|
||||||
"01/27/2026 21:51:02.167","10847","69.624756735196513091","0","0","0","0","0"," ","2051203072"," ","1079.6855064763569771"," ","263821.55487757461378"," ","810.76755504916025075"," ","2168586240"," ","109.751006949810062"," ","57","12.202665547141954505"
|
|
||||||
"01/27/2026 21:51:03.168","10849","69.684247851763913673","0","5250893.5505910031497","0.00016317461928934008501","196.686462110749261","3.2095340896803254971"," ","2055299072"," ","5555.144544082279026"," ","4536028.1175777697936"," ","1100.2460976956633658"," ","2191577088"," ","102.9644064153140306"," ","57","15.756394751106695296"
|
|
||||||
"01/27/2026 21:51:04.169","10856","69.712802271757453809","0","8531379.4195607192814","0.0020068325000000002307","39.978048053813651563","8.0228925381371567482"," ","2060988416"," ","1731.0494807301311084"," ","393424.97034878149861"," ","1298.2871105475983313"," ","2203787264"," ","115.56108316490150401"," ","57","20.356550791757076269"
|
|
||||||
"01/27/2026 21:51:05.170","10849","69.724191415525083926","0","4538432.9638415165246","0.00018337375000000000593","79.929007055932927983","1.5253854076654620453"," ","2065584128"," ","1798.4026587584908157"," ","440072.12793596729171"," ","1350.8002192452665895"," ","2214973440"," ","115.52014236677368331"," ","57","17.262600737310741295"
|
|
||||||
"01/27/2026 21:51:06.170","10848","69.716261436631029369","0","0","0","0","0"," ","2069254144"," ","1792.3446678796333345"," ","412778.37728195131058"," ","1344.258500909724944"," ","2225627136"," ","115.64416223768279224"," ","57","18.73412877534982357"
|
|
||||||
"01/27/2026 21:51:07.169","10969","69.12894308779597452","0","3536749.0025888914242","5.1693461538461537089e-005","260.13982515602134526","1.3448517043913830182"," ","1943576576"," ","5603.0116187450748839"," ","4545725.3273634575307"," ","1131.6082394286929684"," ","2009141248"," ","125.07642169365482232"," ","57","15.575915837467901426"
|
|
||||||
"01/27/2026 21:51:08.167","10955","69.25419095653440138","0","3958417.8775537703186","0.00013134404761904762936","168.42010711518813082","2.2120007289864540567"," ","1958141952"," ","2125.3013516916598746"," ","423185.59414603788173"," ","1426.5584072911469775"," ","2031947776"," ","112.77636977176268829"," ","57","9.1523687949689467303"
|
|
||||||
"01/27/2026 21:51:09.170","10944","69.313736462340159505","0","3431093.3259299322963","0.00015316785714285715832","55.844617935057492275","0.85535799904426157436"," ","1965596672"," ","1822.9278854515196144"," ","424424.08243303827476"," ","1368.1931394089085643"," ","2053844992"," ","104.39677585858899533"," ","57","15.859314979644690169"
|
|
||||||
"01/27/2026 21:51:10.170","10942","69.351221637183627422","0","0","0","0","0"," ","1972744192"," ","2051.440572155972859"," ","529316.65534808661323"," ","1543.5790659887047696"," ","2068332544"," ","114.03177983851151112"," ","57","18.771882854758924708"
|
|
||||||
"01/27/2026 21:51:11.159","10933","69.405426101154972685","0","3214128.124581430573","3.3966014234875444445e-005","568.11720197223610285","1.9297162698709551254"," ","1980702720"," ","6421.1396208676933384"," ","4731661.1615570662543"," ","1544.6318231558304888"," ","2090156032"," ","124.7842212291939461"," ","57","30.499927416651473777"
|
|
||||||
"01/27/2026 21:51:12.153","10923","69.474990123294986688","0","4661357.2497754106298","6.2699632352941186411e-005","273.50850882959878163","1.7148467378917853221"," ","1989677056"," ","2043.2694483152376961"," ","446587.10652734088944"," ","1532.4520862364283857"," ","2116431872"," ","111.55005481601116912"," ","57","23.014750901626101154"
|
|
||||||
"01/27/2026 21:51:13.168","10920","69.55603029801417847","1","2316338.9945661542006","4.0819999999999998809e-005","275.85973163183047063","1.1260116110341964468"," ","1996836864"," ","6147.7311620807940926"," ","4630412.9836929459125"," ","1586.1934568830254193"," ","2144813056"," ","113.91045401923862812"," ","57","13.794954945795257828"
|
|
||||||
"01/27/2026 21:51:14.171","10908","69.643836531239529108","0","2255227.8631046907976","0.0017867749999999999952","3.9898024638825626553","0.71292974313840906664"," ","2006106112"," ","2146.5137255688186997"," ","471688.41158882016316"," ","1611.8801954085554371"," ","2178650112"," ","127.80585599698991928"," ","57","17.396239702272353611"
|
|
||||||
"01/27/2026 21:51:15.161","10899","69.732219276421758991","0","3647323.3959229663014","0.00010993148148148149026","109.03589552543944308","1.1987011227360666599"," ","2017054720"," ","2511.8639635860495218"," ","499335.94110850134166"," ","1548.7135531113342495"," ","2214453248"," ","135.66991507139039186"," ","57","30.58748531231189105"
|
|
||||||
"01/27/2026 21:51:16.154","11082","68.95247129000547659","0","3170305.795681017451","0.0028924333333333334013","3.0234392124948668013","0.87444063230039970058"," ","1825878016"," ","2326.0325674793839426"," ","580051.85198249435052"," ","1747.5478648220328068"," ","1922043904"," ","133.83961195002689237"," ","57","33.865966342941625555"
|
|
||||||
"01/27/2026 21:51:17.172","11065","69.280210851989735943","0","0","0","0","0"," ","1843060736"," ","3058.8487753659401278"," ","568397.12495701562148"," ","1968.2258647653111439"," ","2044796928"," ","138.04804336961504418"," ","57","34.045188825372662222"
|
|
||||||
"01/27/2026 21:51:18.168","11183","69.080406001089230017","0","2818655.3482180978172","3.3382317073170731465e-005","494.26124519677523494","1.6499834239157733506"," ","1725759488"," ","1679.6845568475775963"," ","424679.91059095360106"," ","1260.7680136625060641"," ","1968840704"," ","141.27344511306898767"," ","57","16.805637877859368245"
|
|
||||||
"01/27/2026 21:51:19.170","11276","68.489465319665484344","0","3847505.9075129828416","4.1715081206496520125e-005","430.23624461855314394","1.7947277178083345106"," ","1631940608"," ","11783.082671641303023"," ","9209048.0978167559952"," ","2706.1959609301570708"," ","1750941696"," ","143.49476585996487188"," ","57","39.170697081101849335"
|
|
||||||
"01/27/2026 21:51:20.170","11363","68.491999844203704129","0","2771359.9461277257651","3.621496598639455534e-005","440.74044795020216725","1.5960867552122313118"," ","1541435392"," ","4153.5539720658507576"," ","642452.65962874470279"," ","2785.3597016716857979"," ","1749553152"," ","251.40605001507560701"," ","57","57.837447582265568258"
|
|
||||||
"01/27/2026 21:51:21.169","11182","69.436612974255481845","0","0","0","0","0"," ","1721573376"," ","12682.119062800207757"," ","8935040.2686303406954"," ","3364.6846819076940847"," ","2103476224"," ","283.05272684927700766"," ","57","64.033075459033028665"
|
|
||||||
"01/27/2026 21:51:22.169","11094","69.772728502231814218","0","3809092.6258455528878","3.5215892857142855549e-005","560.4244094052426135","1.9735640549926740661"," ","1805754368"," ","23501.797911558423948"," ","17955458.668849918991"," ","5166.9129031415486679"," ","2228150272"," ","237.67752134068936698"," ","57","45.271623375499160602"
|
|
||||||
"01/27/2026 21:51:23.170","11022","69.865658197055026335","0","7655567.20818094071","0.00041408310502283107507","218.65312867666730767","9.0536797040737972964"," ","1877463040"," ","11146.317481946638509"," ","5353247.6079946765676"," ","5293.6022294232425338"," ","2266091520"," ","179.39542630643049392"," ","57","37.599703194236241188"
|
|
||||||
"01/27/2026 21:51:24.169","10978","69.928249500209531675","0","9695842.8733884748071","0.0024785974358974356864","39.068479230395034563","9.6838873997753580625"," ","1927356416"," ","10322.092563845908444"," ","5292950.4836076674983"," ","4665.1771224602480288"," ","2287968256"," ","183.14076150091423756"," ","57","40.520190627905428471"
|
|
||||||
"01/27/2026 21:51:25.167","10930","69.9812682741277996","0","0","0","0","0"," ","1966698496"," ","9749.3517372041005729"," ","5138615.3336995020509"," ","4236.802300750878203"," ","2305388544"," ","153.33423990377269774"," ","57","28.026785351290371295"
|
|
||||||
"01/27/2026 21:51:26.158","10897","70.023811671422748759","0","9472794.6024891883135","0.00044844450261780104865","192.80862199778803756","8.7297495675105949431"," ","1995489280"," ","26052.381761879129044"," ","22057618.282537512481"," ","3871.3144783325506069"," ","2318254080"," ","153.00035140237409337"," ","57","27.44313232464733332"
|
|
||||||
"01/27/2026 21:51:27.171","10887","70.062765332419147057","0","8428257.3178220055997","0.004643081818181817727","10.861074904785398942","5.0427131694315052712"," ","2007629824"," ","4664.3379863823838605"," ","877255.93165263789706"," ","3501.2156011244569527"," ","2335494144"," ","134.21607151002669411"," ","57","27.492467115272933853"
|
|
||||||
"01/27/2026 21:51:28.169","10865","70.088284851538091402","4","4611143.9571270849556","0.00099768703703703698858","54.084918730899389061","5.3961103206290186307"," ","2028404736"," ","9162.3858620419923682"," ","5182432.9379558842629"," ","3800.967899699317968"," ","2346729472"," ","143.97947448643648727"," ","57","28.010262756781756366"
|
|
||||||
"01/27/2026 21:51:29.170","10798","70.193963421088170662","0","2123532.2839591512457","8.2523821339950366384e-005","402.56555125708337073","3.4769670747013661227"," ","2060161024"," ","13209.744044227471022"," ","9382995.8708561733365"," ","3772.9282558263125793"," ","2354552832"," ","195.09798976025859929"," ","57","36.007859358635187164"
|
|
||||||
"01/27/2026 21:51:30.169","10794","70.203340139753038329","0","8483553.7887180037796","0.0039239888888888884919","18.018966764416223469","7.070866031376104921"," ","2081951744"," ","9878.3980017366284301"," ","5307519.6952312001958"," ","4337.5657216786385106"," ","2362077184"," ","150.16322742821446923"," ","57","21.789985714471626466"
|
|
||||||
"01/27/2026 21:51:31.168","10768","70.183542410420159285","0","9212289.5628119334579","0.00054239588235294124065","170.15843451838006217","9.2288864898797555725"," ","2103881728"," ","5368.9990750387687513"," ","959846.7132747300202"," ","4030.7530341500969371"," ","2367655936"," ","136.05774783753881252"," ","57","15.550363411182798856"
|
|
||||||
"01/27/2026 21:51:32.171","10758","70.185000046665081186","0","0","0","0","0"," ","2125774848"," ","4976.541369543786459"," ","857751.56630957254674"," ","3733.4029304770592717"," ","2374946816"," ","138.63467339789409039"," ","57","18.999966104601195838"
|
|
||||||
"01/27/2026 21:51:33.171","10740","70.264397671042104321","0","8407394.7506972104311","0.0046668999999999998679","10.997785046091717476","5.1325906845937092626"," ","2137690112"," ","11989.585297521080065"," ","8999945.410994226113"," ","2521.4921714766646801"," ","2380029952"," ","145.28421316369701799"," ","57","26.576795497916560151"
|
|
||||||
"01/27/2026 21:51:34.170","10745","70.275471352570562544","0","8719042.9648272171617","0.0010013129870129870453","77.133077698954011225","7.7233609704169436938"," ","2139545600"," ","6314.8950884961823249"," ","4559993.3565380349755"," ","1492.5751398888503445"," ","2382225408"," ","122.08446030855138531"," ","57","15.479989017156736253"
|
|
||||||
"01/27/2026 21:51:35.170","10737","70.274601124756642889","0","6066180.3613462727517","0.00046712727272727274687","142.90361151403379836","6.6753347125859248123"," ","2141454336"," ","2270.4685689502434798"," ","457058.7138974762056"," ","1702.8514267126824961"," ","2384289792"," ","121.7913415275047555"," ","57","18.805772314996826111"
|
|
||||||
"01/27/2026 21:51:36.168","10738","70.26768279500950598","0","0","0","0","0"," ","2144112640"," ","2457.3666430116732045"," ","455895.63046737771947"," ","1843.024982258755017"," ","2385657856"," ","123.70992161839586743"," ","57","15.438787754514216033"
|
|
||||||
"01/27/2026 21:51:37.168","10741","70.264136598041318393","0","7740229.4281174419448","0.0010103707692307691609","64.989835589713763397","7.0307161229876662389"," ","2148913152"," ","10898.295506582770031"," ","8922516.5184165183455"," ","2032.6820885213551264"," ","2387492864"," ","131.22361093184161973"," ","57","14.077270117266838412"
|
|
||||||
"01/27/2026 21:51:38.169","10730","70.266420969335925406","0","5991393.1992665911093","0.00042139046242774562054","345.70279930343883734","14.568267455385516485"," ","2153140224"," ","3049.3784493470966481"," ","541047.86115376616362"," ","2287.0338370103222587"," ","2388283392"," ","128.02093178455234579"," ","57","9.4512511014474664961"
|
|
||||||
"01/27/2026 21:51:39.169","10739","70.21686136029121883","0","7275000.8850614232942","0.00013430194003527336417","567.03935253106567416","7.6153997701855216107"," ","2156089344"," ","16041.113253259776684"," ","13263696.50053713657"," ","2817.1955133686278714"," ","2388910080"," ","146.88425370798358927"," ","57","28.120471589710149374"
|
|
||||||
"01/27/2026 21:51:40.170","10739","70.219276233161636469","0","0","0","0","0"," ","2159890432"," ","14936.049666661703668"," ","9651294.998871402815"," ","5065.9467360165135688"," ","2389733376"," ","138.95125562474680692"," ","57","20.376246776830463148"
|
|
||||||
"01/27/2026 21:51:41.169","10713","70.235157937331862854","0","9738187.2361161895096","0.00045125491803278688788","244.15271752481174872","11.017220416751676737"," ","2184790016"," ","6816.2635728648265285"," ","1298510.2181414475199"," ","5114.1989314316106174"," ","2397507584"," ","151.65335552529518282"," ","57","24.952292172133827108"
|
|
||||||
"01/27/2026 21:51:42.168","10692","70.238453934490323149","0","6191358.3524073306471","0.00013794331550802139204","374.63500633573909226","5.1679135145704551135"," ","2203250688"," ","14496.571689012876959"," ","9616176.4190302565694"," ","4720.0004006791514257"," ","2398257152"," ","153.38718471483306871"," ","57","24.874337304008420801"
|
|
||||||
"01/27/2026 21:51:43.170","10674","70.245350517855342787","3","7691683.0553319035098","0.0014850056603773586616","52.858619051622625307","7.8494315130361105304"," ","2220285952"," ","14166.10990583486273"," ","9550378.6023522876203"," ","4496.9719491276682675"," ","2400722944"," ","171.414255622312794"," ","57","36.107632595923575991"
|
|
||||||
"01/27/2026 21:51:44.168","10659","70.251572660694733941","0","1604540.3413351159543","0.00014607435897435897074","117.2194817576430097","1.7123349089709347659"," ","2238144512"," ","9529.8436793051314453"," ","5210695.5062659326941"," ","4073.6274600562092019"," ","2402811904"," ","145.5900966372582559"," ","57","26.424059510374718229"
|
|
||||||
"01/27/2026 21:51:45.167","10649","70.252442888508653596","0","9943580.3513102997094","0.00036639831081081082912","296.5658476372919381","10.866102965472784092"," ","2249609216"," ","13425.616075472000375"," ","9375880.1793822608888"," ","3916.4726297776155661"," ","2404974592"," ","156.54841258406776205"," ","57","23.291277833806802278"
|
|
||||||
"01/27/2026 21:51:46.169","10639","70.269031652777698582","0","9008396.8973790332675","0.00058435955882352944978","135.64940055931438678","7.9267280673931823642"," ","2262802432"," ","13664.682262225052909"," ","9445250.8047699909657"," ","4126.3350743667915594"," ","2408382464"," ","158.96265088360792106"," ","57","25.194046643008039865"
|
|
||||||
"01/27/2026 21:51:47.161","10606","70.336746440813115555","0","0","0","0","0"," ","2273886208"," ","9721.8962820907036075"," ","5216159.4052135786042"," ","4026.9223915340439817"," ","2410160128"," ","149.69955708596896216"," ","57","29.089683485593642587"
|
|
||||||
"01/27/2026 21:51:48.153","10609","70.345231185281932085","0","8536742.4601857867092","0.004004923809523809608","21.153928677414072013","8.4719061894694949189"," ","2274164736"," ","11979.167611038481482"," ","9165546.0121117327362"," ","2798.3625650407757348"," ","2410160128"," ","129.06291309575820492"," ","57","18.155225841714319301"
|
|
||||||
"01/27/2026 21:51:49.172","10623","70.335321424141369562","0","8790511.1008961647749","0.00071812195121951228737","120.81138102141602531","8.6757696652841715945"," ","2274590720"," ","4203.8431770053703076"," ","735204.04354710073676"," ","3152.8823827540281854"," ","2410160128"," ","139.65809635369961939"," ","57","12.521851734495847097"
|
|
||||||
"01/27/2026 21:51:50.168","10628","70.332667224652283267","0","7592319.3420785907656","0.00031383513513513517116","259.78395002599341979","8.1527646509353175475"," ","2275934208"," ","4710.214012826506405"," ","812151.83057913871016"," ","3533.6635364539570219"," ","2410766336"," ","126.94296074765681226"," ","57","9.0996446435542495124"
|
|
||||||
"01/27/2026 21:51:51.169","10616","70.332656351461224631","0","0","0","0","0"," ","2279432192"," ","13356.162433235424032"," ","9301091.4426543768495"," ","3880.560659087851036"," ","2411986944"," ","128.01422928739759755"," ","57","15.700650236477786237"
|
|
||||||
"01/27/2026 21:51:52.167","10612","70.345731561618336514","0","6618651.9876699792221","0.00052081891891891897092","74.178198285741828499","3.8634121618201175963"," ","2282975232"," ","4967.9344689748177188"," ","845738.71812254574616"," ","3560.5535177156079953"," ","2412523520"," ","122.17073870718849093"," ","57","7.5888002086651145106"
|
|
||||||
"01/27/2026 21:51:53.170","10608","70.345448742235419104","0","5985047.7535923402756","0.00016649228855721392149","400.95543091138961245","6.6755401380790608812"," ","2283057152"," ","7995.1709805614409561"," ","4822575.2270185705274"," ","2766.7919536024746776"," ","2414170112"," ","118.44039680275758997"," ","57","15.844981219093289937"
|
|
||||||
"01/27/2026 21:51:54.166","10824","69.66960622688513638","0","8589344.9906502962112","0.0018326512195121951246","41.15717100463248812","7.5424846783728112243"," ","2077839360"," ","2497.537596573796236"," ","452199.86082853202242"," ","1873.1531974303470633"," ","2123825152"," ","117.63377903088738208"," ","57","5.8886595081816590636"
|
|
||||||
"01/27/2026 21:51:55.156","11042","69.546425125921700783","0","0","0","0","0.15782502707108853057"," ","2036101120"," ","1955.7523160915986864"," ","341986.93121045309817"," ","1467.8244397113082869"," ","2109165568"," ","110.49166188248294418"," ","57","16.345889455455552053"
|
|
||||||
"01/27/2026 21:51:56.170","11047","69.609484185839420434","0","5081998.0377077050507","0.00024019839572192515126","184.28518437781971784","4.5038887948840429232"," ","2037841920"," ","5936.5451908662353162"," ","4457263.3788087414578"," ","1262.4027871015350684"," ","2129428480"," ","132.42528922422286541"," ","57","13.769579109808383066"
|
|
||||||
"01/27/2026 21:51:57.172","11051","69.662644381090686352","0","5601166.2441976014525","0.0001617604240282685484","282.47783971328999542","4.5694464893083424073"," ","2038771712"," ","5777.3206228287017439"," ","4453791.1670277491212"," ","1100.9648664443775488"," ","2150416384"," ","106.05565301584844917"," ","57","14.219692413651985774"
|
|
||||||
"01/27/2026 21:51:58.170","11153","69.217565159596873059","0","5608201.4268641658127","0.00099406376811594205321","69.161124672148702075","7.0715037672889806686"," ","1926578176"," ","1323.0823850324100022"," ","229492.6490244322049"," ","992.31178877430750163"," ","1982783488"," ","106.49704118644018536"," ","57","4.4658895239286566792"
|
|
||||||
"01/27/2026 21:51:59.169","11159","69.242551636240108337","0","0","0","0","0"," ","1927266304"," ","1337.704369136717105"," ","247286.06717819173355"," ","1003.2782768525377151"," ","2009829376"," ","107.94752075896532517"," ","57","1.4392201765968892779"
|
|
||||||
"01/27/2026 21:52:00.168","11161","69.250797054090256211","0","3183767.7654357752763","0.00097802222222222222683","9.0033249278958713546","0.88056861711551592808"," ","1927278592"," ","1568.5792763267475038"," ","294128.62169999378966"," ","1176.4344572450606847"," ","2014027776"," ","107.85519987362997085"," ","57","6.2128696751043710478"
|
|
||||||
"01/27/2026 21:52:01.168","11164","69.256529717067380147","0","3953744.280777621083","8.0767295597484269109e-005","159.0444211068151219","1.2845569783842929468"," ","1927331840"," ","1564.4369472393639171"," ","285472.73253419680987"," ","1173.3277104295229947"," ","2016792576"," ","110.96833810115830943"," ","57","6.2239396328239600109"
|
|
||||||
"01/27/2026 21:52:02.167","11171","69.208177544457882391","0","0","0","0","0"," ","1927405568"," ","1561.7754263046231245"," ","292495.50889451126568"," ","1171.331569728467457"," ","2019667968"," ","109.49760446772249622"," ","57","0"
|
|
||||||
"01/27/2026 21:52:03.170","11174","69.217837105788717622","0","3835545.6203418713994","3.8384448818897639254e-005","506.6001624311701903","1.944587440466063688"," ","1927528448"," ","9697.2046840171242366"," ","8676928.9100357890129"," ","1147.8283207840097475"," ","2023600128"," ","104.40066960034002363"," ","57","3.3904251459540013514"
|
|
||||||
"01/27/2026 21:52:04.171","11178","69.241355072995958153","0","5225551.4330876432359","0.0001444529411764705726","169.83617602460665807","2.4532940545207315708"," ","1927561216"," ","5566.630428288872281"," ","4492144.8770515955985"," ","1106.9322531486127446"," ","2030944256"," ","109.26784102830353618"," ","57","4.7808813896211947991"
|
|
||||||
"01/27/2026 21:52:05.169","11182","69.264949175823701921","0","4022810.2774396105669","0.00010643759398496240975","133.15339270840007657","1.417270269501807789"," ","1927696384"," ","5538.3802140065354251"," ","4468549.7693342734128"," ","1079.2432882680848252"," ","2040180736"," ","103.24521873269235073"," ","57","1.4477457551573036376"
|
|
||||||
"01/27/2026 21:52:06.159","11165","69.384845152819735858","0","24825.641687683000782","0.00015348000000000000097","5.0507897313808189921","0.077518886516047283419"," ","1876365312"," ","1438.4649154972571523"," ","250078.74181191221578"," ","910.15230959482357775"," ","1932718080"," ","105.75004472437711911"," ","57","5.298467411005558958"
|
|
||||||
"01/27/2026 21:52:07.152","11267","68.847902251604921275","0","5296760.8292625145987","0.0034185142857142856651","7.0499077066368229794","2.4100513612384948381"," ","1820516352"," ","1361.6393170532835484"," ","265481.39589783997508"," ","1021.2294877899626044"," ","1863147520"," ","102.287895065435535"," ","57","8.727724403149817789"
|
|
||||||
"01/27/2026 21:52:08.169","11271","68.867950180747541822","0","4083020.5788267301396","3.8631346153846156898e-005","511.63698647389236385","1.9765244976949747358"," ","1820717056"," ","1405.0338782398428066"," ","281079.58552681293804"," ","1053.7754086798820481"," ","1872273408"," ","103.00394177329224021"," ","57","9.2950363488919052202"
|
|
||||||
"01/27/2026 21:52:09.170","11273","68.879904963280978336","0","3373549.3049142681994","0.00023990238095238097674","41.929767639204328589","1.0061301052173297066"," ","1820778496"," ","1433.5987221404145657"," ","279585.69396261259681"," ","1075.1990416053110948"," ","1879023616"," ","106.09605462473949444"," ","57","4.825598057218972059"
|
|
||||||
"01/27/2026 21:52:10.169","11273","68.898473486513481134","0","0","0","0","0"," ","1820868608"," ","1433.7174501335148307"," ","311026.57873867102899"," ","1075.288087600136123"," ","1885958144"," ","107.91623988139798485"," ","57","1.4677809778540162888"
|
|
||||||
"01/27/2026 21:52:11.170","11276","68.876434925216358351","0","3155191.4725937340409","5.8902898550724636496e-005","206.81485933792069432","1.218205675128568144"," ","1820938240"," ","1426.7227977514528448"," ","278181.97149911400629"," ","1070.0420983135898041"," ","1890983936"," ","103.03329006851001282"," ","57","0.088930842656953501546"
|
|
||||||
"01/27/2026 21:52:12.170","11281","68.920990701047941229","0","3163592.8778261104599","5.8719323671497581415e-005","207.09694207858697723","1.2160476794658030553"," ","1821495296"," ","5310.4858384209646829"," ","4463004.1322342986241"," ","910.4261704904065482"," ","1917403136"," ","106.29874860611077736"," ","57","4.6437696327535800123"
|
|
||||||
"01/27/2026 21:52:13.170","11284","68.964578367062983943","0","3462856.7556923464872","0.0002172895833333333374","47.96731986497598399","1.0423006223683655147"," ","1821605888"," ","1319.1012962868396698"," ","277259.10337287205039"," ","989.32597221512969554"," ","1933033472"," ","110.86417443502867286"," ","57","4.7504980206091733663"
|
|
||||||
"01/27/2026 21:52:14.167","11283","68.994818853446005846","0","0","0","0","0"," ","1821683712"," ","1323.6434609907230424"," ","257075.62636914369068"," ","992.73259574304233865"," ","1945399296"," ","106.54333511022547043"," ","57","5.9911749027422249725"
|
|
||||||
"01/27/2026 21:52:15.169","11284","69.128747277224604773","0","2547943.0252250363119","4.598987341772151912e-005","236.64103920762596545","1.088325335346309819"," ","1821855744"," ","1377.9098485507333862"," ","225839.42417746523279"," ","866.68532503046139936"," ","1974722560"," ","109.21096513037417708"," ","57","6.3906013168221313947"
|
|
||||||
"01/27/2026 21:52:16.170","11378","68.564403003213087118","0","2744428.3456138232723","5.7699761336515509921e-005","418.39207631311705882","2.4140361219733184051"," ","1717841920"," ","1162.3111618817858925"," ","236884.8063763352111"," ","872.73192051948524295"," ","1761341440"," ","107.65267883300451501"," ","57","6.3889749278221552586"
|
|
||||||
"01/27/2026 21:52:17.168","11378","68.589280701379578886","0","0","0","0","0"," ","1718079488"," ","1350.7081698806107397"," ","274689.75295467412798"," ","1013.0311274104579979"," ","1775206400"," ","109.59593444424615427"," ","57","2.9293152065248295735"
|
|
||||||
"01/27/2026 21:52:18.168","11380","68.630181525049309244","0","3351713.1657754178159","0.00029478181818181817654","33.011672927547181189","0.97313120305118128162"," ","1718226944"," ","1336.4725767031220585"," ","261361.41739719163161"," ","1002.3544325273416007"," ","1791643648"," ","106.28835946893234166"," ","57","7.7792175196028212625"
|
|
||||||
"01/27/2026 21:52:19.169","11381","68.672616120584450528","0","2274787.5192818092182","4.2674324324324325937e-005","221.74767332252628194","0.94627052827319946271"," ","1718464512"," ","1302.5178649215058613"," ","255188.62086831394117"," ","976.88839869112939596"," ","1808556032"," ","106.12669139726551748"," ","57","3.2374284319049650982"
|
|
||||||
"01/27/2026 21:52:20.169","11380","68.680002207094801747","0","696272.86232722050045","4.2639743589743588027e-005","77.994719757472424249","0.33256585571887481434"," ","1718943744"," ","1311.9111836128693085"," ","260960.3329854568874"," ","983.93338770965203821"," ","1824301056"," ","110.92944652218248791"," ","57","1.5696460436972259345"
|
|
||||||
"01/27/2026 21:52:21.169","11387","68.684636188830396009","0","0","0","0","0"," ","1718800384"," ","1356.4346016463675824"," ","274109.82478786201682"," ","1017.3259512347756299"," ","1833156608"," ","103.15905280333038263"," ","57","3.0930110029320667664"
|
|
||||||
"01/27/2026 21:52:22.168","11388","68.697874566702608945","0","1975568.565649635857","4.4211764705882350965e-005","187.12280869934940597","0.82730569083367377914"," ","1718857728"," ","5495.6067667209990759"," ","4497437.6683417325839"," ","1048.688254101166649"," ","1839206400"," ","104.75659745167914139"," ","57","1.497527769316642221"
|
|
||||||
"01/27/2026 21:52:23.170","11389","68.720903030764517894","0","3082931.8350497144274","4.1107430340557274851e-005","322.42978292889023351","1.325413150336072432"," ","1718882304"," ","9559.0947409506279655"," ","8673398.0954681634903"," ","1038.1640069537022555"," ","1848311808"," ","112.3003075930380561"," ","57","3.2969573504394444896"
|
|
||||||
"01/27/2026 21:52:24.169","11393","68.737187205985463834","0","4635161.5854253908619","5.3479027355623097917e-005","329.47493812330475293","1.7620200389145388442"," ","1718923264"," ","1406.0267876143459489"," ","294844.01765144453384"," ","1055.5215342916815189"," ","1853861888"," ","107.96936869577808693"," ","57","4.5488189790947259894"
|
|
||||||
"01/27/2026 21:52:25.170","11392","68.763479047632657171","0","53170.105795010305883","0.00011891428571428570642","6.9897600015976593113","0.08311651379545409446"," ","1718984704"," ","1393.9578517471902614"," ","289031.56875177862821"," ","1045.4683888103927529"," ","1862746112"," ","107.65256059073035999"," ","57","3.265593785991438569"
|
|
||||||
"01/27/2026 21:52:26.170","11386","68.783820669349239552","0","2581663.6928031505086","0.00014103770491803277096","61.027981329439548119","0.8887668747299327654"," ","1719009280"," ","1396.6403596048787676"," ","281335.99255258537596"," ","1047.4802697036591326"," ","1871671296"," ","103.17455473864096405"," ","57","3.0815805480855384957"
|
|
||||||
"01/27/2026 21:52:27.170","11499","68.429397667858140153","0","2608781.8138606129214","3.9983050847457625844e-005","294.95814543916219463","1.1793517554843357953"," ","1719103488"," ","1363.8064758610753415"," ","300029.42582447547466"," ","1022.8548568958065061"," ","1883553792"," ","109.36125329046137722"," ","57","4.6943810656870592624"
|
|
||||||
"01/27/2026 21:52:28.161","11588","67.92617618756511888","0","1157020.7079952240456","3.6635329341317364785e-005","168.4766132770668321","0.61720598504485313374"," ","1616953344"," ","1154.1152430476913651"," ","200412.51549202989554"," ","697.10981900870172012"," ","1670135808"," ","111.91593626438501019"," ","57","10.156843831738360251"
|
|
||||||
"01/27/2026 21:52:29.154","11521","68.474540852120739487","0","144337.32244400057243","0.00014359999999999999393","12.081807124218798322","0.17350364184466429696"," ","1618296832"," ","708.79935128750287276"," ","164595.49253953443258"," ","531.59951346562706931"," ","1778860032"," ","103.83335112134986389"," ","57","8.75250962063195459"
|
|
||||||
"01/27/2026 21:52:30.169","11383","68.932401591197674406","0","3250164.1930545722134","3.9614784946236560189e-005","366.57054044026898509","1.4521451429747143091"," ","1618800640"," ","5317.2436457411058655"," ","4412382.9893531948328"," ","961.75496631640464784"," ","1806462976"," ","101.61872594900312095"," ","57","53.809670023180402154"
|
|
||||||
"01/27/2026 21:52:31.168","11315","69.245673592492394732","0","4198254.5575386434793","0.0028305249999999999681","4.0037675452600893777","1.1332664309669473468"," ","1618788352"," ","5653.3197739072465993"," ","4508249.2625560648739"," ","998.94000254239233527"," ","1834594304"," ","112.60497035336341298"," ","57","10.854398470253967091"
|
|
||||||
"01/27/2026 21:52:32.170","11490","68.264358506599521093","0","159474.66324125183746","0.00022950999999999998962","9.9831394757394242845","0.22911786477443854548"," ","1523314688"," ","1253.8823181528716759"," ","273488.10593788151164"," ","940.4117386146537001"," ","1588318208"," ","107.62829417452030611"," ","57","40.726446686496068139"
|
|
||||||
"01/27/2026 21:52:33.170","11494","68.365566285412199932","0","6533100.7998062018305","0.00024039172932330829871","265.83255207544766563","6.3903525406079033644"," ","1523625984"," ","1427.1010690366138078"," ","297048.88890487881145"," ","1070.3258017774603559"," ","1626517504"," ","104.62090879155417156"," ","57","4.748127816644709398"
|
|
||||||
"01/27/2026 21:52:34.170","11499","68.427711601468672598","0","5296768.9090379942209","0.00028078372093023255644","172.02081451855676164","4.8299832842582199888"," ","1523752960"," ","1524.1844263155842327"," ","323104.09559556707973"," ","1143.1383197366881177"," ","1649709056"," ","109.38639806267813981"," ","57","0"
|
|
||||||
"01/27/2026 21:52:35.170","11504","68.592304752346194618","0","5870508.5471239397302","0.00026199523809523807966","231.01686423108887425","6.0525191236058635269"," ","1524805632"," ","1416.1033755464147816"," ","266127.42730219307123"," ","895.06533976980313128"," ","1711206400"," ","104.69492287003147624"," ","57","6.2456873016158764855"
|
|
||||||
"01/27/2026 21:52:36.169","11588","68.239176219384916067","0","0","0","0","0"," ","1433886720"," ","913.58140941970555104"," ","218970.03713427943876"," ","685.18605706477910644"," ","1577254912"," ","106.43714843809870274"," ","57","7.6501212081202414339"
|
|
||||||
"01/27/2026 21:52:37.169","11591","68.399885987956054123","0","4772604.4457504795864","0.00030543749999999999069","87.957472562016263851","2.6865164132524612661"," ","1434279936"," ","1383.3311593844375693"," ","330937.99148111883551"," ","1037.4983695383282338"," ","1642655744"," ","107.75900842821624792"," ","57","3.1730648905883174216"
|
|
||||||
"01/27/2026 21:52:38.170","11595","68.40927360309503058","0","5460453.6617471762002","0.00090117777777777769745","44.936581003230138265","4.0495751156589934183"," ","1434284032"," ","1829.4181421759469686"," ","442200.92183900863165"," ","1372.0636066319602833"," ","1642659840"," ","113.90147748128471505"," ","57","4.8220530635840086475"
|
|
||||||
"01/27/2026 21:52:39.170","11604","68.392782744111684679","0","4308829.0992017518729","0.0015428000000000002042","11.010049973615917196","1.6986344203580698853"," ","1434284032"," ","1833.6737774240327781"," ","452406.9570704139187"," ","1376.2562467019895394"," ","1642659840"," ","106.347318431096312"," ","57","6.164130796091504827"
|
|
||||||
"01/27/2026 21:52:40.168","11605","68.352186486206989002","0","0","0","0","0"," ","1434284032"," ","1837.9701201718121411"," ","438786.33507256431039"," ","1378.4775901288592195"," ","1642659840"," ","104.80615104359645784"," ","57","1.4509326007973610828"
|
|
||||||
"01/27/2026 21:52:41.168","11607","68.329604032526162882","0","3041500.0465847379528","0.0019922285714285715291","7.0052237953842180218","1.3954790625105695234"," ","1434284032"," ","10027.47748996426526"," ","8800582.5944406744093"," ","1374.024610151790057"," ","1642659840"," ","114.13767107016681734"," ","57","4.6228151932640741961"
|
|
||||||
"01/27/2026 21:52:42.169","11607","68.328374826425772426","0","0","0","0","0"," ","1434284032"," ","5921.8115232742065928"," ","4551605.9619424780831"," ","1375.6332230397599687"," ","1642659840"," ","109.18892024212337333"," ","57","3.2912906257689944489"
|
|
||||||
"01/27/2026 21:52:43.170","11606","68.328940465191607245","0","0","0","0","0"," ","1434284032"," ","2047.0198868781626516"," ","372783.51125481119379"," ","1368.3448364922874134"," ","1643044864"," ","110.88623729249967198"," ","57","7.8554946262074150098"
|
|
||||||
"01/27/2026 21:52:44.170","11672","67.840164799205993518","0","0","0","0","0"," ","1347993600"," ","1407.780386259743409"," ","267300.30115302011836"," ","1055.8352896948076705"," ","1435299840"," ","118.73005335103702862"," ","57","10.952459986722228535"
|
|
||||||
"01/27/2026 21:52:45.170","11674","68.021477262494499882","0","2239769.5164053118788","9.0798701298701301431e-005","307.89793183559652334","2.7956872081265466967"," ","1348235264"," ","5762.0898672090206674"," ","4531607.7720235744491"," ","1252.5847681493585242"," ","1500983296"," ","110.90129072857712345"," ","57","1.5946293535160749322"
|
|
||||||
"01/27/2026 21:52:46.170","11679","68.202604811685759501","0","4129973.9523940989748","6.5709137055837562474e-005","197.05754080191414346","1.2948548308557303876"," ","1348526080"," ","1716.5012183557598746"," ","343563.32048958295491"," ","1288.3762058521087965"," ","1570168832"," ","106.28158688762299278"," ","57","1.5332356776433964107"
|
|
||||||
"01/27/2026 21:52:47.170","11751","67.47875820553832682","0","0","0","0","0"," ","1267175424"," ","1779.6809032140536146"," ","366958.20439395215362"," ","1335.7604981426829909"," ","1300090880"," ","104.66886894132845498"," ","57","7.8289064546510740428"
|
|
||||||
"01/27/2026 21:52:48.170","11753","67.831114415971356379","0","3665705.5562249608338","7.063435897435897475e-005","194.98859316729971169","1.3775355797766892785"," ","1267761152"," ","1599.9064054752798256"," ","315235.55871981492965"," ","1199.9298041064598692"," ","1445130240"," ","104.70008495021102135"," ","57","3.113354225177866752"
|
|
||||||
"01/27/2026 21:52:49.169","11770","67.858755092103677953","0","3424651.4303509052843","8.5942281879194625774e-005","149.19567012136417361","1.282036136170911389"," ","1267572736"," ","2371.1097103851702741"," ","394777.75102046335815"," ","1779.3335960111687655"," ","1453629440"," ","109.50278975564484085"," ","57","4.5761403557952107235"
|
|
||||||
"01/27/2026 21:52:50.171","11765","68.094989975936471183","0","3606016.7355129374191","5.3705952380952382756e-005","251.5357655910252106","1.3508657722180332783"," ","1267998720"," ","6420.1509693709285784"," ","4584962.9923014082015"," ","1583.078270743515759"," ","1527201792"," ","109.17100306367521512"," ","57","11.10361179100732798"
|
|
||||||
"01/27/2026 21:52:51.169","11831","67.625185327517144174","0","65652.283324223870295","0.00017369999999999999373","1.0017743427158183334","0.017400438587220623532"," ","1190428672"," ","2007.5557828024998344"," ","366000.25965990964323"," ","1506.6686114445906242"," ","1349922816"," ","108.00142689606929025"," ","57","1.3900015296758660988"
|
|
||||||
"01/27/2026 21:52:52.169","11904","67.192724432608287088","0","3145536.1222965396009","0.0026756333333333333357","2.9998170111623188028","0.80264697812197272064"," ","1116168192"," ","2003.8777634564289656"," ","337459.41497568646446"," ","1335.9185089709526437"," ","1189756928"," ","104.68188905074688932"," ","57","4.6926084761856774463"
|
|
||||||
"01/27/2026 21:52:53.169","12032","67.104200266093059213","0","1936818.0452234249096","4.6982485875706210391e-005","176.94610221726460964","0.83134483640774337054"," ","983527424"," ","1403.5724718250821752"," ","95648.865355612681014"," ","885.73020657907602526"," ","1157603328"," ","106.21867611241444251"," ","57","10.963756788123180996"
|
|
||||||
"01/27/2026 21:52:54.169","12875","64.284719727999700467","0","1425976.3941907244734","5.3624271844660192693e-005","103.04107217136750307","0.63337741194332253247"," ","89731072"," ","3822.5236579300508311"," ","68664.369617729622405"," ","587.23407150090031337"," ","64794624"," ","106.29487769736383029"," ","42","7.7735619978755003956"
|
|
||||||
"01/27/2026 21:52:55.169","12799","64.438728723777160212","0","126998.7200710206962","0.00029823333333333333639","3.000536796032810205","0.089485964296030409693"," ","172609536"," ","11118068.022369202226"," ","11118180.042409587651"," ","1.000178932010936661"," ","150069248"," ","1358.0547769722118119"," ","43","95.311663600786374673"
|
|
||||||
"01/27/2026 21:52:56.169","12800","64.459168250779427467","0","1224374.7656255231705","3.7970414201183433486e-005","168.95456811663342478","0.64159721612597653273"," ","178470912"," ","11192136.434512758628"," ","11192136.434512758628"," ","0"," ","160227328"," ","1399.7757559239009879"," ","43","100"
|
|
||||||
"01/27/2026 21:52:57.170","12800","64.477943457774330227","0","2541887.4526651543565","9.3185714285714285566e-005","90.956468234303059717","0.84748176518541828983"," ","183078912"," ","11570004.595800448209"," ","11570004.595800448209"," ","0"," ","164810752"," ","1397.5993596640093983"," ","43","98.438436469649147398"
|
|
||||||
"01/27/2026 21:52:58.169","12811","64.445549148238626458","0","2273558.7383013158105","0.0015530999999999999667","4.0004904601304120959","0.69753681219245655676"," ","182304768"," ","12124985.523225147277"," ","12124985.523225147277"," ","0"," ","163053568"," ","1400.2545662801496746"," ","43","100"
|
|
||||||
"01/27/2026 21:52:59.168","12819","64.354555696331559034","0","36923.095414210445597","0.00011594285714285714796","7.0112214599466442522","0.081283217228997436954"," ","180166656"," ","13016855.477191245183"," ","13016855.477191245183"," ","0"," ","162537472"," ","1402.1254820181914056"," ","43","96.869869089411366758"
|
|
||||||
"01/27/2026 21:53:00.169","12858","64.220801313459134008","0","1414978.3532321983948","5.0420720720720715456e-005","110.82474175339118005","0.55878036577964218523"," ","176037888"," ","12653208.216526383534"," ","12653208.216526383534"," ","0"," ","151994368"," ","1394.6545704583732004"," ","43","98.440041017187112971"
|
|
||||||
"01/27/2026 21:53:01.168","12859","64.260842782505989135","0","2083708.9468075239565","0.0001200519230769230781","52.073496533006682796","0.62518433208567525394"," ","184410112"," ","12455951.329706747085"," ","12455951.329706747085"," ","0"," ","167632896"," ","1402.0504988545746983"," ","43","98.435347037523641234"
|
|
||||||
"01/27/2026 21:53:02.169","12870","64.180302984123187571","0","0","0","0","0"," ","164106240"," ","8933820.4769572746009"," ","8933820.4769572746009"," ","0"," ","137252864"," ","808.64043103245023758"," ","43","71.900525562578948779"
|
|
||||||
"01/27/2026 21:53:03.164","12691","64.694032483895782093","0","4990849.4276560340077","0.00081619999999999999843","19.085666011894389271","1.5578011656336272495"," ","70488064"," ","20955554.004147615284"," ","69340084.971394106746"," ","6.0270524248087538055"," ","44609536"," ","69.061266032133431736"," ","42","13.673417459833213883"
|
|
||||||
"01/27/2026 21:53:04.169","12761","64.504017742468050756","0","3091323.2170643433928","5.8458910891089107121e-005","201.12526599314261944","1.1759938590627820876"," ","70488064"," ","0"," ","0"," ","0"," ","44609536"," ","0"," ","42","3.5250238411450807163"
|
|
||||||
"01/27/2026 21:53:05.169","12778","64.441295891171620269","0","6685720.9896232718602","0.00028733437500000000298","64.010043175774285373","1.8388333476658353938"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0.005799663619510120327"
|
|
||||||
"01/27/2026 21:53:06.170","12774","64.435378337380328162","0","0","0","0","0"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","1.7022989903758833918"
|
|
||||||
"01/27/2026 21:53:07.167","12784","64.423793383041356719","0","4609387.847764544189","0.0018197631578947368481","38.104589477197009728","6.9329905821973598634"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
|
||||||
"01/27/2026 21:53:08.170","12788","64.420519155548078061","0","4185490.194747899659","0.0028377250000000001431","3.9915945003012653913","1.1324953399266939336"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0.22858628596024166413"
|
|
||||||
"01/27/2026 21:53:09.169","12792","64.407737637964942223","0","4194958.8330738423392","0.0028811250000000000054","4.0006244974840567963","1.1528838301852986081"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
|
||||||
"01/27/2026 21:53:10.169","12792","64.413165711893967114","0","0","0","0","0"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
|
||||||
"01/27/2026 21:53:11.168","12874","64.189027031927608391","0","3149801.0076830349863","0.0027905000000000000117","3.0038843228178357947","0.83821209855007272616"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
|
||||||
"01/27/2026 21:53:12.168","12878","64.19145280127213482","0","3163343.2505624974146","0.0010999000000000000096","8.9947659456961996938","0.98911751904374212163"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0.080055859245575788918"
|
|
||||||
"01/27/2026 21:53:13.170","12881","64.188526655591218173","0","2093272.7469453609083","0.0019350666666666665756","2.9944506839924254216","0.57961608870965730667"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","1.7157742500570338784"
|
|
||||||
"01/27/2026 21:53:14.160","12866","64.376485502435016883","0","0","0","0","0"," ","70426624"," ","452.55249709982535933"," ","452.55249709982535933"," ","0"," ","44494848"," ","0"," ","41","5.3241936928644095772"
|
|
||||||
|
196
arnis_before.csv
196
arnis_before.csv
@@ -1,196 +0,0 @@
|
|||||||
"(PDH-CSV 4.0) (Mitteleurop<6F>ische Zeit)(-60)","\\ROADRUNNER\Arbeitsspeicher\Verf<72>gbare MB","\\ROADRUNNER\Arbeitsspeicher\Zugesicherte verwendete Bytes (%)","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Aktuelle Warteschlangenl<6E>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Bytes geschrieben/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Mittlere Sek./Schreibvorg<72>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Schreibvorg<72>nge/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Zeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Arbeitsseiten","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Bytes gelesen/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Datenbytes/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Schreibvorg<72>nge/s","\\ROADRUNNER\Prozess(arnis-windows)\Private Bytes","\\ROADRUNNER\Prozess(arnis-windows)\Prozessorzeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Threadanzahl","\\ROADRUNNER\Prozessorinformationen(0,0)\Prozessorzeit (%)"
|
|
||||||
"01/27/2026 21:31:33.091","11421","64.050442880031283721","0"," "," "," "," ","31358976"," "," "," ","8216576"," ","39"," "
|
|
||||||
"01/27/2026 21:31:34.097","11408","64.115242395576842682","0","246727.30288993721479","4.1506060606060607952e-005","33.199051553398710723","0.56725460153538675989","31358976","225.35113781700945879","225.35113781700945879","0","8216576","0","39","8.3468561592205166022"
|
|
||||||
"01/27/2026 21:31:35.101","11387","64.1610600203650705","0","191824.80681207642192","0.00085968260869565221814","22.91789891891288633","16.690626749736260592","31358976","0","0","0","8216576","0","39","15.925976596382607653"
|
|
||||||
"01/27/2026 21:31:36.096","11388","64.180194205646444061","0","177491.21235621796222","7.215000000000000547e-005","18.086740389560254982","0.67737982265856211406","31420416","225.07943595897205569","614.94917324504865519","3.0144567315933756824","8216576","1.5700557362250091575","39","10.506823035174473802"
|
|
||||||
"01/27/2026 21:31:37.107","11380","64.110825970794721229","0","2007210.325699219713","0.0020881878787878788671","65.289416110816318906","16.830145160593072973","35909632","443.17664269160161439","4163470.4529779683799","4.9461678871830541127","9101312","0","39","2.6226761380818586211"
|
|
||||||
"01/27/2026 21:31:38.099","11384","64.081042344700790636","0","0","0","0","0.27008660793354716256","37519360","505413.64501628652215","510546.31371673452668","6.0479206996637859817","9785344","6.2996018046897663822","44","10.227190600185853242"
|
|
||||||
"01/27/2026 21:31:39.096","11399","64.048648035165072656","0","1051167.1269679761026","0.0025612999999999998615","1.0024710912399064089","1.5267090626088362093","37535744","449.10704887547808539","449.10704887547808539","0","9785344","0","44","2.8845323906524944491"
|
|
||||||
"01/27/2026 21:31:40.091","11400","64.019734636885047507","0","1346647.1964355160017","0.00041656249999999999485","8.0433342677006649524","0.33505285238813004023","37588992","337.82003924342791379","563.03339873904656088","2.0108335669251662381","9785344","0","44","7.313773954896507945"
|
|
||||||
"01/27/2026 21:31:41.097","11403","63.998490144498298093","0","1042045.915068630944","0.0025485999999999998239","0.99377242571700186158","0.67652123119748863722","37588992","222.60502336060841344","222.60502336060841344","0","9785344","0","44","3.7218092488283671671"
|
|
||||||
"01/27/2026 21:31:42.100","11412","63.961516242465357607","0","236949.53801083788858","0.00042709999999999997316","3.9895867795467050421","0.26616860044348800152","37588992","558.5421491365386828","558.5421491365386828","0","9785344","0","44","1.8269683300157657513"
|
|
||||||
"01/27/2026 21:31:43.091","11421","63.936007619820536263","0","143113.12005148778553","7.3613333333333335522e-005","30.272687285259429757","0.2228481290605907883","37588992","0","0","0","9785344","0","44","0"
|
|
||||||
"01/27/2026 21:31:44.105","11422","63.933005315236044908","0","82291.606682091078255","8.2508333333333337602e-005","11.832573813567544008","0.097629614875278436514","37588992","331.3120667798912109","331.3120667798912109","0","9785344","0","44","0"
|
|
||||||
"01/27/2026 21:31:45.106","11422","63.840934975035700916","0","98184.17603157107078","6.2114999999999997364e-005","19.975621751214816868","0.12407633146348247266","37588992","0","0","0","9785344","0","44","0.12369680151133044532"
|
|
||||||
"01/27/2026 21:31:46.107","11450","63.83411452729117741","0","36814.910997675695398","0.0002331333333333333261","2.9960051267639729033","0.069862655054589065107","37588992","0","0","0","9785344","0","44","1.671359637745051252"
|
|
||||||
"01/27/2026 21:31:47.107","11449","63.839118360504301108","0","106591.7407014980854","9.9259999999999994937e-005","10.008990074885261379","0.099326658120264504914","37588992","0","0","0","9785344","0","44","3.0557818282253568221"
|
|
||||||
"01/27/2026 21:31:48.107","11449","63.84364356376315186","0","90035.649768995892373","0.0002737999999999999874","2.9974581554841495112","0.082071404719935084349","37564416","0","0","0","9728000","0","43","1.6489210930355491236"
|
|
||||||
"01/27/2026 21:31:49.100","11449","63.901600885182062939","0","41257.088166175810329","0.00010826249999999999556","8.058025032456214376","0.087241190009720331888","38756352","225.62470090877397411","338.43705136316094695","1.007253129057026797","10551296","0","43","5.5667756216224333343"
|
|
||||||
"01/27/2026 21:31:50.091","11434","63.934506467528294138","0","24816.266125372632814","0.00033240000000000000232","2.0195529073382676444","0.067127112073095851486","48848896","0","0","0","21000192","1.5777092751836985229","43","5.3374434889780992819"
|
|
||||||
"01/27/2026 21:31:51.105","11431","63.957654583258005232","0","0","0","0","0","60133376","0","0","0","32718848","1.5396153119644908625","43","3.0042353462370718908"
|
|
||||||
"01/27/2026 21:31:52.108","11416","63.982249462041593802","0","0","0","0","0","71974912","0","0","0","45096960","1.5586280561828305125","43","3.3614648317033091196"
|
|
||||||
"01/27/2026 21:31:53.104","11401","64.011695879514263652","0","4110.6495328050104945","0.0006143999999999999722","1.0035765460949732653","0.061661531383802592465","84168704","0","0","0","57466880","9.4088030065616745645","43","7.483567074376241024"
|
|
||||||
"01/27/2026 21:31:54.106","11395","64.010912683794956024","0","0","0","0","0","96387072","0","0","0","70115328","1.5603024700012502191","43","4.8215493299237355274"
|
|
||||||
"01/27/2026 21:31:55.103","11392","64.015992652628568749","0","0","0","0","0","107159552","0","0","0","81117184","3.1336630113950012522","43","1.2896151410574541174"
|
|
||||||
"01/27/2026 21:31:56.107","11375","64.044296872812395804","0","191652.22893577121431","0.00015568333333333334088","11.946407222558878658","0.18598557571455665016","118534144","0","0","0","93200384","1.5555211543410489838","43","3.5576884308549661107"
|
|
||||||
"01/27/2026 21:31:57.105","11371","64.0751465372916158","0","0","0","0","0.067868783809043237154","130277376","0","0","0","105025536","0","43","5.9884976049378924046"
|
|
||||||
"01/27/2026 21:31:58.107","11358","64.108791822592920084","0","110285.84649014337629","0.00029033333333333336121","2.9916950545286287166","0.08685816082282102335","142499840","0","0","0","117673984","1.5581616106275297806","43","4.9521417517206849368"
|
|
||||||
"01/27/2026 21:31:59.106","11360","64.091822333655272814","0","41028.903941278986167","0.0001168571428571428593","7.0117755759021704876","0.081937663468393248656","153161728","0","0","0","128679936","3.1302591483952189044","43","1.3968368255506069531"
|
|
||||||
"01/27/2026 21:32:00.108","11346","64.119256326025180215","0","0","0","0","0","164245504","0","0","0","139956224","1.5596997151314528907","43","4.8583173769813692289"
|
|
||||||
"01/27/2026 21:32:01.106","11394","64.137433274681072248","0","45141.516088077252789","0.00024409999999999999732","3.0056939866883825019","0.199841617552878964","156962816","0","0","0","145809408","10.958426210691937897","43","2.9396535624428454803"
|
|
||||||
"01/27/2026 21:32:02.108","11186","64.600689421547741631","0","8176.7519928836700274","0.00032000000000000002618","1.9962773420126147528","0.063877527616301263413","317206528","670.74918691623850009","1006.123780374357807","2.9944160130189221292","297693184","99.808636900470730779","42","17.343406865969413388"
|
|
||||||
"01/27/2026 21:32:03.106","10722","66.327606855502267535","0","270664.88490164402174","8.4250000000000001429e-005","26.031631034870471808","0.21932538495334744089","808951808","2177724.15261784615","2177836.2888746117242","1.0012165782642488132","949456896","111.07696824132379732","51","6.1350623838890676609"
|
|
||||||
"01/27/2026 21:32:04.105","10609","66.283246866958961618","0","0","0","0","0","920911872","0","0","0","934006784","100.16787133557127731","51","6.0926206229019230776"
|
|
||||||
"01/27/2026 21:32:05.108","10745","65.813529250634260848","0","48981.314812273667485","0.00010804999999999999718","7.9722192077268339006","0.30873040922774841466","789962752","0","0","0","771710976","99.661181879962697394","51","1.8960240869117295226"
|
|
||||||
"01/27/2026 21:32:06.106","10746","65.810972933147795061","0","0","0","0","0","789962752","0","0","0","771710976","100.21529251290547791","51","6.0481632691511189037"
|
|
||||||
"01/27/2026 21:32:07.107","10751","65.770594255479650769","0","4091.6109289565088147","0.00062169999999999998701","0.99892844945227265985","0.062102072752822964907","789962752","0","0","0","771710976","99.890739509124912843","51","3.2308461005352340223"
|
|
||||||
"01/27/2026 21:32:08.108","10763","65.72810527070613773","0","118684.13916530630377","0.00029340000000000003013","2.9974779220763645426","0.087946063744210239976","789950464","0","0","0","771670016","99.916000618280207846","50","3.2049140229826300619"
|
|
||||||
"01/27/2026 21:32:09.109","10763","65.708198762896515177","0","0","0","0","0","789950464","0","0","0","771670016","99.936770005617447055","50","0.064738062682989649943"
|
|
||||||
"01/27/2026 21:32:10.105","10764","65.707176240558538893","0","0","0","0","0","789950464","0","0","0","771670016","100.34158281622298148","50","2.794091646783980476"
|
|
||||||
"01/27/2026 21:32:11.107","10764","65.708655623185592276","0","130780.49028714993619","0.00085130000000000003845","0.99777595739097546534","0.084942870855505323013","789950464","0","0","0","771670016","99.780184254088240436","50","0.21981574591175556677"
|
|
||||||
"01/27/2026 21:32:12.107","10737","65.755332602596681113","0","1213328.4229740765877","0.0013396499999999999554","44.033112900901478781","17.437400217985516093","789950464","0","0","0","771670016","100.07598769745868594","50","10.869823456950866714"
|
|
||||||
"01/27/2026 21:32:13.107","10751","65.702716299729118532","0","67073.428664030550863","6.7353333333333337859e-005","15.00031950680549464","0.33861335227218747335","789950464","0","0","0","771670016","100.0009900098011002","50","3.1240409280051895102"
|
|
||||||
"01/27/2026 21:32:14.107","10751","65.704435008974854782","0","49141.179112359459396","9.6600000000000003464e-005","8.9980186362962868429","0.086918548502229645014","789950464","0","0","0","771670016","99.975326089521104223","50","3.1489028507764249554"
|
|
||||||
"01/27/2026 21:32:15.107","10755","65.701291306340436904","0","350660.98375643382315","8.6031034482758625219e-005","28.995120121283591175","0.24944285530034823739","789950464","0","0","0","771670016","99.98110357142499538","50","3.1401101428177202735"
|
|
||||||
"01/27/2026 21:32:16.107","10757","65.689967425002237178","0","0","0","0","0.041078963429820383735","789950464","0","0","0","771670016","100.02182476216310647","50","1.5442659980628437033"
|
|
||||||
"01/27/2026 21:32:17.107","10995","65.110350624916691231","0","45057.198521480670024","8.7830000000000004422e-005","10.000266007075788721","0.087829200754273140106","668336128","1120.0297927924882515","1680.0446891887324909","5.0001330035378943606","673898496","634.36922724003204621","41","45.309705325942154275"
|
|
||||||
"01/27/2026 21:32:18.100","10835","65.573639414639600886","0","137164.45483544687158","9.0741666666666672292e-005","12.085683872954081863","0.10967126519184715316","833945600","676.79829688542861277","1015.1974453281428623","3.0214209682385204658","854089728","122.74943011531244963","41","10.303931822427713882"
|
|
||||||
"01/27/2026 21:32:19.095","10618","66.200988359330452226","0","0","0","0","0","1061310464","1125.0692244043989376","1687.6038366065981791","5.0226304660910665589","1091837952","98.884596816044052048","41","8.9633870582451535824"
|
|
||||||
"01/27/2026 21:32:20.114","10380","66.903318503616219459","0","0","0","0","0","1317523456","1319.6504693645204043","1979.4757040467807201","5.8912967382344660905","1360404480","101.25514155198776223","41","11.01820893916227817"
|
|
||||||
"01/27/2026 21:32:21.099","10134","67.621127881021507733","0","0","0","0","0","1574141952","1137.0409463673013306","1705.5614195509522233","5.0760756534254527494","1630420992","99.933565434696674856","41","7.9955590154301425798"
|
|
||||||
"01/27/2026 21:32:22.094","9912","68.278478008759776685","0","0","0","0","0","1805234176","899.70986365178180222","1349.5647954776725328","4.0165618913025973313","1878568960","98.84850218724434967","41","2.7227293024845611313"
|
|
||||||
"01/27/2026 21:32:23.114","9696","68.911287646952843033","0","27614.554556935945584","0.0003468999999999999753","1.961260977055109711","0.068035062474485624717","2037628928","658.98368829051685225","988.47553243577522153","2.9418914655826644555","2132140032","101.12591262152392346","41","5.0029305676593356367"
|
|
||||||
"01/27/2026 21:32:24.088","9446","69.665766337342972747","0","0","0","0","0","2302824448","919.80954604216526604","1379.7143190632477854","4.1062926162596662394","2415419392","99.44998894886057883","41","5.3621072906004147995"
|
|
||||||
"01/27/2026 21:32:25.093","9178","70.44045612459080985","0","0","0","0","0.19130000251946385759","2582384640","1115.342108287370138","1673.013162431055207","4.9792058405686168143","2711367680","99.583551545790655268","41","8.1964134187242354557"
|
|
||||||
"01/27/2026 21:32:26.100","8998","71.104953012220789788","0","131060.24399771049502","0.00045479999999999999594","1.9843181322327776428","0.090251158376991283405","2766368768","4889.3598778215646234","7334.0398167323464804","21.827499454560555847","2954424320","100.77103422013166778","41","10.076725266708708162"
|
|
||||||
"01/27/2026 21:32:27.096","8926","71.423402864351729136","0","0","0","0","0","2841747456","1799.7208022416166386","2699.5812033624247306","8.0344678671500737721","3075321856","98.868805276136612292","41","12.121074862700476515"
|
|
||||||
"01/27/2026 21:32:28.090","8895","71.586299075648724966","0","115415.51012653157522","0.00028796666666666665508","3.0190301546769906516","0.086928697136152741076","2877227008","3606.7346914541117258","5410.1020371811673613","16.101494158277283475","3133210624","102.19580741857289752","41","10.38213810986684571"
|
|
||||||
"01/27/2026 21:32:29.107","8769","72.043594175959839276","0","0","0","0","0.077168620664255921371","2935410688","2863.6250680873081365","4295.4376021309617499","12.784040482532624594","3225677824","99.878144975321674792","41","13.951136636645944833"
|
|
||||||
"01/27/2026 21:32:30.105","8466","73.232774593223069814","0","354789.38736084837001","9.315000000000000092e-005","18.024659536712174912","0.16788561549598146616","3104595968","0","0","0","3503640576","100.12859515475724947","41","24.900485633512971617"
|
|
||||||
"01/27/2026 21:32:31.107","8442","73.331458697405508929","0","3272347.3170057502575","0.0031957500000000002398","5.9918468939314273314","2.832207999519033681","3167928320","12303.258955539198723","18454.888433308795356","54.925263194371417796","3563728896","99.868403404833443915","41","15.739467932813278495"
|
|
||||||
"01/27/2026 21:32:32.111","8403","73.396954399858827855","0","136061.19250214289059","0.00013690769230769231617","12.938871595036410156","0.17714236461468302331","3206610944","445.89342112125478934","668.84013168188209875","1.9905956300056015795","3600154624","99.529365442568277444","41","20.687536912953408574"
|
|
||||||
"01/27/2026 21:32:33.103","8341","73.596617829426335788","0","4329182.2302357004955","0.00056137187500000004358","32.272649020721765112","1.811733754338871405","3273838592","1129.5427157252615871","1694.314073587892608","5.0426014094877755767","3668025344","100.8541438295064836","41","13.328470146517879868"
|
|
||||||
"01/27/2026 21:32:34.094","8297","73.715926397991353269","0","3174102.8928102776408","0.0020274833333333332208","6.0541208130078842942","1.227438753682851047","3323801600","2034.1845931706488955","3051.2768897559735706","9.0811812195118264412","3713888256","99.323465310570284714","41","11.712475279493073543"
|
|
||||||
"01/27/2026 21:32:35.107","8292","73.736344178611489042","0","105174.05676313841832","6.54117647058823445e-005","16.788977660882597576","0.10981905828774904399","3329404928","1990.9752331964305085","2986.4628497946459902","8.8882822910554928342","3718004736","100.30124197256755281","41","10.500430239862801329"
|
|
||||||
"01/27/2026 21:32:36.109","8292","73.747135017473979701","0","3139086.9476535441354","0.0020241999999999998709","5.9873331978865511616","1.2119597350596862384","3320193024","2458.7981665987435917","3688.1972498981153876","10.976777529458678018","3704479744","101.34840150182738228","41","11.125247913782132514"
|
|
||||||
"01/27/2026 21:32:37.108","8295","73.748973378387503885","0","1864122.1697786715813","0.0013898199999999999443","5.0011862813859453425","0.69509385232393960941","3322634240","4032.9566173096259263","6049.434925964438662","18.004270612989401457","3704479744","100.02645699787593969","41","10.913936736266737881"
|
|
||||||
"01/27/2026 21:32:38.107","8264","73.746373568136803556","0","123046.35867693120963","0.00029179999999999999124","3.0040614911360159489","0.087655916183561138899","3353731072","3588.8521280771606143","5383.2781921157402394","16.021661286058751728","3704479744","100.13241510573583071","41","9.2549988104269065303"
|
|
||||||
"01/27/2026 21:32:39.105","8255","73.729153902672493359","0","0","0","0","0","3372011520","3815.1155720534366083","5722.673358080154685","17.03176594666712873","3704483840","100.18795259907587081","41","7.6392311977269367063"
|
|
||||||
"01/27/2026 21:32:40.108","8246","73.723965155362151336","0","106194.93735260536778","0.00028513333333333334323","2.9915190435116447709","0.085294386285252987712","3379941376","3797.2348392307808354","5695.8522588461719351","16.951941246565986887","3704483840","99.71286682868013429","41","9.63187140552977894"
|
|
||||||
"01/27/2026 21:32:41.107","8201","73.72801172866671493","0","2364718.095375535544","0.00093906896551724139584","29.022559235293591939","2.7383549634516382021","3385401344","478139.65795612928923","59993269.76859112829","2228.7323936896150371","3705380864","150.11649039654773219","41","29.633747219500971681"
|
|
||||||
"01/27/2026 21:32:42.103","8191","73.731449123875108853","0","45249.868536758891423","0.0006897999999999999618","1.0043028350665590409","0.069280365022555287502","3385413632","712220.43724131665658","1860524.2300110592041","3822.376590263324033","3705393152","133.39081588225752739","41","32.521634505536113124"
|
|
||||||
"01/27/2026 21:32:43.105","8197","73.674590460414663085","0","10769392.93756887503","0.0012150461538461539233","77.859230511235679728","9.4599499081889693031","3385450496","707961.00650024751667","1814075.1521249581128","3776.1726797949304455","3705397248","146.6051878123501524","41","26.697406093824927353"
|
|
||||||
"01/27/2026 21:32:44.105","8206","73.67389427350691733","0","8964501.7032879590988","0.00057407555555555561493","134.9752725300724876","7.7487313859705126973","3385454592","710156.89925605629105","1835702.6992654947098","3780.3074476755859905","3705397248","137.47713755202511265","41","31.261431223987447225"
|
|
||||||
"01/27/2026 21:32:45.105","8210","73.673600580932941284","0","8996869.7105756998062","0.00080313560606060606633","131.97004280028434664","10.598958598897413097","3385462784","703095.3973448027391","1842493.7539178607985","3749.1489431898958173","3705405440","146.84131460243020229","41","32.827909277611709626"
|
|
||||||
"01/27/2026 21:32:46.106","8225","73.640390409538611038","0","0","0","0","0","3385470976","701030.65371630515438","1831356.0794334874954","3744.5239086752008006","3705409536","151.50447378653976216","41","31.276321168992271993"
|
|
||||||
"01/27/2026 21:32:47.106","8234","73.617949353907704335","0","8957696.3511126283556","0.00062185748031496062161","126.93833335765484094","7.893628976979351286","3385487360","699301.27943844872061","1792399.2524431629572","3745.1805912687614182","3705413632","156.17163307452318577","41","32.846197777955019603"
|
|
||||||
"01/27/2026 21:32:48.107","8239","73.617905837860391216","0","9938171.0329610593617","0.0002836638888888888977","143.89984570738764091","4.082011173675646809","3385487360","709383.26924460567534","1838536.3786804382689","3789.3626036278747051","3705413632","149.89905797436006196","41","21.92757397168746536"
|
|
||||||
"01/27/2026 21:32:49.108","8234","73.619733325582870975","0","4722118.0751299634576","0.00021927107438016529774","120.88055792071853034","2.6504897529008055734","3385503744","713169.31739747954998","1841499.4144286029041","3797.2479393111666468","3705413632","146.72607303586860894","41","34.441541835037433827"
|
|
||||||
"01/27/2026 21:32:50.107","8233","73.610748204777650017","0","0","0","0","0","3385507840","712406.68731175595894","1853210.6529621593654","3800.6551376791826442","3705413632","142.3274363354049683","41","23.362149665551168454"
|
|
||||||
"01/27/2026 21:32:51.106","8222","73.615545354228359543","0","4660295.3692265432328","0.00023683454545454546598","110.07424507830532434","2.8110694092459560522","3385516032","713292.11553192627616","10215671.470406789333","3766.540531588556405","3705417728","146.93246528717381238","41","29.659990022097638018"
|
|
||||||
"01/27/2026 21:32:52.107","8210","73.614022432270928675","0","8484982.8137039877474","0.0039004428571428571837","13.990065654378824433","5.5536146841798110785","3385528320","733726.98047116736416","27015670.172610428184","3785.3120499133565318","3705417728","149.89567261186215319","41","28.174990206816051597"
|
|
||||||
"01/27/2026 21:32:53.096","8189","73.674840660224390376","1","28986.805402715108357","0.00022921666666666669072","6.0658772466619224062","0.13904097214843424979","3385593856","722615.82464034156874","23067720.363463323563","3857.8979288769828599","3705417728","134.27164337572838804","41","41.552343471741757241"
|
|
||||||
"01/27/2026 21:32:54.106","8185","73.677037976141320996","0","9049535.8860253747553","0.00023766320474777447133","333.65632982195523937","7.9294480349760014803","3385597952","738246.807270302088","5923064.1967649590224","3926.6498637206959756","3705421824","157.7870313168274663","41","25.746191373647942413"
|
|
||||||
"01/27/2026 21:32:55.107","8193","73.667748289571093778","0","12101243.712429301813","0.00031502820512820513222","350.6623472258562515","11.046750349310400452","3385597952","731268.86121353751514","1730138.0500715861563","3896.2483025095134508","3705421824","146.73234681242894339","41","29.753973814579261159"
|
|
||||||
"01/27/2026 21:32:56.106","8197","73.671512020209689808","0","9409318.9491815753281","0.00046907947598253275229","229.31914345194206817","10.756819270371391184","3385597952","682981.50536101090256","1670860.336330070626","4054.6428464494038053","3705421824","129.86737930707823807","41","23.330929992689842578"
|
|
||||||
"01/27/2026 21:32:57.106","8189","73.668390063957403413","0","0","0","0","0","3385602048","639909.08786523889285","1579287.5051285496447","4149.8151223380882584","3705421824","126.50343554594360285","41","32.846915955572761447"
|
|
||||||
"01/27/2026 21:32:58.107","8193","73.63537572641749307","0","8313593.0557949626818","0.00022954146341463415736","245.96192509399546111","5.6458488455231359282","3385606144","635243.66428076929878","1580454.3456672907341","4155.3567507749803553","3705425920","134.35426913627225076","41","21.887052827748675554"
|
|
||||||
"01/27/2026 21:32:59.107","8212","73.575677926087678316","0","4296367.1648142784834","0.0017089583333333333796","11.999059273752937571","2.0505519166848484858","3385610240","725066.1548134626355","5979708.1908778352663","3946.6905794585704825","3705430016","148.42316232251963015","41","25.007244300200603959"
|
|
||||||
"01/27/2026 21:33:00.106","8252","73.461318801497426989","0","10648108.854902390391","0.00036156298701298702613","308.19394645050130066","11.143045345200421892","3385610240","720904.66530587698799","5957482.0434499429539","3915.4640014961419183","3705430016","146.9660748766010272","41","42.151651378359169087"
|
|
||||||
"01/27/2026 21:33:01.107","8245","73.462656786074546744","0","0","0","0","0","3385610240","738368.33458186208736","5971947.402203050442","3920.8562287135509905","3705430016","148.24476697845133799","41","28.218323357802521656"
|
|
||||||
"01/27/2026 21:33:02.107","8255","73.463472624650108855","0","10026832.491504179314","0.00034494556574923547574","327.12790701164158236","11.284091457030454464","3385610240","735274.49232649966143","1796272.3424859121442","3935.5387956691065483","3705430016","123.4853382200264349","41","12.466089363019229097"
|
|
||||||
"01/27/2026 21:33:03.107","8255","73.449733870441505701","0","7796265.1844468135387","0.00017896167247386757606","287.05807184793485476","5.1369171136290621149","3385610240","661372.79571657348424","10034154.909538200125","4107.8310142141754113","3705430016","150.02470906958376418","41","28.108184504355570255"
|
|
||||||
"01/27/2026 21:33:04.102","8244","73.522517730836412397","0","11423832.143688943237","0.00015366815365551426292","810.49347000375723837","12.455436556772884416","3385671680","642979.43425547133666","31201949.764068063349","4121.7660481975453877","3705438208","139.66919504945582275","41","34.093275113801460918"
|
|
||||||
"01/27/2026 21:33:05.107","8233","73.523116012458473278","0","0","0","0","0","3385671680","640731.61630296625663","5944267.0006540464237","4029.1889256627578106","3705438208","127.53127813464415397","41","33.123841953784157965"
|
|
||||||
"01/27/2026 21:33:06.106","8238","73.520722885970187122","0","10451561.874567780644","0.00020501098901098900834","546.51421522510531759","11.203950209203261679","3385671680","724013.22404250164982","10312449.984190125018","3905.6748494658627351","3705438208","156.39447721805404967","41","31.184639170283933396"
|
|
||||||
"01/27/2026 21:33:07.107","8237","73.505939863041490412","0","12420756.400261098519","0.00024187659574468085432","516.55901357011521213","12.494680651760669221","3385671680","737619.29440836363938","1935475.6844082206953","3948.6290553754261055","3705438208","138.94752092643335573","41","25.063957911922173594"
|
|
||||||
"01/27/2026 21:33:08.106","8238","73.503394418746097472","2","5254583.6382454391569","0.00038778800000000002157","125.08359336544612006","4.8506435869955701889","3385675776","646770.23654908570461","1757666.6486212734599","4119.7532310843334926","3705438208","148.53835754477287878","41","31.20328703189465358"
|
|
||||||
"01/27/2026 21:33:09.107","8238","73.47495965042034527","0","1890922.2736688789446","4.0785943775100405096e-005","248.81187334256568988","1.0148037217097369833","3385679872","646674.0497509832494","5961920.1921427203342","4096.9023321466638663","3705442304","145.20285732243581833","41","29.740552908498795404"
|
|
||||||
"01/27/2026 21:33:10.107","8257","73.449429281393392444","0","12603932.430661311373","0.0075874599999999998642","14.995769693369501496","11.378063308202905901","3385679872","645699.84807285864372","1774447.4283804539591","4118.8380757788227129","3705442304","156.20707429598346039","41","37.517170281606617266"
|
|
||||||
"01/27/2026 21:33:11.107","8255","73.455401224423056306","0","12068186.831245079637","0.00027742693965517246143","464.36796517560514985","12.882410938241815046","3385688064","735841.08047216618434","10388753.047915168107","3921.1070852543557521","3705442304","143.85944826824248821","41","31.197655176057949689"
|
|
||||||
"01/27/2026 21:33:12.107","8258","73.426030965853954058","0","0","0","0","0","3385688064","735790.75217920681462","1968282.279914725339","3913.0045949825407661","3705442304","137.39523613244901412","41","25.057143927755088697"
|
|
||||||
"01/27/2026 21:33:13.105","8259","73.438986524343249584","0","11474312.430410953239","0.00032581509433962261776","371.70933292001126347","12.110950636895610799","3385688064","724972.46495487331413","1968220.9560504308902","3876.3973290229746453","3705442304","156.55002812890904806","41","31.117987623280018994"
|
|
||||||
"01/27/2026 21:33:14.106","8258","73.438747197724580928","0","10122618.759597938508","0.00043586516853932584861","355.76139083516687833","15.517454340500544063","3385688064","713211.64894705126062","1967360.4913184728939","3896.386693444706907","3705442304","126.47801268752473902","41","25.050066555540894342"
|
|
||||||
"01/27/2026 21:33:15.105","8263","73.428271797818183586","0","9624246.0268020424992","0.00045226769230769230546","195.13864600798865467","8.8253659848718655212","3385688064","638423.59996777703054","14401316.135113997385","4107.9186762194540279","3705442304","142.28658838014794696","41","32.765678018171854546"
|
|
||||||
"01/27/2026 21:33:16.106","8245","73.491015395496745555","0","49123.110698598153249","0.00035659999999999999424","1.9988244913166564043","0.071278815100634843049","3385688064","660224.72184108523652","22830012.669549036771","4126.5731623232377387","3705442304","143.66698921363237673","41","21.920114557808499711"
|
|
||||||
"01/27/2026 21:33:17.107","8237","73.49823831429198151","0","9528464.2821826934814","0.00012933269754768393568","733.14442046132171527","9.481764197720885079","3385688064","663468.73198976798449","14425765.132090851665","4118.1940675232008289","3705442304","135.77631993061035587","41","20.40698486826288871"
|
|
||||||
"01/27/2026 21:33:18.107","8245","73.494485456844444116","0","10700252.231917293742","0.00037792108843537412484","294.15384245960638054","11.116788573126616058","3385688064","746046.18215326615609","6092249.2463558446616","3960.0711171942930378","3705442304","145.38977466523456883","41","29.650109032951011301"
|
|
||||||
"01/27/2026 21:33:19.107","8253","73.484347265559321727","0","11787184.106969796121","0.00026403530701754384996","455.87809819654222565","12.03611511314938376","3385688064","735510.32453921821434","6048117.7333181109279","3992.9322899056792266","3705442304","148.38948116389536835","41","28.143445577520253664"
|
|
||||||
"01/27/2026 21:33:20.108","8247","73.485794028613199202","0","0","0","0","0","3385688064","741179.80620023724623","1823784.3091929613147","3957.5854771004210306","3705442304","148.35651218000094786","41","26.607474939357167898"
|
|
||||||
"01/27/2026 21:33:21.106","8246","73.450343025254625218","0","9500665.0805103778839","0.00044792857142857139688","224.22943155436641405","10.04387086234383375","3385688064","738002.12377304455731","1823500.8060247246176","3997.0898223061835779","3705442304","156.40994480956226198","41","24.92322649141011226"
|
|
||||||
"01/27/2026 21:33:22.107","8251","73.429359605868654626","0","8700328.4613892938942","0.00017130979729729729831","591.75028138125719579","10.137241809472783416","3385688064","729794.02692063956056","5961963.0515922289342","4010.3076501716277562","3705442304","142.12723805106634245","41","28.155462084076354756"
|
|
||||||
"01/27/2026 21:33:23.107","8257","73.429359605868654626","0","9442443.3090156707913","0.00056637777777777779942","189.02328766904082613","10.705877164057790552","3385688064","663223.70916096866131","1681838.2024665437639","4122.5078929724140835","3705442304","151.58143252092187936","41","32.804107232993395371"
|
|
||||||
"01/27/2026 21:33:24.107","8255","73.429196438153553572","0","352138.45618332602317","3.6866071428571427302e-005","55.981313437574542036","0.20638016095353334256","3385688064","667304.25384006823879","1702406.7366313126404","4144.6165270032861372","3705442304","139.01545716929391006","41","28.14931427205033998"
|
|
||||||
"01/27/2026 21:33:25.107","8260","73.433406155890182276","0","8150276.6481214175001","0.00025212078651685391594","356.1456279472676556","8.979070078263116983","3385688064","667829.07530889380723","1682241.8687001115177","4135.6910840842820107","3705442304","129.73908425990171622","41","14.028317659101263715"
|
|
||||||
"01/27/2026 21:33:26.108","8361","73.13167561617019885","0","10759175.221927553415","0.0012532486842105263359","75.935007227314144984","9.5163508217748784546","3385688064","714585.38636780786328","14316983.094169700518","4012.5656450643896278","3705442304","138.94074622408382425","41","40.676048260471866058"
|
|
||||||
"01/27/2026 21:33:27.108","8362","73.13634221747898323","0","0","0","0","0","3385688064","755960.16281049617101","18648620.079941190779","3948.157531485506297","3705442304","148.44657008543222787","41","34.372027052825494309"
|
|
||||||
"01/27/2026 21:33:28.105","8352","73.127074254007794707","0","9321504.4693941622972","0.00074525704225352114047","142.29750138614801358","10.604879949349827584","3385688064","764253.8254729162436","22973187.042149022222","3975.3111830904872477","3705442304","145.61774391479417545","41","20.145108175758018376"
|
|
||||||
"01/27/2026 21:33:29.106","8278","73.328325867962163898","0","12569439.198358025402","0.00022335847701149424038","695.48117104639948138","15.689795510474310092","3385688064","745740.67745461896993","6070951.0705014066771","3958.0472967166497256","3705442304","153.0236218505240231","41","40.664309894694760317"
|
|
||||||
"01/27/2026 21:33:30.105","8175","73.68937350662643837","0","10513108.143749916926","0.00030956630434782607224","276.26120496929848969","8.7681843687647553764","3385688064","692105.38564212468918","1724677.6827490392607","3690.4893576876938823","3705442304","136.06559214844855887","41","64.028636558456128114"
|
|
||||||
"01/27/2026 21:33:31.104","8221","73.489525139678619325","0","520647.93139352131402","0.0015659999999999999008","1.0008764675226096141","0.22650505462963238523","3385688064","721552.8638428671984","1803442.2743996919598","3857.3779058321374578","3705442304","142.30376217369590108","41","35.885118141521623158"
|
|
||||||
"01/27/2026 21:33:32.106","8205","73.586555797044781002","0","9369074.7735941242427","0.00012966219135802470146","646.97377020569967954","8.3888765472467188289","3385688064","727363.25640269403812","6069106.1837713019922","3864.8695439294192511","3705442304","148.20370864960523249","41","40.718516540157899897"
|
|
||||||
"01/27/2026 21:33:33.106","8207","73.589612490867637007","0","10195611.812573723495","0.00030411058394160585825","274.0179481756055111","8.3328533204689883007","3385688064","707069.31304000411183","1776870.3850102182478","3977.2605105634420397","3705442304","137.50368509876065559","41","29.683527119282238971"
|
|
||||||
"01/27/2026 21:33:34.103","8213","73.585500655133628811","0","10946178.488288458437","0.00033489974937343357925","400.25913518747273656","13.497825348744241225","3385688064","643366.90360536170192","1612611.9546870545018","4125.9795063310157275","3705442304","136.37027105519365477","41","31.033182117942391898"
|
|
||||||
"01/27/2026 21:33:35.105","8212","73.578451777244552545","0","0","0","0","0.30544687828980870981","3385688064","645920.80235724058002","1647998.4613332671579","4175.8680912501995408","3705442304","143.4011194165714187","41","20.505901192987586512"
|
|
||||||
"01/27/2026 21:33:36.106","8218","73.579691856536015848","0","11429971.967953160405","0.00022349675090252709114","553.31007766416064442","12.365797798754323722","3385688064","685319.4751464399742","10103586.837572231889","3994.0198566407548242","3705442304","131.08121982135801886","41","29.775056459606162207"
|
|
||||||
"01/27/2026 21:33:37.106","8222","73.551572550449421328","0","10880158.344429560006","0.00024239960629921259859","508.43807024131990602","12.324839527564666497","3385688064","734023.43459124385845","14523976.85846124962","3963.4148782591082636","3705442304","139.18604153046243255","41","26.500260420524103466"
|
|
||||||
"01/27/2026 21:33:38.105","8224","73.519722133297406685","5","8451719.1047615930438","0.00062652171428571423566","175.09826514640016626","10.970321649539000575","3385692160","741334.03666137438267","10391676.809025226161","3914.1966471583855309","3705446400","146.95794306306478916","41","37.464705079546902766"
|
|
||||||
"01/27/2026 21:33:39.105","8220","73.516567534188865807","0","4030029.9657726860605","0.00025596335078534033884","190.97943151522579797","4.888216627315500773","3385692160","733551.99644998228177","14514916.743466727436","3852.5850765872514785","3705446400","149.97903293119620116","41","26.570584578637891582"
|
|
||||||
"01/27/2026 21:33:40.108","8213","73.585391876656885302","0","6151092.1357134478167","0.0027918600000000001748","14.957484845076354674","4.1760773558400750005","3385696256","736286.18016150896437","14440259.007796239108","3855.0424274043457444","3705450496","158.92925112935751031","41","36.118565537839543822"
|
|
||||||
"01/27/2026 21:33:41.107","8216","73.565452725991008265","0","12020973.212743533775","0.00024568720472440948397","508.65998633226610082","12.496513146314487841","3385696256","719472.51558897667564","1785323.4571856984403","3841.9849755057184666","3705450496","129.84963024832805445","41","24.903568672532394146"
|
|
||||||
"01/27/2026 21:33:42.105","8212","73.567225824475116269","0","0","0","0","0","3385696256","721148.07909403520171","1813855.5360864156391","3858.196738971945706","3705450496","153.45643520876387811","41","35.801122324153197951"
|
|
||||||
"01/27/2026 21:33:43.107","8218","73.564702149844890755","0","10972726.702423078939","0.00029280302267002515388","395.94887452280437401","11.593609107107148759","3385700352","709270.10066376801115","5995612.4476352632046","3934.5549370087233001","3705450496","135.57882621524757383","41","25.197888984690997916"
|
|
||||||
"01/27/2026 21:33:44.107","8224","73.565931355945281211","0","9133774.5164243169129","0.00094089052631578945236","95.039441368167800306","9.0105913566662430014","3385700352","617594.30163517862093","1614698.0997113802005","4144.7200588244122628","3705450496","134.42902702596174436","41","26.532973602090681453"
|
|
||||||
"01/27/2026 21:33:45.107","8200","73.638290975624300927","0","10729922.999066483229","0.00028707616580310880549","386.0899203424477264","11.08386663119127391","3385704448","571407.08070909709204","5779685.0886571481824","4237.9870271786294325","3705454592","142.22248673173601219","41","20.292892051444656687"
|
|
||||||
"01/27/2026 21:33:46.105","8224","73.616372065994966079","0","0","0","0","0","3385716736","573215.56904937978834","1658937.5175761110149","4252.3172424954509552","3705454592","136.14145351150560259","41","35.841383977336448652"
|
|
||||||
"01/27/2026 21:33:47.100","8219","73.611487907732723102","0","10567858.764713684097","0.00022798756756756759462","557.60289029188254517","12.712150732093640215","3385729024","652757.07000277296174","10257358.348772067577","4138.3176668689438884","3705454592","147.55800172257326608","41","32.498645703060958567"
|
|
||||||
"01/27/2026 21:33:48.092","8225","73.598140751383766656","0","10416436.922348136082","0.00053727163461538462921","209.82137543157131176","11.273366619032083591","3385729024","657724.44279815373011","14571439.840681016445","4128.8408155837569211","3705454592","154.4694090547672829","41","35.376405641986551132"
|
|
||||||
"01/27/2026 21:33:49.102","8233","73.600599140301483203","0","8585736.9486130997539","0.00012545989085948158108","725.7728986300219276","9.1054892786698289342","3385733120","666377.34764759475365","6036450.2358415368944","4158.5895965158151739","3705454592","139.2377326978119072","41","21.098618137906598236"
|
|
||||||
"01/27/2026 21:33:50.093","8216","73.667139111474881474","0","70274.413551480858587","0.00035722500000000002298","4.0369033519922368214","0.14421213687083178634","3385733120","677187.50961918383837","18850930.78375973925","4141.8628391440352061","3705454592","138.77226411742859113","41","25.882995300918821613"
|
|
||||||
"01/27/2026 21:33:51.105","8210","73.68666491789898032","0","11381638.169149212539","0.00027894565701559021452","443.37078713817879816","12.367556170718973618","3385733120","750391.72648387018126","10396393.232957083732","3956.7633498055288328","3705454592","152.74716799961765901","41","39.82687321227182764"
|
|
||||||
"01/27/2026 21:33:52.107","8207","73.679006885196784538","0","10729973.141882600263","0.0022963686274509805159","50.914916083732471463","11.691369675021995533","3385733120","750007.66219570476096","6345048.788968754001","3986.3384298498776843","3705454592","163.78078067915379279","41","29.804915318147340741"
|
|
||||||
"01/27/2026 21:33:53.108","8209","73.67556946670534046","0","9400419.4161849729717","0.00019082725563909774813","531.31110202511422358","10.232128342861274817","3385733120","745139.85166832676623","2144435.5248984163627","3979.8397397933836146","3705454592","156.05795508047796716","41","36.019267376293093719"
|
|
||||||
"01/27/2026 21:33:54.099","8194","73.681497916970755568","0","4135.833035127923722","5.3699999999999997464e-005","1.0097248620917782524","0.10077925455864321369","3385733120","688259.76747248088941","1997763.8633204114158","4110.5899135756289979","3705454592","156.18966976796033919","41","33.737715856016826876"
|
|
||||||
"01/27/2026 21:33:55.103","8203","73.6523669617372434","0","10136598.536697486416","0.00038810072463768113251","274.8621805175296231","10.667320221668276758","3385733120","660006.83570292417426","1939341.9002696436364","4102.0192809844365911","3705454592","149.38020658485874037","41","34.646159619124297535"
|
|
||||||
"01/27/2026 21:33:56.107","8216","73.629458149343136597","0","10761314.931790962815","0.00022894880382775121869","624.4506800984978554","14.29654706940549147","3385733120","677473.2155973239569","6065321.3256878796965","4203.8378320506535601","3705454592","143.16375872439945738","41","26.861992825578539623"
|
|
||||||
"01/27/2026 21:33:57.107","8215","73.613837518173696139","0","0","0","0","0","3385733120","714222.99420466448646","6205074.3488698359579","4169.3852511972108914","3705454592","146.89114333665270351","41","26.553965570799331175"
|
|
||||||
"01/27/2026 21:33:58.106","8222","73.608757549340083415","0","10223832.412511598319","0.00042215743944636676408","289.12195163920142704","12.205545900890008681","3385733120","764705.5528021720238","10465172.209638025612","4024.6976174550427459","3705454592","159.44287483181588527","41","37.47377650186488296"
|
|
||||||
"01/27/2026 21:33:59.105","8234","73.567497770666960832","0","9021694.9377210512757","0.0006260585937499999875","128.1490758199012987","8.0228862327090038065","3385733120","767464.79179229191504","10492934.430623143911","4056.7191814237503422","3705454592","140.78883596841643566","41","34.298543214738998586"
|
|
||||||
"01/27/2026 21:34:00.096","8239","73.566290310948687647","0","11044346.263615325093","0.00024998416833667334684","503.55180619693658173","12.588079247254817972","3385733120","748821.90068908897229","10504691.104772480205","3953.739432223642325","3705454592","141.90867751506576155","41","41.659765910472955852"
|
|
||||||
"01/27/2026 21:34:01.090","8240","73.569890897155246989","0","65902.006564055453055","0.00016630000000000000359","1.0055848169564125527","0.016722091904536665746","3385733120","749552.86671114037745","10523279.187954060733","3970.0488573439170068","3705454592","142.97489132148569979","41","21.438630875196274417"
|
|
||||||
"01/27/2026 21:34:02.106","8250","73.557424841811283045","0","11411477.928586313501","0.00019542072829131651858","702.90029064041004858","13.736588454742094001","3385733120","739061.26189503690694","14382987.922814458609","4016.5730893737718361","3705454592","149.21133227395952758","41","27.705092101374617641"
|
|
||||||
"01/27/2026 21:34:03.106","8241","73.566127143233586594","0","12637936.849253848195","0.00033720262529832932498","419.19479982347797886","14.13523283912807571","3385733120","669927.31522338429932","6016259.7559085702524","4154.9307963410592492","3705454592","150.06840117725658956","41","23.402586899108612783"
|
|
||||||
"01/27/2026 21:34:04.106","8247","73.557664168429951701","0","10124307.373945007101","0.00014211002747252746039","727.63341828386865018","10.340544167412383914","3385733120","663300.82904232852161","1769759.3952166899107","4158.9047437900790101","3705454592","134.30920192197842766","41","20.351519790454652536"
|
|
||||||
"01/27/2026 21:34:05.109","8246","73.528554959578571015","0","0","0","0","0","3385733120","677176.73285492823925","10163742.965447761118","4202.3237293807278547","3705454592","144.97535980078328066","41","26.732882681324575458"
|
|
||||||
"01/27/2026 21:34:06.108","8252","73.503144242219434545","0","9980735.6300343964249","0.00032271467181467180553","259.28762776548023794","8.3676231645821808058","3385733120","754796.29553063213825","1903505.5587162838783","4024.4643382904655482","3705454592","142.34593102122664732","41","24.916431988803534381"
|
|
||||||
"01/27/2026 21:34:07.107","8261","73.483781626793515329","0","6305711.3344156285748","0.00014825326370757180273","383.05730537288377491","5.6789989855394109597","3385733120","751303.3949878901476","1899300.1353002409451","4021.6016316040877427","3705454592","142.21001184487505498","41","20.299883471553549441"
|
|
||||||
"01/27/2026 21:34:08.107","8264","73.485924553472074194","0","9038958.1691177729517","0.00073350489510489510093","143.1152363883398948","10.497416107950112263","3385733120","755032.95253337989561","10332017.340362459421","3986.2096960472572391","3705454592","137.60875219686116111","41","29.631888081150535186"
|
|
||||||
"01/27/2026 21:34:09.107","8262","73.483640205460517336","0","229323.55370326802949","0.00016939999999999999763","2.9993140568751925912","0.050809990431884922979","3385733120","755010.32913772610482","6098625.2444066032767","3997.085866462339709","3705454592","143.72168682769495263","41","32.825733330533878984"
|
|
||||||
"01/27/2026 21:34:10.107","8254","73.553737246793176041","0","10389091.86304683052","0.00029742534059945503148","366.91311497437408207","10.912661795271434428","3385733120","749479.5232488947222","1867517.7717916399706","4005.051603780224923","3705454592","137.46412186419345858","41","23.457477598346830661"
|
|
||||||
"01/27/2026 21:34:11.107","8259","73.55073494220867758","0","7771335.2081617647782","0.00028901301587301586604","315.04958880527794918","9.105547177382494084","3385733120","753099.53786726028193","1861732.0366225643083","4024.6334773093285548","3705454592","129.71082200579664345","41","24.986512574960972444"
|
|
||||||
"01/27/2026 21:34:12.107","8259","73.544175590748011473","0","0","0","0","0","3385733120","761776.8694205355132","1847257.6632643265184","4064.6097974594435982","3705454592","139.05621465909740664","41","25.003389846778922845"
|
|
||||||
"01/27/2026 21:34:13.107","8265","73.548189521196334795","0","7095250.3469975283369","0.00036114941176470589023","169.92533480788540601","6.164927592347480001","3385737216","753700.8238579967292","1636298.0106541183777","4040.2247252557222055","3705458688","135.86648258955045776","41","17.230763479929045445"
|
|
||||||
"01/27/2026 21:34:14.102","8496","72.850373706978359678","0","8279842.4809748930857","0.00021652010309278348988","389.91847688999416732","8.442640208106903188","3133726720","476114.57895135646686","1185209.4676225967705","2532.4602107288278603","3434455040","114.62813251991954644","41","21.487580465808530761"
|
|
||||||
"01/27/2026 21:34:15.102","8830","71.770635589903832852","1","4223427.0620102221146","6.0866160849772388026e-005","659.07038871751501574","4.0114827528683880686","2783662080","0","0","0","3027333120","100.01004100811721287","41","12.491214117897442293"
|
|
||||||
"01/27/2026 21:34:16.106","9008","71.325436716742217413","0","0","0","0","0","2602901504","0","0","0","2867236864","99.610880058140878646","41","5.0583799445844785936"
|
|
||||||
"01/27/2026 21:34:17.106","9247","70.627968961053525732","0","3547437.1774163627997","3.4005872756933113333e-005","613.05204811888529548","2.0847574265282919903","2308984832","0","0","0","2547482624","100.0094708968939301","41","6.241121034161933423"
|
|
||||||
"01/27/2026 21:34:18.107","9520","69.790339788838835489","0","2700274.935885750223","3.2701167315175099187e-005","513.41342516175268429","1.6788782307873248989","2031820800","0","0","0","2233516032","99.883286379865126037","41","3.2380663195056613723"
|
|
||||||
"01/27/2026 21:34:19.107","9746","69.093383305960657026","0","4037786.1911667422391","9.2841875000000001223e-005","160.12794222583843862","1.4867148263240810291","1800921088","0","0","0","1985425408","100.08380016587889827","41","0"
|
|
||||||
"01/27/2026 21:34:20.107","10033","68.115244565558441536","0","0","0","0","0","1507934208","0","0","0","1625628672","99.967310689404570212","41","3.156667769639331933"
|
|
||||||
"01/27/2026 21:34:21.106","10360","67.2421208739378784","0","3155515.1128895659931","0.0021377750000000001786","4.0020230226379434058","0.92649408702433033724","1169424384","0","0","0","1298059264","100.05119619709405754","41","6.2020035652243237223"
|
|
||||||
"01/27/2026 21:34:22.105","10822","65.711451244007662353","0","2099325.2214692649432","0.0029182000000000001341","2.0020725454991006309","0.58424907966555139627","682590208","0","0","0","719970304","100.10435879404278126","41","9.2804248428987268227"
|
|
||||||
"01/27/2026 21:34:23.111","11290","64.376017745671802572","0","1899332.6828896303196","3.7539171974522297307e-005","311.78402621210011603","1.1703957290529867219","197574656","3013618.0113461585715","3013729.2209351258352","0.99294275863726155773","192450560","783.48339310603125796","42","53.401872096563842263"
|
|
||||||
"01/27/2026 21:34:24.091","11281","64.34932343297388968","0","4183.7063838906060482","5.1199999999999997683e-005","1.0214126913795424922","0.0052294914309369564323","199487488","5291289.5355656920001","5291289.5355656920001","0","182243328","1383.6575532905094406","42","96.81186927296948852"
|
|
||||||
"01/27/2026 21:34:25.107","11307","64.291213817030936184","0","2925371.0229666647501","0.00092301111111111103745","8.8659547436355001793","0.88779256408584172888","182259712","5476760.313149462454","5476760.313149462454","0","165285888","1397.6893375243162154","42","96.921521656873494521"
|
|
||||||
"01/27/2026 21:34:26.108","11303","64.27447275823784878","0","1272028.4681582306512","3.3593032786885241962e-005","243.74791590537063257","0.81885507542893321009","183635968","5833032.4778114464134","5833032.4778114464134","0","163164160","1397.0471586676940206","42","92.195267270012877248"
|
|
||||||
"01/27/2026 21:34:27.106","11291","64.304800276715496921","0","0","0","0","0","194453504","6038277.3346951240674","6038277.3346951240674","0","175382528","1397.2819689352140813","42","98.435294547664938136"
|
|
||||||
"01/27/2026 21:34:28.108","11292","64.270632868695685147","0","1173203.1158814644441","0.00016060000000000000057","19.968734951686144541","0.32065376630909248057","194977792","6446278.0624376414344","6446278.0624376414344","0","176173056","1396.0593129603721536","42","100"
|
|
||||||
"01/27/2026 21:34:29.108","11279","64.327154323534841751","0","1200035.1172819226049","0.00033412727272727271244","10.999148665893260457","0.50788905666845773901","188964864","6041477.3896500421688","6041477.3896500421688","0","170024960","1398.5450481141999717","42","95.312139503527831152"
|
|
||||||
"01/27/2026 21:34:30.107","11290","64.331070371980558775","0","1189086.5193982853089","0.00012752692307692307006","26.027284402238866079","0.35861066966081073248","189038592","5962089.0579594587907","5962089.0579594587907","0","171126784","1398.1252035570207681","42","96.872203124033504196"
|
|
||||||
"01/27/2026 21:34:31.106","11288","64.306018609624828741","0","0","0","0","0","189558784","5292224.931323970668","5292224.931323970668","0","167182336","1400.2359397558489036","42","98.436447729321827183"
|
|
||||||
"01/27/2026 21:34:32.108","11301","64.280694901077239933","0","1092468.9575477945618","0.00034147000000000002221","9.9893653216785409654","0.34110477911300368659","181981184","5744505.3995516365394","5744505.3995516365394","0","161472512","1396.941794420713677","42","96.879914758272761333"
|
|
||||||
"01/27/2026 21:34:33.108","11295","64.291442247175481839","0","1081494.003218246391","0.00098117499999999992916","4.000554876961434303","0.39257312896097806831","185393152","5526432.5161899961531","5526432.5161899961531","0","164974592","1398.8049660645851873","41","95.311268270174579698"
|
|
||||||
"01/27/2026 21:34:34.107","11293","64.300993030029587771","0","1053097.4513703535777","0.0015861499999999999298","2.000808326563931594","0.3173516091366211378","187752448","5146855.329553139396","5146855.329553139396","0","165597184","1419.2938734122915321","41","96.872222220554078831"
|
|
||||||
"01/27/2026 21:34:35.092","11289","64.311718606462633829","0","137244.06349974797922","0.00044024999999999999464","2.03071826911322173","0.089392832309097572385","191561728","5216245.0963230598718","5216245.0963230598718","0","171655168","1375.3475584182920102","41","93.657933488871250916"
|
|
||||||
"01/27/2026 21:34:36.107","11287","64.317473015821875038","0","228091.87423683636007","4.4388372093023252291e-005","42.380870576016157258","0.18812007949002279572","191545344","5800675.6693491786718","5800675.6693491786718","0","171032576","1398.3096493763810031","41","96.920022798730443014"
|
|
||||||
"01/27/2026 21:34:37.106","11298","64.305344169099200258","0","222316.13456785379094","5.0890909090909087239e-005","33.016059011103003229","0.16803768030356044938","185384960","5994757.8502183463424","5994757.8502183463424","0","163323904","1394.5606581105596433","41","98.436591190459012068"
|
|
||||||
"01/27/2026 21:34:38.106","11300","64.320507939979549406","0","110638.46815662577865","5.6231818181818185381e-005","22.009243882430620687","0.12376669752413579917","190230528","6724260.1892794976011","6724260.1892794976011","0","174882816","1395.9519856046053974","41","96.873567781400666377"
|
|
||||||
"01/27/2026 21:34:39.107","11293","64.303940945375686056","0","102226.03173918624816","0.00027953333333333331553","2.9949032736089722384","0.083710325937224244752","189734912","6293502.7170760799199","6293502.7170760799199","0","166821888","1395.9415564969833667","41","96.880577527381035452"
|
|
||||||
"01/27/2026 21:34:40.106","11310","64.287754652157374835","0","172356.06387129079667","7.9088461538461541614e-005","26.048977287095198108","0.20600658002829169702","181878784","5501967.799857291393","5501967.799857291393","0","162775040","1163.0635802246506501","41","92.173192596065618432"
|
|
||||||
"01/27/2026 21:34:41.107","11324","64.254435725569365445","0","66479.387095208352548","7.3715384615384620332e-005","12.984255292032882423","0.095714243194280831939","171872256","3338910.2374460734427","3338910.2374460734427","0","148455424","575.86604760203010756","41","68.787748097450943874"
|
|
||||||
"01/27/2026 21:34:42.102","11197","64.560637079309827868","0","3997103.1871975827962","0.00073616666666666665322","18.090005010931388796","1.3317745878795346215","77455360","20837736.072052892298","69458145.054906174541","6.0300016703104626359","52105216","48.681481850739537265","40","7.3481474453666928426"
|
|
||||||
"01/27/2026 21:34:43.093","11197","64.562334042173418425","0","3228690.9341432941146","0.00064717857142857144245","14.130044451101264613","0.91442909669887106894","77455360","113.04035560881011691","113.04035560881011691","0","52400128","0","40","0.65215583434123924889"
|
|
||||||
"01/27/2026 21:34:44.106","11202","64.524903256568336474","0","6249070.275020962581","0.00063857857142857134183","27.631470784703175525","1.7648343127330383684","77455360","0","0","0","52400128","0","40","4.3812015268212327612"
|
|
||||||
"01/27/2026 21:34:45.106","11194","64.523619661229574263","0","4276239.8220873419195","0.00074496250000000000836","16.0000592002190416","1.1917099999700058177","77455360","0","0","0","52400128","0","40","3.1436932671994322064"
|
|
||||||
"01/27/2026 21:34:46.106","11212","64.525033781427197255","0","0","0","0","0","77455360","0","0","0","52400128","0","40","0.044140507551859720081"
|
|
||||||
"01/27/2026 21:34:47.106","11219","64.501787760411815498","0","4198303.7239578142762","0.0028684249999999999678","4.0038144340112822306","1.1482473758198639135","77455360","0","0","0","52400128","0","40","0"
|
|
||||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
@@ -1,892 +0,0 @@
|
|||||||
# Parallel Region Processing Architecture
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
This document outlines a comprehensive plan to parallelize the Arnis world generation pipeline by splitting large user-selected areas into smaller processing units (**1 Minecraft region = 512×512 blocks per unit**). The goal is to:
|
|
||||||
|
|
||||||
1. **Reduce memory usage by ~90%** by processing and flushing regions incrementally
|
|
||||||
2. **Utilize multiple CPU cores** for parallel generation
|
|
||||||
3. **Maintain visual consistency** across region boundaries (colors, elevation, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Architecture Analysis
|
|
||||||
|
|
||||||
### Processing Pipeline Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ CURRENT PROCESSING FLOW │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ [1/7] Fetch Data (retrieve_data.rs) │
|
|
||||||
│ └── Downloads OSM data for entire bbox from Overpass API │
|
|
||||||
│ └── Single HTTP request with full bounding box │
|
|
||||||
│ │
|
|
||||||
│ [2/7] Parse Data (osm_parser.rs) │
|
|
||||||
│ └── Transforms lat/lon → Minecraft X/Z coordinates │
|
|
||||||
│ └── Clips ways/relations to bounding box (clipping.rs) │
|
|
||||||
│ └── Sorts elements by priority │
|
|
||||||
│ │
|
|
||||||
│ [3/7] Fetch Elevation (ground.rs / elevation_data.rs) │
|
|
||||||
│ └── Downloads Terrarium tiles for entire bbox │
|
|
||||||
│ └── Builds height grid matching world dimensions │
|
|
||||||
│ │
|
|
||||||
│ [4/7] Process Data (data_processing.rs) │
|
|
||||||
│ └── Pre-computes flood fills in parallel (floodfill_cache.rs) │
|
|
||||||
│ └── Builds highway connectivity map │
|
|
||||||
│ └── Collects building footprints │
|
|
||||||
│ │
|
|
||||||
│ [5/7] Process Terrain + Elements (data_processing.rs) │
|
|
||||||
│ └── Iterates ALL elements sequentially │
|
|
||||||
│ └── Calls element_processing/* for each element type │
|
|
||||||
│ └── Places blocks via WorldEditor │
|
|
||||||
│ │
|
|
||||||
│ [6/7] Generate Ground (data_processing.rs) │
|
|
||||||
│ └── Iterates ALL blocks in bbox │
|
|
||||||
│ └── Sets grass, dirt, stone, bedrock layers │
|
|
||||||
│ │
|
|
||||||
│ [7/7] Save World (world_editor/mod.rs → java.rs) │
|
|
||||||
│ └── Iterates ALL regions in memory │
|
|
||||||
│ └── Writes .mca files in parallel │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Data Structures and Memory Usage
|
|
||||||
|
|
||||||
#### WorldToModify (world_editor/common.rs)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct WorldToModify {
|
|
||||||
pub regions: FnvHashMap<(i32, i32), RegionToModify>, // Key: (region_x, region_z)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegionToModify {
|
|
||||||
pub chunks: FnvHashMap<(i32, i32), ChunkToModify>, // 32×32 chunks per region
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ChunkToModify {
|
|
||||||
pub sections: FnvHashMap<i8, SectionToModify>, // 24 sections per chunk (-4 to 19)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SectionToModify {
|
|
||||||
pub blocks: [Block; 4096], // 16×16×16 = 4096 blocks
|
|
||||||
pub properties: FnvHashMap<usize, Value>, // Block properties (stairs, slabs, etc.)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Memory estimate per region:**
|
|
||||||
- Section: ~4KB (blocks) + ~variable (properties)
|
|
||||||
- Chunk: ~24 sections × 4KB = ~96KB minimum, typically ~200-500KB with properties
|
|
||||||
- Region: ~1024 chunks × 300KB = **~300MB per region**
|
|
||||||
- **For a 10×10 region area: ~30GB of memory required!**
|
|
||||||
|
|
||||||
#### Why Elements Are "Scattered"
|
|
||||||
|
|
||||||
The current design processes elements in OSM priority order (entrance → building → highway → waterway → water → barrier → other), NOT by spatial location. This means:
|
|
||||||
|
|
||||||
1. A building in region (0,0) might be followed by a highway in region (5,5)
|
|
||||||
2. Each `set_block()` call potentially accesses different regions
|
|
||||||
3. ALL regions must remain in memory until the end because any element might touch any region
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proposed Architecture: Region-Based Parallel Processing
|
|
||||||
|
|
||||||
### Core Concept
|
|
||||||
|
|
||||||
Split the user-selected area into **processing units** of **1 Minecraft region each** (512×512 blocks = 32×32 chunks). Process each unit independently in parallel, then flush to disk immediately.
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ PROPOSED PARALLEL PROCESSING FLOW │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ GLOBAL PHASE (Run Once for Entire Area) │
|
|
||||||
│ ═══════════════════════════════════════ │
|
|
||||||
│ │
|
|
||||||
│ [1] Fetch Elevation Data for ENTIRE bbox │
|
|
||||||
│ └── Must be consistent across all units │
|
|
||||||
│ └── Store as shared read-only Arc<Ground> │
|
|
||||||
│ │
|
|
||||||
│ [2] Compute Processing Unit Grid │
|
|
||||||
│ └── Divide bbox into N×N region units │
|
|
||||||
│ └── Create sub-bboxes with small overlap for boundary elements │
|
|
||||||
│ │
|
|
||||||
│ PARALLEL PHASE (Per Processing Unit) │
|
|
||||||
│ ═════════════════════════════════════ │
|
|
||||||
│ │
|
|
||||||
│ For each processing unit (in parallel, using N-1 CPU cores): │
|
|
||||||
│ │
|
|
||||||
│ [3] Fetch OSM Data for Unit's Sub-BBox │
|
|
||||||
│ └── Separate Overpass API query per unit │
|
|
||||||
│ └── Include small buffer zone for boundary elements │
|
|
||||||
│ │
|
|
||||||
│ [4] Parse & Clip Elements to Unit Bounds │
|
|
||||||
│ └── Same as current, but for smaller area │
|
|
||||||
│ │
|
|
||||||
│ [5] Pre-compute Flood Fills │
|
|
||||||
│ └── Only for elements in this unit │
|
|
||||||
│ │
|
|
||||||
│ [6] Process Elements │
|
|
||||||
│ └── Generate buildings, roads, etc. │
|
|
||||||
│ └── Use deterministic RNG keyed by element ID │
|
|
||||||
│ │
|
|
||||||
│ [7] Generate Ground Layer │
|
|
||||||
│ └── Only for this unit's blocks │
|
|
||||||
│ │
|
|
||||||
│ [8] Save Regions to Disk │
|
|
||||||
│ └── Write .mca files immediately │
|
|
||||||
│ └── FREE MEMORY for this unit │
|
|
||||||
│ │
|
|
||||||
│ FINALIZATION PHASE │
|
|
||||||
│ ══════════════════ │
|
|
||||||
│ │
|
|
||||||
│ [9] Wait for all units to complete │
|
|
||||||
│ [10] Generate map preview (optional) │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critical Considerations
|
|
||||||
|
|
||||||
### 1. Deterministic Randomness ✅ ALREADY IMPLEMENTED
|
|
||||||
|
|
||||||
The codebase already has `deterministic_rng.rs` which provides:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Creates RNG seeded by element ID - same element always produces same random values
|
|
||||||
pub fn element_rng(element_id: u64) -> ChaCha8Rng {
|
|
||||||
ChaCha8Rng::seed_from_u64(element_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For coordinate-based randomness
|
|
||||||
pub fn coord_rng(x: i32, z: i32, element_id: u64) -> ChaCha8Rng
|
|
||||||
```
|
|
||||||
|
|
||||||
**Impact on buildings crossing boundaries:**
|
|
||||||
- Building colors are chosen using `element_rng(element.id)` in buildings.rs
|
|
||||||
- Even if a building is processed in two different units, SAME element ID → SAME color
|
|
||||||
- The existing implementation already supports this use case!
|
|
||||||
|
|
||||||
**Files using deterministic RNG:**
|
|
||||||
- `element_processing/buildings.rs` - wall colors, window styles, accent blocks
|
|
||||||
- `element_processing/natural.rs` - grass/flower distribution
|
|
||||||
- `element_processing/tree.rs` - tree variations
|
|
||||||
|
|
||||||
### 2. Elevation Data Consistency ⚠️ REQUIRES CHANGES
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- Elevation is fetched once in `ground.rs` → `Ground::new_enabled()`
|
|
||||||
- Height grid dimensions match the world's XZ dimensions
|
|
||||||
- Lookup uses relative coordinates: `ground.level(XZPoint::new(x - min_x, z - min_z))`
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- If each unit downloads its own elevation tiles, slight differences in tile boundaries or interpolation could cause height discontinuities at unit boundaries
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. **Download elevation ONCE for the entire area** before parallel processing starts
|
|
||||||
2. Pass `Arc<Ground>` (read-only) to all processing units
|
|
||||||
3. The `Ground::level()` function already uses world-relative coordinates, so no changes needed
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Proposed: Global elevation fetch before parallel processing
|
|
||||||
let global_ground = Arc::new(Ground::new_enabled(&args.bbox, args.scale, args.ground_level));
|
|
||||||
|
|
||||||
// Each processing unit receives a clone of the Arc
|
|
||||||
for unit in processing_units {
|
|
||||||
let ground_ref = Arc::clone(&global_ground);
|
|
||||||
// spawn task with ground_ref
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Element Clipping ⚠️ REQUIRES NEW LOGIC
|
|
||||||
|
|
||||||
**Current clipping (clipping.rs):**
|
|
||||||
- Uses Sutherland-Hodgman algorithm to clip polygons to user's bbox
|
|
||||||
- Works on the OUTER boundary of the entire selected area
|
|
||||||
|
|
||||||
**New requirement:**
|
|
||||||
- Need to clip elements to each processing unit's internal boundary
|
|
||||||
- But with OVERLAP to handle elements that straddle unit boundaries
|
|
||||||
|
|
||||||
**Proposed approach:**
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ UNIT BOUNDARY HANDLING │
|
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ Example: 4 processing units arranged in a 2×2 grid │
|
|
||||||
│ │
|
|
||||||
│ Unit A │ Unit B │
|
|
||||||
│ (regions 0,0-1,1) │ (regions 2,0-3,1) │
|
|
||||||
│ │ │
|
|
||||||
│ ┌──────────────────┼──────────────────┐ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ ████████ │ │ ← Building straddles │
|
|
||||||
│ │ █ BLD █─────┼──────────────────│ Unit A and B │
|
|
||||||
│ │ ████████ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ ├──────────────────┼──────────────────┤ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ └──────────────────┴──────────────────┘ │
|
|
||||||
│ Unit C │ Unit D │
|
|
||||||
│ (regions 0,2-1,3) │ (regions 2,2-3,3) │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Strategy for boundary elements:**
|
|
||||||
|
|
||||||
1. **Expanded Fetch BBox**: Each unit fetches OSM data with a buffer zone (e.g., +256 blocks)
|
|
||||||
2. **Clip to Processing BBox**: Clip elements to the unit's actual processing bounds
|
|
||||||
3. **Process Normally**: Elements partially in the unit are still processed, just clipped
|
|
||||||
4. **Deterministic Results**: Same element in adjacent units produces identical blocks due to RNG seeding
|
|
||||||
|
|
||||||
**Example: Building straddling Unit A and B**
|
|
||||||
|
|
||||||
| Step | Unit A | Unit B |
|
|
||||||
|------|--------|--------|
|
|
||||||
| Fetch | Gets building (with buffer) | Gets building (with buffer) |
|
|
||||||
| Clip | Clips to Unit A bounds → left half | Clips to Unit B bounds → right half |
|
|
||||||
| Color | `element_rng(building_id)` → BLUE | `element_rng(building_id)` → BLUE |
|
|
||||||
| Place | Places left half in blue | Places right half in blue |
|
|
||||||
| **Result** | **Seamless blue building across boundary** |
|
|
||||||
|
|
||||||
### 4. OSM Data Downloading Strategy ⚠️ REQUIRES CAREFUL DESIGN
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
|
|
||||||
#### Option A: Download Once, Distribute Elements (RECOMMENDED)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ │
|
|
||||||
│ [1] Download ALL OSM data for entire bbox (single API call) │
|
|
||||||
│ [2] Parse into ProcessedElements │
|
|
||||||
│ [3] For each processing unit: │
|
|
||||||
│ └── Filter elements that intersect unit's bbox │
|
|
||||||
│ └── Clip filtered elements to unit bounds │
|
|
||||||
│ └── Send to parallel processor │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Single Overpass API call (respects rate limits)
|
|
||||||
- No duplicate data transfer
|
|
||||||
- Elements are already parsed, just need filtering
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Must keep all elements in memory during distribution phase
|
|
||||||
- For very large areas, this might still be memory-intensive
|
|
||||||
|
|
||||||
#### Option B: Download Per Unit (Simpler, Higher Bandwidth)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ │
|
|
||||||
│ For each processing unit (sequentially or with rate limiting): │
|
|
||||||
│ [1] Download OSM data for unit's expanded bbox │
|
|
||||||
│ [2] Parse into ProcessedElements │
|
|
||||||
│ [3] Send to parallel processor │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Lower peak memory usage
|
|
||||||
- Simpler code structure
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Multiple API calls (may hit rate limits)
|
|
||||||
- Duplicate data transfer for overlapping areas
|
|
||||||
- Slower due to network latency
|
|
||||||
|
|
||||||
#### Recommendation: Option A with Streaming
|
|
||||||
|
|
||||||
Download once, but use a streaming approach to distribute elements to units:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Pseudo-code for element distribution
|
|
||||||
fn distribute_elements_to_units(
|
|
||||||
elements: Vec<ProcessedElement>,
|
|
||||||
units: &[ProcessingUnit],
|
|
||||||
) -> Vec<Vec<ProcessedElement>> {
|
|
||||||
let mut unit_elements = vec![Vec::new(); units.len()];
|
|
||||||
|
|
||||||
for element in elements {
|
|
||||||
let element_bbox = compute_element_bbox(&element);
|
|
||||||
for (i, unit) in units.iter().enumerate() {
|
|
||||||
if unit.expanded_bbox.intersects(&element_bbox) {
|
|
||||||
// Clone element for each unit that needs it
|
|
||||||
// (or use Arc for large elements)
|
|
||||||
unit_elements[i].push(element.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unit_elements
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Flood Fill Cache ⚠️ REQUIRES CHANGES
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- `FloodFillCache::precompute()` runs in parallel for ALL elements
|
|
||||||
- Results are stored in a `FnvHashMap<u64, Vec<(i32, i32)>>`
|
|
||||||
- Cache is consumed during sequential element processing
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- If we process units in parallel, each unit needs its own flood fill cache
|
|
||||||
- But we don't want to re-compute the same flood fills multiple times
|
|
||||||
|
|
||||||
**Solution A: Per-Unit Flood Fill (Simpler)**
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Each unit computes flood fills only for its elements
|
|
||||||
fn process_unit(unit_elements: Vec<ProcessedElement>) {
|
|
||||||
let flood_fill_cache = FloodFillCache::precompute(&unit_elements, timeout);
|
|
||||||
// Process elements using this cache
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:** Simple, no coordination needed
|
|
||||||
**Cons:** Elements at boundaries may be flood-filled twice
|
|
||||||
|
|
||||||
**Solution B: Global Flood Fill + Distribution (More Complex)**
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Compute flood fills globally, then distribute to units
|
|
||||||
let global_cache = FloodFillCache::precompute(&all_elements, timeout);
|
|
||||||
|
|
||||||
// For each unit, create a view into the global cache
|
|
||||||
let unit_caches: Vec<_> = units.iter()
|
|
||||||
.map(|unit| global_cache.filter_for_bbox(&unit.bbox))
|
|
||||||
.collect();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Recommendation:** Start with Solution A. The overhead of re-computing some flood fills at boundaries is acceptable given the simplicity.
|
|
||||||
|
|
||||||
### 6. Building Footprints Bitmap ⚠️ REQUIRES CHANGES
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- `BuildingFootprintBitmap` is a memory-efficient bitmap covering the entire world
|
|
||||||
- Used to prevent trees from spawning inside buildings
|
|
||||||
- Created AFTER flood fill precomputation
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- With parallel processing, each unit only knows about buildings in its own area
|
|
||||||
- A tree in Unit B might spawn inside a building that exists in Unit A (near boundary)
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
- Compute building footprints GLOBALLY before parallel processing
|
|
||||||
- Use `Arc<BuildingFootprintBitmap>` shared across all units (read-only)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Global building footprint computation
|
|
||||||
let all_building_coords = compute_all_building_footprints(&all_elements, &global_xzbbox);
|
|
||||||
let global_footprints = Arc::new(BuildingFootprintBitmap::from(all_building_coords));
|
|
||||||
|
|
||||||
// Each unit receives Arc clone
|
|
||||||
for unit in units {
|
|
||||||
let footprints = Arc::clone(&global_footprints);
|
|
||||||
// spawn task
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Highway Connectivity ⚠️ REQUIRES CHANGES
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- `highways::build_highway_connectivity_map()` creates a map of connected highway segments
|
|
||||||
- Used for intersection detection and road marking placement
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- Highway segments crossing unit boundaries won't see their full connectivity
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
- Build highway connectivity map GLOBALLY before parallel processing
|
|
||||||
- Pass as `Arc<HighwayConnectivityMap>` to all units
|
|
||||||
|
|
||||||
### 8. Water Areas and Ring Merging ✅ ALREADY SUPPORTED
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- Water relations contain multiple ways that must be merged into closed rings
|
|
||||||
- `merge_way_segments()` in water_areas.rs handles this
|
|
||||||
- **Clipping happens AFTER ring merging** via `clip_water_ring_to_bbox()`
|
|
||||||
- Water uses `inverse_floodfill()` which iterates over bounding box (not flood fill)
|
|
||||||
|
|
||||||
**Why water CAN be clipped per-unit:**
|
|
||||||
1. Ring merging happens on the UNCLIPPED ways (preserved in osm_parser.rs)
|
|
||||||
2. After merging, `clip_water_ring_to_bbox()` clips the assembled polygon
|
|
||||||
3. The `inverse_floodfill` algorithm iterates block-by-block within bounds
|
|
||||||
4. Each unit can independently clip and fill its portion of a water body
|
|
||||||
|
|
||||||
**No special handling needed** - water relations work the same as other elements:
|
|
||||||
- Distribute relation to units that intersect its bbox
|
|
||||||
- Each unit clips to its own bounds
|
|
||||||
- Each unit fills its portion independently
|
|
||||||
|
|
||||||
### 9. Element Priority Order ⚠️ MUST BE PRESERVED
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- Elements are sorted by priority before processing (osm_parser.rs):
|
|
||||||
```rust
|
|
||||||
const PRIORITY_ORDER: [&str; 6] = [
|
|
||||||
"entrance", "building", "highway", "waterway", "water", "barrier",
|
|
||||||
];
|
|
||||||
```
|
|
||||||
- This ensures entrances are placed before buildings (so doors work)
|
|
||||||
- Buildings before highways (so sidewalks don't overwrite buildings)
|
|
||||||
|
|
||||||
**Requirement:**
|
|
||||||
- Each unit must process its elements in the SAME priority order
|
|
||||||
- This is natural: just sort the unit's elements the same way
|
|
||||||
|
|
||||||
### 10. SPONGE Block as Placeholder ⚠️ MINOR CONSIDERATION
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- `SPONGE` block is used as a blacklist marker in some places
|
|
||||||
- Example: `editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));`
|
|
||||||
- Prevents certain blocks from overwriting sponge blocks
|
|
||||||
|
|
||||||
**Impact on parallel processing:**
|
|
||||||
- None - this is a per-block check, not cross-region coordination
|
|
||||||
- Each unit handles its own sponge blocks independently
|
|
||||||
|
|
||||||
### 11. Tree Placement and Building Footprints ⚠️ REQUIRES GLOBAL FOOTPRINTS
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- Trees check `building_footprints.contains(x, z)` before spawning
|
|
||||||
- Prevents trees from appearing inside buildings
|
|
||||||
- Uses `coord_rng(x, z, element_id)` for deterministic placement
|
|
||||||
|
|
||||||
**Problem with per-unit footprints:**
|
|
||||||
- A tree near a unit boundary might not see a building from the adjacent unit
|
|
||||||
- Could spawn a tree inside a building that exists in neighbor unit
|
|
||||||
|
|
||||||
**Solution (already planned):**
|
|
||||||
- Compute building footprints GLOBALLY before parallel processing
|
|
||||||
- Pass as `Arc<BuildingFootprintBitmap>` to all units
|
|
||||||
- Tree placement will correctly avoid all buildings
|
|
||||||
|
|
||||||
### 12. Relations with Multiple Members Across Units ⚠️ REQUIRES CAREFUL HANDLING
|
|
||||||
|
|
||||||
**Current behavior:**
|
|
||||||
- Relations (buildings, landuse, leisure, natural) process each member way
|
|
||||||
- Member ways can be scattered across the entire bbox
|
|
||||||
|
|
||||||
**Example: Building relation with courtyard**
|
|
||||||
```
|
|
||||||
Building Relation:
|
|
||||||
- Outer way 1 (in Unit A)
|
|
||||||
- Outer way 2 (in Unit A and B) ← straddles boundary
|
|
||||||
- Inner way (courtyard, in Unit A)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Strategy:**
|
|
||||||
1. Distribute entire relation to all units that any member touches
|
|
||||||
2. Each unit clips all members to its bounds
|
|
||||||
3. Each unit processes the clipped relation independently
|
|
||||||
4. Deterministic RNG ensures consistent colors/styles
|
|
||||||
|
|
||||||
**Important:** The relation-level tags (e.g., `building:levels`) must be preserved for all units processing that relation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proposed Processing Unit Structure
|
|
||||||
|
|
||||||
### ProcessingUnit Definition
|
|
||||||
|
|
||||||
```rust
|
|
||||||
struct ProcessingUnit {
|
|
||||||
/// Which region this unit covers (1 region per unit)
|
|
||||||
region_x: i32,
|
|
||||||
region_z: i32,
|
|
||||||
|
|
||||||
/// Minecraft coordinate bounds for this unit (512×512 blocks)
|
|
||||||
min_x: i32, // region_x * 512
|
|
||||||
max_x: i32, // region_x * 512 + 511
|
|
||||||
min_z: i32, // region_z * 512
|
|
||||||
max_z: i32, // region_z * 512 + 511
|
|
||||||
|
|
||||||
/// Expanded bounds for element fetching (includes buffer for boundary elements)
|
|
||||||
fetch_min_x: i32,
|
|
||||||
fetch_max_x: i32,
|
|
||||||
fetch_min_z: i32,
|
|
||||||
fetch_max_z: i32,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Grid Calculation
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn compute_processing_units(
|
|
||||||
global_xzbbox: &XZBBox,
|
|
||||||
buffer_blocks: i32, // e.g., 64-128 blocks overlap
|
|
||||||
) -> Vec<ProcessingUnit> {
|
|
||||||
let blocks_per_region = 512; // 32 chunks × 16 blocks
|
|
||||||
|
|
||||||
// Calculate which regions are covered by the bbox
|
|
||||||
let min_region_x = global_xzbbox.min_x() >> 9; // divide by 512
|
|
||||||
let max_region_x = global_xzbbox.max_x() >> 9;
|
|
||||||
let min_region_z = global_xzbbox.min_z() >> 9;
|
|
||||||
let max_region_z = global_xzbbox.max_z() >> 9;
|
|
||||||
|
|
||||||
let mut units = Vec::new();
|
|
||||||
|
|
||||||
// Create one unit per region
|
|
||||||
for rx in min_region_x..=max_region_x {
|
|
||||||
for rz in min_region_z..=max_region_z {
|
|
||||||
// Compute Minecraft coordinate bounds for this region
|
|
||||||
let min_x = rx * blocks_per_region;
|
|
||||||
let max_x = min_x + blocks_per_region - 1;
|
|
||||||
let min_z = rz * blocks_per_region;
|
|
||||||
let max_z = min_z + blocks_per_region - 1;
|
|
||||||
|
|
||||||
// Add buffer for fetch bounds (clamped to global bbox)
|
|
||||||
let fetch_min_x = (min_x - buffer_blocks).max(global_xzbbox.min_x());
|
|
||||||
let fetch_max_x = (max_x + buffer_blocks).min(global_xzbbox.max_x());
|
|
||||||
let fetch_min_z = (min_z - buffer_blocks).max(global_xzbbox.min_z());
|
|
||||||
let fetch_max_z = (max_z + buffer_blocks).min(global_xzbbox.max_z());
|
|
||||||
|
|
||||||
units.push(ProcessingUnit {
|
|
||||||
region_x: rx,
|
|
||||||
region_z: rz,
|
|
||||||
min_x, max_x, min_z, max_z,
|
|
||||||
fetch_min_x, fetch_max_x, fetch_min_z, fetch_max_z,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
units
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parallel Execution Strategy
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn process_units_parallel(
|
|
||||||
units: Vec<ProcessingUnit>,
|
|
||||||
elements: &[ProcessedElement],
|
|
||||||
global_ground: Arc<Ground>,
|
|
||||||
global_building_footprints: Arc<BuildingFootprintBitmap>,
|
|
||||||
global_highway_connectivity: Arc<HighwayConnectivityMap>,
|
|
||||||
args: &Args,
|
|
||||||
) {
|
|
||||||
// Use CPU-1 cores for parallel processing
|
|
||||||
let num_threads = std::thread::available_parallelism()
|
|
||||||
.map(|n| n.get().saturating_sub(1).max(1))
|
|
||||||
.unwrap_or(1);
|
|
||||||
|
|
||||||
units.into_par_iter()
|
|
||||||
.with_min_len(1) // Process 1 unit per task
|
|
||||||
.for_each(|unit| {
|
|
||||||
// 1. Filter elements that intersect this unit's fetch bounds
|
|
||||||
let unit_elements = filter_elements_for_unit(elements, &unit);
|
|
||||||
|
|
||||||
// 2. Clip elements to unit's actual bounds
|
|
||||||
let clipped_elements = clip_elements_to_unit(unit_elements, &unit);
|
|
||||||
|
|
||||||
// 3. Create per-unit structures
|
|
||||||
let unit_xzbbox = XZBBox::new(unit.min_x, unit.max_x, unit.min_z, unit.max_z);
|
|
||||||
let mut editor = WorldEditor::new(args.path.clone(), &unit_xzbbox, ...);
|
|
||||||
editor.set_ground(Arc::clone(&global_ground));
|
|
||||||
|
|
||||||
// 4. Pre-compute flood fills for this unit's elements
|
|
||||||
let flood_fill_cache = FloodFillCache::precompute(&clipped_elements, args.timeout.as_ref());
|
|
||||||
|
|
||||||
// 5. Process elements (same as current, just for this unit)
|
|
||||||
for element in clipped_elements {
|
|
||||||
process_element(&mut editor, &element, ...);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Generate ground layer for this unit
|
|
||||||
generate_ground_for_unit(&mut editor, &unit, &global_ground);
|
|
||||||
|
|
||||||
// 7. Save region immediately and FREE MEMORY
|
|
||||||
editor.save_single_region(unit.region_x, unit.region_z);
|
|
||||||
drop(editor); // Release memory
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Memory Usage Comparison
|
|
||||||
|
|
||||||
### Understanding Minecraft Region Sizes
|
|
||||||
|
|
||||||
```
|
|
||||||
1 Region = 32×32 chunks = 512×512 blocks (horizontally)
|
|
||||||
1 Chunk = 16×16×384 blocks (with sections from Y=-64 to Y=319)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Current Architecture (All Regions in Memory)
|
|
||||||
|
|
||||||
| Stage | Memory Usage |
|
|
||||||
|-------|--------------|
|
|
||||||
| OSM Data (parsed) | ~50-200 MB |
|
|
||||||
| Flood Fill Cache | ~100-500 MB |
|
|
||||||
| Building Footprints | ~10-50 MB |
|
|
||||||
| WorldToModify (all regions) | **~300 MB × N regions** |
|
|
||||||
| **Total for 100 regions** | **~30+ GB** |
|
|
||||||
|
|
||||||
### Unit Size Analysis
|
|
||||||
|
|
||||||
The optimal unit size depends on balancing:
|
|
||||||
1. **Memory per unit** - Larger units = more memory
|
|
||||||
2. **Parallelism overhead** - Smaller units = more coordination
|
|
||||||
3. **Boundary overhead** - More units = more elements processed multiple times
|
|
||||||
|
|
||||||
| Unit Size | Blocks | Memory per Unit | Parallel Units (8 cores) | Peak Memory |
|
|
||||||
|-----------|--------|-----------------|--------------------------|-------------|
|
|
||||||
| 1 region (32×32 chunks) | 512×512 | ~300 MB | 7 units | ~2.5 GB |
|
|
||||||
| 2×2 regions | 1024×1024 | ~1.2 GB | 7 units | ~9 GB |
|
|
||||||
| 4×4 regions | 2048×2048 | ~4.8 GB | 7 units | ~35 GB |
|
|
||||||
|
|
||||||
### Recommendation: 1 Region Per Unit
|
|
||||||
|
|
||||||
**1 region per unit is optimal because:**
|
|
||||||
|
|
||||||
1. **Lowest memory footprint** - Only ~300 MB per unit
|
|
||||||
2. **Natural alignment** - Regions are the atomic save unit in Minecraft (.mca files)
|
|
||||||
3. **Maximum parallelism** - More units = better CPU utilization
|
|
||||||
4. **Simple boundary logic** - No partial region handling
|
|
||||||
|
|
||||||
**Memory calculation for 7 parallel units (8-core CPU, using 7):**
|
|
||||||
- Per-unit WorldToModify: ~300 MB
|
|
||||||
- Per-unit flood fill cache: ~50 MB
|
|
||||||
- Per-unit OSM elements: ~20 MB
|
|
||||||
- **Peak memory: ~370 MB × 7 = ~2.6 GB**
|
|
||||||
|
|
||||||
Plus global shared data:
|
|
||||||
- Elevation data: ~50-100 MB
|
|
||||||
- Building footprints: ~10-50 MB
|
|
||||||
- Highway connectivity: ~20-50 MB
|
|
||||||
|
|
||||||
**Total peak: ~3 GB** (vs ~30 GB for 100 regions currently!)
|
|
||||||
|
|
||||||
### Why Not Smaller Than 1 Region?
|
|
||||||
|
|
||||||
- Regions are the minimum save unit for Minecraft
|
|
||||||
- Going smaller would require buffering partial regions
|
|
||||||
- No memory benefit (still need full region in memory to save)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 1: Refactor Global Data Preparation
|
|
||||||
|
|
||||||
**Goal:** Extract global computations that must run before parallel processing
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Move elevation fetching to a separate global phase
|
|
||||||
2. Move building footprint collection to global phase
|
|
||||||
3. Move highway connectivity map building to global phase
|
|
||||||
4. Create shared data structures with `Arc<T>`
|
|
||||||
|
|
||||||
**Files affected:**
|
|
||||||
- `data_processing.rs` - restructure `generate_world_with_options()`
|
|
||||||
- `ground.rs` - no changes, already returns `Ground`
|
|
||||||
- `floodfill_cache.rs` - add method to collect building footprints globally
|
|
||||||
- `element_processing/highways.rs` - extract connectivity map building
|
|
||||||
|
|
||||||
### Phase 2: Implement Processing Unit Grid
|
|
||||||
|
|
||||||
**Goal:** Add logic to divide the world into processing units
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Create `processing_unit.rs` module
|
|
||||||
2. Implement grid computation
|
|
||||||
3. Implement element-to-unit distribution
|
|
||||||
4. Add unit-level bounding box clipping
|
|
||||||
|
|
||||||
**New files:**
|
|
||||||
- `src/processing_unit.rs`
|
|
||||||
|
|
||||||
### Phase 3: Parallelize Unit Processing
|
|
||||||
|
|
||||||
**Goal:** Process units in parallel using rayon
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Create per-unit WorldEditor instances
|
|
||||||
2. Implement unit processing function
|
|
||||||
3. Add parallel execution with CPU cap
|
|
||||||
4. Implement region saving after unit completion
|
|
||||||
|
|
||||||
**Files affected:**
|
|
||||||
- `data_processing.rs` - main parallel loop
|
|
||||||
- `world_editor/mod.rs` - support per-unit saving
|
|
||||||
|
|
||||||
### Phase 4: Handle Boundary Cases
|
|
||||||
|
|
||||||
**Goal:** Ensure seamless results across unit boundaries
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Verify deterministic RNG produces identical results
|
|
||||||
2. Implement special handling for large water bodies
|
|
||||||
3. Add boundary verification tests
|
|
||||||
4. Optimize overlap buffer size
|
|
||||||
|
|
||||||
**Files affected:**
|
|
||||||
- `element_processing/water_areas.rs` - global water handling
|
|
||||||
- `clipping.rs` - potential optimizations
|
|
||||||
|
|
||||||
### Phase 5: Optimize Memory Management
|
|
||||||
|
|
||||||
**Goal:** Fine-tune memory usage and parallelism
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Implement memory pressure monitoring
|
|
||||||
2. Add dynamic unit size adjustment
|
|
||||||
3. Optimize flood fill cache memory
|
|
||||||
4. Profile and optimize hot paths
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
1. **Deterministic RNG Test**
|
|
||||||
- Process same building in two units
|
|
||||||
- Verify identical colors/styles
|
|
||||||
|
|
||||||
2. **Elevation Consistency Test**
|
|
||||||
- Check ground level at unit boundaries
|
|
||||||
- Verify no height discontinuities
|
|
||||||
|
|
||||||
3. **Clipping Accuracy Test**
|
|
||||||
- Verify elements clipped correctly at unit boundaries
|
|
||||||
- Check polygon integrity after clipping
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
1. **Small Area Test**
|
|
||||||
- Process 2×2 region area
|
|
||||||
- Verify world loads correctly in Minecraft
|
|
||||||
|
|
||||||
2. **Boundary Building Test**
|
|
||||||
- Create world with buildings at unit boundaries
|
|
||||||
- Verify buildings are complete and correctly colored
|
|
||||||
|
|
||||||
3. **Large Water Body Test**
|
|
||||||
- Process area with lake spanning multiple units
|
|
||||||
- Verify water body is continuous
|
|
||||||
|
|
||||||
### Performance Tests
|
|
||||||
|
|
||||||
1. **Memory Usage Test**
|
|
||||||
- Monitor peak memory during processing
|
|
||||||
- Compare with current architecture
|
|
||||||
|
|
||||||
2. **CPU Utilization Test**
|
|
||||||
- Verify parallel units use expected cores
|
|
||||||
- Measure speedup vs sequential processing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration Options
|
|
||||||
|
|
||||||
### Proposed CLI Arguments
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Number of CPU cores to use for parallel processing (default: available - 1)
|
|
||||||
/// Set to 1 to disable parallel processing
|
|
||||||
#[arg(long, default_value_t = 0)]
|
|
||||||
pub parallel_cores: usize,
|
|
||||||
|
|
||||||
/// Buffer size for boundary overlap in blocks (default: 64)
|
|
||||||
/// Larger values ensure buildings at boundaries are complete but increase processing time
|
|
||||||
#[arg(long, default_value_t = 64)]
|
|
||||||
pub boundary_buffer: i32,
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Risk Assessment
|
|
||||||
|
|
||||||
### High Risk
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|------------|
|
|
||||||
| Elevation discontinuities at boundaries | Use global elevation data (already planned) |
|
|
||||||
| Race conditions in file writing | Each unit writes different regions (no overlap) |
|
|
||||||
| Trees spawning inside buildings at boundaries | Use global building footprints bitmap |
|
|
||||||
|
|
||||||
### Medium Risk
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|------------|
|
|
||||||
| Overpass API rate limiting | Download once globally, distribute elements |
|
|
||||||
| Complex relations broken at boundaries | Distribute full relation to all touching units |
|
|
||||||
| Highway connectivity missing at boundaries | Build connectivity map globally |
|
|
||||||
|
|
||||||
### Low Risk
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|------------|
|
|
||||||
| Different random values at boundaries | Deterministic RNG already implemented |
|
|
||||||
| Performance regression | Benchmark before/after, make parallel optional |
|
|
||||||
| Water bodies split incorrectly | Water already supports clipping via `clip_water_ring_to_bbox` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Questions to Resolve
|
|
||||||
|
|
||||||
1. **Should we support Bedrock format with this change?**
|
|
||||||
- Bedrock writes to a single .mcworld file (LevelDB database)
|
|
||||||
- May need different handling (write to temp, merge at end)
|
|
||||||
- Could be deferred to a follow-up implementation
|
|
||||||
|
|
||||||
2. **What buffer size for boundary overlap?**
|
|
||||||
- Current thinking: 64-128 blocks should be sufficient
|
|
||||||
- Most buildings are smaller than this
|
|
||||||
- Larger buffers = more duplicate processing
|
|
||||||
|
|
||||||
3. **Should flood fills be computed globally or per-unit?**
|
|
||||||
- Per-unit is simpler and avoids coordination
|
|
||||||
- Some redundant computation at boundaries (acceptable)
|
|
||||||
- **Recommendation:** Start per-unit
|
|
||||||
|
|
||||||
4. **How to report progress across parallel units?**
|
|
||||||
- Current progress is linear (element by element)
|
|
||||||
- With parallel, need aggregated progress reporting
|
|
||||||
- Option: Track completed regions, report as percentage
|
|
||||||
|
|
||||||
5. **Should we limit parallelism based on available RAM?**
|
|
||||||
- Could detect system RAM and adjust parallel units
|
|
||||||
- Or just document memory requirements per parallel unit
|
|
||||||
- **Recommendation:** Start with CPU-1 cores, let users override
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The proposed parallel region processing architecture will:
|
|
||||||
|
|
||||||
1. ✅ **Reduce memory usage by ~90%** by processing 1 region at a time per unit (~300 MB vs ~30 GB for 100 regions)
|
|
||||||
2. ✅ **Utilize multiple CPU cores** through rayon-based parallel processing (CPU-1 cores)
|
|
||||||
3. ✅ **Maintain visual consistency** using deterministic RNG and global shared data
|
|
||||||
4. ✅ **Be backward compatible** with a `--no-parallel` flag for the current behavior
|
|
||||||
|
|
||||||
The main implementation work is:
|
|
||||||
- Refactoring to extract global computations (elevation, building footprints, highway connectivity)
|
|
||||||
- Adding element-to-unit distribution logic with proper clipping
|
|
||||||
- Per-unit WorldEditor instances with immediate region saving
|
|
||||||
|
|
||||||
**The design is simpler than originally thought** because:
|
|
||||||
- Water relations already support clipping (no special handling)
|
|
||||||
- Deterministic RNG already exists (no changes needed)
|
|
||||||
- Priority order is preserved naturally (just sort per-unit)
|
|
||||||
|
|
||||||
Estimated implementation effort: **3-4 weeks** for a fully tested solution.
|
|
||||||
21
src/args.rs
21
src/args.rs
@@ -59,22 +59,13 @@ pub struct Args {
|
|||||||
#[arg(long, value_parser = parse_duration)]
|
#[arg(long, value_parser = parse_duration)]
|
||||||
pub timeout: Option<Duration>,
|
pub timeout: Option<Duration>,
|
||||||
|
|
||||||
/// Number of parallel threads (0 = auto, uses available cores - 1)
|
/// Generate a top-down map preview image after world generation (optional)
|
||||||
#[arg(long, default_value_t = 0)]
|
|
||||||
pub threads: usize,
|
|
||||||
|
|
||||||
/// Number of regions to batch per processing unit (1 = one region, 2 = 2x2=4 regions, etc.)
|
|
||||||
/// Larger batches reduce element duplication overhead but use more memory per unit
|
|
||||||
#[arg(long, default_value_t = 2)]
|
|
||||||
pub region_batch_size: usize,
|
|
||||||
|
|
||||||
/// Disable parallel processing (process sequentially) - DEFAULT due to correctness issues
|
|
||||||
#[arg(long, default_value_t = true)]
|
|
||||||
pub no_parallel: bool,
|
|
||||||
|
|
||||||
/// Force parallel processing (experimental, may have visual bugs)
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub force_parallel: bool,
|
pub generate_map: bool,
|
||||||
|
|
||||||
|
/// Spawn point coordinates (lat, lng)
|
||||||
|
#[arg(skip)]
|
||||||
|
pub spawn_point: Option<(f64, f64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||||
|
|||||||
@@ -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))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -266,7 +266,6 @@ impl Block {
|
|||||||
185 => "quartz_stairs",
|
185 => "quartz_stairs",
|
||||||
186 => "polished_andesite_stairs",
|
186 => "polished_andesite_stairs",
|
||||||
187 => "nether_brick_stairs",
|
187 => "nether_brick_stairs",
|
||||||
188 => "fern",
|
|
||||||
_ => panic!("Invalid id"),
|
_ => panic!("Invalid id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,7 +697,6 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
|||||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
||||||
pub const FERN: Block = Block::new(188);
|
|
||||||
|
|
||||||
/// Maps a block to its corresponding stair variant
|
/// Maps a block to its corresponding stair variant
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub mod xzbbox;
|
mod xzbbox;
|
||||||
mod xzpoint;
|
mod xzpoint;
|
||||||
mod xzvector;
|
mod xzvector;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub mod rectangle;
|
mod rectangle;
|
||||||
mod xzbbox_enum;
|
mod xzbbox_enum;
|
||||||
|
|
||||||
pub use xzbbox_enum::XZBBox;
|
pub use xzbbox_enum::XZBBox;
|
||||||
|
|||||||
@@ -3,36 +3,18 @@ use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
|||||||
use crate::coordinate_system::cartesian::XZBBox;
|
use crate::coordinate_system::cartesian::XZBBox;
|
||||||
use crate::coordinate_system::geographic::LLBBox;
|
use crate::coordinate_system::geographic::LLBBox;
|
||||||
use crate::element_processing::*;
|
use crate::element_processing::*;
|
||||||
use crate::floodfill_cache::FloodFillCache;
|
|
||||||
use crate::ground::Ground;
|
use crate::ground::Ground;
|
||||||
use crate::map_renderer;
|
use crate::map_renderer;
|
||||||
use crate::osm_parser::ProcessedElement;
|
use crate::osm_parser::ProcessedElement;
|
||||||
use crate::parallel_processing::{
|
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready};
|
||||||
calculate_parallel_threads, compute_processing_units, distribute_elements_to_units_indices,
|
|
||||||
ParallelConfig, ProcessingStats,
|
|
||||||
};
|
|
||||||
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
use crate::telemetry::{send_log, LogLevel};
|
use crate::telemetry::{send_log, LogLevel};
|
||||||
use crate::unit_processing::{process_unit_refs, SharedProcessingData};
|
use crate::world_editor::WorldEditor;
|
||||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use rayon::prelude::*;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub const MIN_Y: i32 = -64;
|
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(
|
pub fn generate_world(
|
||||||
elements: Vec<ProcessedElement>,
|
elements: Vec<ProcessedElement>,
|
||||||
xzbbox: XZBBox,
|
xzbbox: XZBBox,
|
||||||
@@ -40,415 +22,21 @@ pub fn generate_world(
|
|||||||
ground: Ground,
|
ground: Ground,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Default to Java format when called from CLI
|
let mut editor: WorldEditor = WorldEditor::new(args.path.clone(), &xzbbox, llbbox);
|
||||||
let options = GenerationOptions {
|
|
||||||
path: args.path.clone(),
|
|
||||||
format: WorldFormat::JavaAnvil,
|
|
||||||
level_name: None,
|
|
||||||
spawn_point: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use sequential by default (parallel has correctness issues)
|
println!("{} Processing data...", "[4/7]".bold());
|
||||||
// Use --force-parallel to enable experimental parallel mode
|
|
||||||
let parallel_config = if args.force_parallel {
|
|
||||||
ParallelConfig {
|
|
||||||
num_threads: args.threads,
|
|
||||||
buffer_blocks: 64,
|
|
||||||
enabled: true,
|
|
||||||
region_batch_size: args.region_batch_size,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ParallelConfig::sequential()
|
|
||||||
};
|
|
||||||
|
|
||||||
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options, parallel_config)
|
// Build highway connectivity map once before processing
|
||||||
.map(|_| ())
|
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate world with explicit format options (used by GUI for Bedrock support)
|
// Set ground reference in the editor to enable elevation-aware block placement
|
||||||
pub fn generate_world_with_options(
|
editor.set_ground(&ground);
|
||||||
elements: Vec<ProcessedElement>,
|
|
||||||
xzbbox: XZBBox,
|
|
||||||
llbbox: LLBBox,
|
|
||||||
ground: Ground,
|
|
||||||
args: &Args,
|
|
||||||
options: GenerationOptions,
|
|
||||||
parallel_config: ParallelConfig,
|
|
||||||
) -> Result<PathBuf, String> {
|
|
||||||
let _output_path = options.path.clone();
|
|
||||||
let _world_format = options.format;
|
|
||||||
|
|
||||||
// Determine if we should use parallel processing
|
|
||||||
let num_threads = calculate_parallel_threads(parallel_config.num_threads);
|
|
||||||
|
|
||||||
// Calculate region count to decide if parallel is worth the overhead
|
|
||||||
let min_region_x = xzbbox.min_x() >> 9;
|
|
||||||
let max_region_x = xzbbox.max_x() >> 9;
|
|
||||||
let min_region_z = xzbbox.min_z() >> 9;
|
|
||||||
let max_region_z = xzbbox.max_z() >> 9;
|
|
||||||
let region_count = ((max_region_x - min_region_x + 1) * (max_region_z - min_region_z + 1)) as usize;
|
|
||||||
|
|
||||||
// Auto-disable parallel for small areas (< 6 regions) - overhead isn't worth it
|
|
||||||
// User can still force parallel with explicit --threads > 1 and region count check
|
|
||||||
let use_parallel = parallel_config.enabled && num_threads > 1 && region_count >= 6;
|
|
||||||
|
|
||||||
let mode_reason = if !parallel_config.enabled {
|
|
||||||
"disabled by --no-parallel"
|
|
||||||
} else if num_threads <= 1 {
|
|
||||||
"single thread"
|
|
||||||
} else if region_count < 6 {
|
|
||||||
"small area (< 6 regions)"
|
|
||||||
} else {
|
|
||||||
"parallel"
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{} Processing data ({} mode, {} thread(s), {} regions)...",
|
|
||||||
"[4/7]".bold(),
|
|
||||||
if use_parallel { "parallel" } else { "sequential" },
|
|
||||||
num_threads,
|
|
||||||
region_count
|
|
||||||
);
|
|
||||||
|
|
||||||
if !use_parallel && parallel_config.enabled && region_count < 6 {
|
|
||||||
println!(" (auto-selected sequential: {})", mode_reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build highway connectivity map once before processing (needed for all units)
|
|
||||||
let highway_connectivity = Arc::new(highways::build_highway_connectivity_map(&elements));
|
|
||||||
|
|
||||||
let ground = Arc::new(ground);
|
|
||||||
|
|
||||||
println!("{} Processing terrain...", "[5/7]".bold());
|
println!("{} Processing terrain...", "[5/7]".bold());
|
||||||
emit_gui_progress_update(25.0, "Processing terrain...");
|
emit_gui_progress_update(25.0, "Processing terrain...");
|
||||||
|
|
||||||
// Pre-compute all flood fills in parallel for better CPU utilization
|
|
||||||
let flood_fill_cache = Arc::new(FloodFillCache::precompute(
|
|
||||||
&elements,
|
|
||||||
args.timeout.as_ref(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Collect building footprints to prevent trees from spawning inside buildings
|
|
||||||
let building_footprints =
|
|
||||||
Arc::new(flood_fill_cache.collect_building_footprints(&elements, &xzbbox));
|
|
||||||
|
|
||||||
if use_parallel {
|
|
||||||
// === PARALLEL PROCESSING PATH ===
|
|
||||||
generate_world_parallel(
|
|
||||||
elements,
|
|
||||||
xzbbox,
|
|
||||||
llbbox,
|
|
||||||
ground,
|
|
||||||
highway_connectivity,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
args,
|
|
||||||
options,
|
|
||||||
parallel_config,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// === SEQUENTIAL PROCESSING PATH (original logic) ===
|
|
||||||
generate_world_sequential(
|
|
||||||
elements,
|
|
||||||
xzbbox,
|
|
||||||
llbbox,
|
|
||||||
ground,
|
|
||||||
highway_connectivity,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
args,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parallel world generation - processes regions in parallel, saving each immediately
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn generate_world_parallel(
|
|
||||||
elements: Vec<ProcessedElement>,
|
|
||||||
xzbbox: XZBBox,
|
|
||||||
llbbox: LLBBox,
|
|
||||||
ground: Arc<Ground>,
|
|
||||||
highway_connectivity: Arc<highways::HighwayConnectivityMap>,
|
|
||||||
flood_fill_cache: Arc<FloodFillCache>,
|
|
||||||
building_footprints: Arc<crate::floodfill_cache::BuildingFootprintBitmap>,
|
|
||||||
args: &Args,
|
|
||||||
options: GenerationOptions,
|
|
||||||
parallel_config: ParallelConfig,
|
|
||||||
) -> Result<PathBuf, String> {
|
|
||||||
let output_path = options.path.clone();
|
|
||||||
let world_format = options.format;
|
|
||||||
|
|
||||||
// Compute processing units (one or more regions per unit depending on batch size)
|
|
||||||
let units = compute_processing_units(
|
|
||||||
&xzbbox,
|
|
||||||
parallel_config.buffer_blocks,
|
|
||||||
parallel_config.region_batch_size
|
|
||||||
);
|
|
||||||
let total_units = units.len();
|
|
||||||
|
|
||||||
println!(
|
|
||||||
" {} unit(s) to process across {} thread(s) (batch size: {})",
|
|
||||||
total_units,
|
|
||||||
calculate_parallel_threads(parallel_config.num_threads),
|
|
||||||
parallel_config.region_batch_size
|
|
||||||
);
|
|
||||||
|
|
||||||
// Distribute elements to units based on spatial intersection
|
|
||||||
// Returns indices into the elements vector for each unit
|
|
||||||
let unit_element_indices = distribute_elements_to_units_indices(&elements, &units);
|
|
||||||
|
|
||||||
// Wrap elements in Arc for shared access across threads
|
|
||||||
let elements = Arc::new(elements);
|
|
||||||
|
|
||||||
// Create shared data for all units
|
|
||||||
let shared = Arc::new(SharedProcessingData {
|
|
||||||
ground: Arc::clone(&ground),
|
|
||||||
highway_connectivity: Arc::clone(&highway_connectivity),
|
|
||||||
building_footprints: Arc::clone(&building_footprints),
|
|
||||||
floodfill_cache: Arc::clone(&flood_fill_cache),
|
|
||||||
llbbox,
|
|
||||||
world_dir: options.path.clone(),
|
|
||||||
format: options.format,
|
|
||||||
level_name: options.level_name.clone(),
|
|
||||||
terrain_enabled: args.terrain,
|
|
||||||
ground_level: args.ground_level,
|
|
||||||
fill_ground: args.fillground,
|
|
||||||
interior: args.interior,
|
|
||||||
roof: args.roof,
|
|
||||||
debug: args.debug,
|
|
||||||
timeout: args.timeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up progress tracking
|
|
||||||
let stats = Arc::new(ProcessingStats::new(total_units, 0));
|
|
||||||
let process_pb = ProgressBar::new(total_units as u64);
|
|
||||||
process_pb.set_style(
|
|
||||||
ProgressStyle::default_bar()
|
|
||||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} regions ({eta})")
|
|
||||||
.unwrap()
|
|
||||||
.progress_chars("█▓░"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process units in parallel
|
|
||||||
println!("{} Processing regions in parallel...", "[5/7]".bold());
|
|
||||||
|
|
||||||
// Log element distribution stats
|
|
||||||
let total_element_refs: usize = unit_element_indices.iter().map(|v| v.len()).sum();
|
|
||||||
let avg_elements_per_unit = total_element_refs as f64 / total_units as f64;
|
|
||||||
println!(
|
|
||||||
" Total element references: {} (avg {:.1} per unit, original: {})",
|
|
||||||
total_element_refs, avg_elements_per_unit, elements.len()
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" Element processing overhead: {:.1}x (elements processed multiple times across regions)",
|
|
||||||
total_element_refs as f64 / elements.len() as f64
|
|
||||||
);
|
|
||||||
|
|
||||||
// Configure thread pool to use requested number of threads
|
|
||||||
let num_threads = calculate_parallel_threads(parallel_config.num_threads);
|
|
||||||
|
|
||||||
// Process each unit: generate blocks, save region, free memory
|
|
||||||
let units_with_indices: Vec<_> = units
|
|
||||||
.into_iter()
|
|
||||||
.zip(unit_element_indices.into_iter())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Track timing for each unit
|
|
||||||
let unit_times = std::sync::Mutex::new(Vec::with_capacity(total_units));
|
|
||||||
let parallel_start = std::time::Instant::now();
|
|
||||||
|
|
||||||
// Track which thread processes each unit
|
|
||||||
let thread_ids = std::sync::Mutex::new(std::collections::HashSet::new());
|
|
||||||
|
|
||||||
// Use rayon's parallel iterator with configured thread count
|
|
||||||
println!(" Starting parallel processing with {} threads...", num_threads);
|
|
||||||
rayon::ThreadPoolBuilder::new()
|
|
||||||
.num_threads(num_threads)
|
|
||||||
.build()
|
|
||||||
.unwrap()
|
|
||||||
.install(|| {
|
|
||||||
units_with_indices
|
|
||||||
.par_iter()
|
|
||||||
.for_each(|(unit, element_indices)| {
|
|
||||||
// Track thread usage
|
|
||||||
let thread_id = std::thread::current().id();
|
|
||||||
thread_ids.lock().unwrap().insert(format!("{:?}", thread_id));
|
|
||||||
|
|
||||||
let unit_start = std::time::Instant::now();
|
|
||||||
|
|
||||||
// Collect elements for this unit using indices - only clone what's needed
|
|
||||||
let unit_elements: Vec<&ProcessedElement> = element_indices
|
|
||||||
.iter()
|
|
||||||
.map(|&idx| &elements[idx])
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Create bbox for this specific unit
|
|
||||||
let unit_bbox = unit.bbox();
|
|
||||||
|
|
||||||
// Process this unit and save immediately
|
|
||||||
let process_start = std::time::Instant::now();
|
|
||||||
let mut editor = process_unit_refs(unit, &unit_elements, &shared, &unit_bbox, args);
|
|
||||||
let process_time = process_start.elapsed();
|
|
||||||
|
|
||||||
// Save this region silently (no progress output)
|
|
||||||
let save_start = std::time::Instant::now();
|
|
||||||
editor.save_silent();
|
|
||||||
let save_time = save_start.elapsed();
|
|
||||||
|
|
||||||
// editor is dropped here, freeing its memory
|
|
||||||
let total_time = unit_start.elapsed();
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
let completed = stats.increment_completed();
|
|
||||||
process_pb.inc(1);
|
|
||||||
|
|
||||||
// Store timing info
|
|
||||||
unit_times.lock().unwrap().push((
|
|
||||||
unit.region_x,
|
|
||||||
unit.region_z,
|
|
||||||
element_indices.len(),
|
|
||||||
process_time,
|
|
||||||
save_time,
|
|
||||||
total_time,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Progress: 25% (terrain done) to 90% (regions done)
|
|
||||||
// This covers the full parallel processing phase
|
|
||||||
let progress = 25.0 + (completed as f64 / total_units as f64) * 65.0;
|
|
||||||
emit_gui_progress_update(progress, &format!("Processing unit {}/{}...", completed, total_units));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process_pb.finish();
|
|
||||||
let parallel_duration = parallel_start.elapsed();
|
|
||||||
|
|
||||||
// Report thread usage
|
|
||||||
let unique_threads = thread_ids.into_inner().unwrap();
|
|
||||||
println!(" Threads actually used: {} (requested: {})", unique_threads.len(), num_threads);
|
|
||||||
|
|
||||||
// Print timing summary
|
|
||||||
let times = unit_times.into_inner().unwrap();
|
|
||||||
println!("\n === Unit Processing Times ===");
|
|
||||||
|
|
||||||
let mut total_process = std::time::Duration::ZERO;
|
|
||||||
let mut total_save = std::time::Duration::ZERO;
|
|
||||||
|
|
||||||
// Sort by total time descending to show slowest first
|
|
||||||
let mut sorted_times = times.clone();
|
|
||||||
sorted_times.sort_by(|a, b| b.5.cmp(&a.5));
|
|
||||||
|
|
||||||
for (rx, rz, elem_count, process, save, total) in sorted_times.iter().take(10) {
|
|
||||||
println!(
|
|
||||||
" Region ({:3},{:3}): {} elements, process: {:>6.2}s, save: {:>5.2}s, total: {:>6.2}s",
|
|
||||||
rx, rz, elem_count,
|
|
||||||
process.as_secs_f64(),
|
|
||||||
save.as_secs_f64(),
|
|
||||||
total.as_secs_f64()
|
|
||||||
);
|
|
||||||
total_process += *process;
|
|
||||||
total_save += *save;
|
|
||||||
}
|
|
||||||
|
|
||||||
if times.len() > 10 {
|
|
||||||
for (_, _, _, process, save, _) in times.iter().skip(10) {
|
|
||||||
total_process += *process;
|
|
||||||
total_save += *save;
|
|
||||||
}
|
|
||||||
println!(" ... and {} more units", times.len() - 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sum_total: std::time::Duration = times.iter().map(|t| t.5).sum();
|
|
||||||
println!(" Sum of all unit times: {:.2}s (process: {:.2}s, save: {:.2}s)",
|
|
||||||
sum_total.as_secs_f64(), total_process.as_secs_f64(), total_save.as_secs_f64());
|
|
||||||
println!(" Actual wall time: {:.2}s", parallel_duration.as_secs_f64());
|
|
||||||
println!(" Parallelism factor: {:.2}x (sum/wall)", sum_total.as_secs_f64() / parallel_duration.as_secs_f64());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Final save for any remaining metadata or global operations
|
|
||||||
println!("{} Finalizing world...", "[7/7]".bold());
|
|
||||||
emit_gui_progress_update(90.0, "Finalizing world...");
|
|
||||||
|
|
||||||
// Save metadata file (regions already saved individually during processing)
|
|
||||||
let mut metadata_editor = WorldEditor::new_with_format_and_name(
|
|
||||||
options.path.clone(),
|
|
||||||
&xzbbox,
|
|
||||||
llbbox,
|
|
||||||
options.format,
|
|
||||||
options.level_name,
|
|
||||||
options.spawn_point,
|
|
||||||
);
|
|
||||||
metadata_editor.set_ground(Arc::clone(&ground));
|
|
||||||
// Only save metadata, not the world data (already saved per-region)
|
|
||||||
if let Err(e) = metadata_editor.save_metadata() {
|
|
||||||
eprintln!("Warning: Failed to save metadata: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_gui_progress_update(99.0, "World generation complete!");
|
|
||||||
|
|
||||||
// Handle spawn point update for GUI
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
if world_format == WorldFormat::JavaAnvil {
|
|
||||||
use crate::gui::update_player_spawn_y_after_generation;
|
|
||||||
let bbox_string = format!(
|
|
||||||
"{},{},{},{}",
|
|
||||||
args.bbox.min().lat(),
|
|
||||||
args.bbox.min().lng(),
|
|
||||||
args.bbox.max().lat(),
|
|
||||||
args.bbox.max().lng()
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(e) =
|
|
||||||
update_player_spawn_y_after_generation(&args.path, bbox_string, args.scale, &ground)
|
|
||||||
{
|
|
||||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
|
||||||
eprintln!("Warning: {}", warning_msg);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sequential world generation - original logic preserved for debugging/comparison
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn generate_world_sequential(
|
|
||||||
elements: Vec<ProcessedElement>,
|
|
||||||
xzbbox: XZBBox,
|
|
||||||
llbbox: LLBBox,
|
|
||||||
ground: Arc<Ground>,
|
|
||||||
highway_connectivity: Arc<highways::HighwayConnectivityMap>,
|
|
||||||
flood_fill_cache: Arc<FloodFillCache>,
|
|
||||||
building_footprints: Arc<crate::floodfill_cache::BuildingFootprintBitmap>,
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set ground reference in the editor to enable elevation-aware block placement
|
|
||||||
editor.set_ground(Arc::clone(&ground));
|
|
||||||
|
|
||||||
// Process data
|
// Process data
|
||||||
let elements_count: usize = elements.len();
|
let elements_count: usize = elements.len();
|
||||||
let mut elements = elements; // Take ownership for consuming
|
|
||||||
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
||||||
process_pb.set_style(ProgressStyle::default_bar()
|
process_pb.set_style(ProgressStyle::default_bar()
|
||||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
||||||
@@ -459,8 +47,7 @@ fn generate_world_sequential(
|
|||||||
let mut current_progress_prcs: f64 = 25.0;
|
let mut current_progress_prcs: f64 = 25.0;
|
||||||
let mut last_emitted_progress: f64 = current_progress_prcs;
|
let mut last_emitted_progress: f64 = current_progress_prcs;
|
||||||
|
|
||||||
// Process elements by draining in insertion order
|
for element in &elements {
|
||||||
for element in elements.drain(..) {
|
|
||||||
process_pb.inc(1);
|
process_pb.inc(1);
|
||||||
current_progress_prcs += progress_increment_prcs;
|
current_progress_prcs += progress_increment_prcs;
|
||||||
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
||||||
@@ -478,46 +65,22 @@ fn generate_world_sequential(
|
|||||||
process_pb.set_message("");
|
process_pb.set_message("");
|
||||||
}
|
}
|
||||||
|
|
||||||
match &element {
|
match element {
|
||||||
ProcessedElement::Way(way) => {
|
ProcessedElement::Way(way) => {
|
||||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
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") {
|
} else if way.tags.contains_key("highway") {
|
||||||
highways::generate_highways(
|
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||||
&mut editor,
|
|
||||||
&element,
|
|
||||||
args,
|
|
||||||
&highway_connectivity,
|
|
||||||
&flood_fill_cache,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("landuse") {
|
} else if way.tags.contains_key("landuse") {
|
||||||
landuse::generate_landuse(
|
landuse::generate_landuse(&mut editor, way, args);
|
||||||
&mut editor,
|
|
||||||
way,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
&building_footprints,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("natural") {
|
} else if way.tags.contains_key("natural") {
|
||||||
natural::generate_natural(
|
natural::generate_natural(&mut editor, element, args);
|
||||||
&mut editor,
|
|
||||||
&element,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
&building_footprints,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("amenity") {
|
} 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") {
|
} else if way.tags.contains_key("leisure") {
|
||||||
leisure::generate_leisure(
|
leisure::generate_leisure(&mut editor, way, args);
|
||||||
&mut editor,
|
|
||||||
way,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
&building_footprints,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("barrier") {
|
} 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") {
|
} else if let Some(val) = way.tags.get("waterway") {
|
||||||
if val == "dock" {
|
if val == "dock" {
|
||||||
// docks count as water areas
|
// docks count as water areas
|
||||||
@@ -537,9 +100,8 @@ fn generate_world_sequential(
|
|||||||
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
||||||
highways::generate_siding(&mut editor, way);
|
highways::generate_siding(&mut editor, way);
|
||||||
} else if way.tags.contains_key("man_made") {
|
} 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);
|
||||||
}
|
}
|
||||||
// Note: flood fill cache entries are managed by Arc, not removed per-element in Arc version
|
|
||||||
}
|
}
|
||||||
ProcessedElement::Node(node) => {
|
ProcessedElement::Node(node) => {
|
||||||
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
|
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
|
||||||
@@ -547,25 +109,13 @@ fn generate_world_sequential(
|
|||||||
} else if node.tags.contains_key("natural")
|
} else if node.tags.contains_key("natural")
|
||||||
&& node.tags.get("natural") == Some(&"tree".to_string())
|
&& node.tags.get("natural") == Some(&"tree".to_string())
|
||||||
{
|
{
|
||||||
natural::generate_natural(
|
natural::generate_natural(&mut editor, element, args);
|
||||||
&mut editor,
|
|
||||||
&element,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
&building_footprints,
|
|
||||||
);
|
|
||||||
} else if node.tags.contains_key("amenity") {
|
} 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") {
|
} else if node.tags.contains_key("barrier") {
|
||||||
barriers::generate_barrier_nodes(&mut editor, node);
|
barriers::generate_barrier_nodes(&mut editor, node);
|
||||||
} else if node.tags.contains_key("highway") {
|
} else if node.tags.contains_key("highway") {
|
||||||
highways::generate_highways(
|
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||||
&mut editor,
|
|
||||||
&element,
|
|
||||||
args,
|
|
||||||
&highway_connectivity,
|
|
||||||
&flood_fill_cache,
|
|
||||||
);
|
|
||||||
} else if node.tags.contains_key("tourism") {
|
} else if node.tags.contains_key("tourism") {
|
||||||
tourisms::generate_tourisms(&mut editor, node);
|
tourisms::generate_tourisms(&mut editor, node);
|
||||||
} else if node.tags.contains_key("man_made") {
|
} else if node.tags.contains_key("man_made") {
|
||||||
@@ -574,12 +124,7 @@ fn generate_world_sequential(
|
|||||||
}
|
}
|
||||||
ProcessedElement::Relation(rel) => {
|
ProcessedElement::Relation(rel) => {
|
||||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||||
buildings::generate_building_from_relation(
|
buildings::generate_building_from_relation(&mut editor, rel, args);
|
||||||
&mut editor,
|
|
||||||
rel,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
);
|
|
||||||
} else if rel.tags.contains_key("water")
|
} else if rel.tags.contains_key("water")
|
||||||
|| rel
|
|| rel
|
||||||
.tags
|
.tags
|
||||||
@@ -589,44 +134,24 @@ fn generate_world_sequential(
|
|||||||
{
|
{
|
||||||
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
|
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
|
||||||
} else if rel.tags.contains_key("natural") {
|
} else if rel.tags.contains_key("natural") {
|
||||||
natural::generate_natural_from_relation(
|
natural::generate_natural_from_relation(&mut editor, rel, args);
|
||||||
&mut editor,
|
|
||||||
rel,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
&building_footprints,
|
|
||||||
);
|
|
||||||
} else if rel.tags.contains_key("landuse") {
|
} else if rel.tags.contains_key("landuse") {
|
||||||
landuse::generate_landuse_from_relation(
|
landuse::generate_landuse_from_relation(&mut editor, rel, args);
|
||||||
&mut editor,
|
|
||||||
rel,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
&building_footprints,
|
|
||||||
);
|
|
||||||
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||||
leisure::generate_leisure_from_relation(
|
leisure::generate_leisure_from_relation(&mut editor, rel, args);
|
||||||
&mut editor,
|
|
||||||
rel,
|
|
||||||
args,
|
|
||||||
&flood_fill_cache,
|
|
||||||
&building_footprints,
|
|
||||||
);
|
|
||||||
} else if rel.tags.contains_key("man_made") {
|
} 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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Note: flood fill cache entries are managed by Arc, dropped when no longer referenced
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Element is dropped here, freeing its memory immediately
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process_pb.finish();
|
process_pb.finish();
|
||||||
|
|
||||||
// Drop remaining caches
|
|
||||||
drop(highway_connectivity);
|
|
||||||
drop(flood_fill_cache);
|
|
||||||
|
|
||||||
// Generate ground layer
|
// Generate ground layer
|
||||||
let total_blocks: u64 = xzbbox.bounding_rect().total_blocks();
|
let total_blocks: u64 = xzbbox.bounding_rect().total_blocks();
|
||||||
let desired_updates: u64 = 1500;
|
let desired_updates: u64 = 1500;
|
||||||
@@ -650,72 +175,46 @@ fn generate_world_sequential(
|
|||||||
let total_iterations_grnd: f64 = total_blocks as f64;
|
let total_iterations_grnd: f64 = total_blocks as f64;
|
||||||
let progress_increment_grnd: f64 = 20.0 / total_iterations_grnd;
|
let progress_increment_grnd: f64 = 20.0 / total_iterations_grnd;
|
||||||
|
|
||||||
// Check if terrain elevation is enabled; when disabled, we can skip ground level lookups entirely
|
let groundlayer_block = GRASS_BLOCK;
|
||||||
let terrain_enabled = ground.elevation_enabled;
|
|
||||||
|
|
||||||
// Process ground generation chunk-by-chunk for better cache locality.
|
for x in xzbbox.min_x()..=xzbbox.max_x() {
|
||||||
// This keeps the same region/chunk HashMap entries hot in CPU cache,
|
for z in xzbbox.min_z()..=xzbbox.max_z() {
|
||||||
// rather than jumping between regions on every Z iteration.
|
// Add default dirt and grass layer if there isn't a stone layer already
|
||||||
let min_chunk_x = xzbbox.min_x() >> 4;
|
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
|
||||||
let max_chunk_x = xzbbox.max_x() >> 4;
|
editor.set_block(groundlayer_block, x, 0, z, None, None);
|
||||||
let min_chunk_z = xzbbox.min_z() >> 4;
|
editor.set_block(DIRT, x, -1, z, None, None);
|
||||||
let max_chunk_z = xzbbox.max_z() >> 4;
|
editor.set_block(DIRT, x, -2, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
for chunk_x in min_chunk_x..=max_chunk_x {
|
// Fill underground with stone
|
||||||
for chunk_z in min_chunk_z..=max_chunk_z {
|
if args.fillground {
|
||||||
// Calculate the block range for this chunk, clamped to bbox
|
// Fill from bedrock+1 to 3 blocks below ground with stone
|
||||||
let chunk_min_x = (chunk_x << 4).max(xzbbox.min_x());
|
editor.fill_blocks_absolute(
|
||||||
let chunk_max_x = ((chunk_x << 4) + 15).min(xzbbox.max_x());
|
STONE,
|
||||||
let chunk_min_z = (chunk_z << 4).max(xzbbox.min_z());
|
x,
|
||||||
let chunk_max_z = ((chunk_z << 4) + 15).min(xzbbox.max_z());
|
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 {
|
block_counter += 1;
|
||||||
for z in chunk_min_z..=chunk_max_z {
|
// Use manual % check since is_multiple_of() is unstable on stable Rust
|
||||||
// Get ground level, when terrain is enabled, look it up once per block
|
#[allow(clippy::manual_is_multiple_of)]
|
||||||
// When disabled, use constant ground_level (no function call overhead)
|
if block_counter % batch_size == 0 {
|
||||||
let ground_y = if terrain_enabled {
|
ground_pb.inc(batch_size);
|
||||||
editor.get_ground_level(x, z)
|
}
|
||||||
} else {
|
|
||||||
args.ground_level
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add default dirt and grass layer if there isn't a stone layer already
|
gui_progress_grnd += progress_increment_grnd;
|
||||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
|
||||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
emit_gui_progress_update(gui_progress_grnd, "");
|
||||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
last_emitted_progress = gui_progress_grnd;
|
||||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
ground_y - 3,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -738,28 +237,24 @@ fn generate_world_sequential(
|
|||||||
// Save world
|
// Save world
|
||||||
editor.save();
|
editor.save();
|
||||||
|
|
||||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
|
||||||
|
|
||||||
// Update player spawn Y coordinate based on terrain height after generation
|
// Update player spawn Y coordinate based on terrain height after generation
|
||||||
#[cfg(feature = "gui")]
|
#[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;
|
use crate::gui::update_player_spawn_y_after_generation;
|
||||||
// Reconstruct bbox string to match the format that GUI originally provided.
|
|
||||||
// This ensures LLBBox::from_str() can parse it correctly.
|
|
||||||
let bbox_string = format!(
|
let bbox_string = format!(
|
||||||
"{},{},{},{}",
|
"{},{},{},{}",
|
||||||
args.bbox.min().lat(),
|
|
||||||
args.bbox.min().lng(),
|
args.bbox.min().lng(),
|
||||||
args.bbox.max().lat(),
|
args.bbox.min().lat(),
|
||||||
args.bbox.max().lng()
|
args.bbox.max().lng(),
|
||||||
|
args.bbox.max().lat()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Always update spawn Y since we now always set a spawn point (user-selected or default)
|
|
||||||
if let Err(e) = update_player_spawn_y_after_generation(
|
if let Err(e) = update_player_spawn_y_after_generation(
|
||||||
&args.path,
|
&args.path,
|
||||||
|
Some(*spawn_coords),
|
||||||
bbox_string,
|
bbox_string,
|
||||||
args.scale,
|
args.scale,
|
||||||
ground.as_ref(),
|
&ground,
|
||||||
) {
|
) {
|
||||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||||
eprintln!("Warning: {}", warning_msg);
|
eprintln!("Warning: {}", warning_msg);
|
||||||
@@ -768,79 +263,57 @@ fn generate_world_sequential(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Bedrock format, emit event to open the mcworld file
|
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||||
if world_format == WorldFormat::BedrockMcWorld {
|
println!("{}", "Done! World generation completed.".green().bold());
|
||||||
if let Some(path_str) = output_path.to_str() {
|
|
||||||
emit_open_mcworld_file(path_str);
|
// 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)
|
Ok(())
|
||||||
}
|
|
||||||
|
|
||||||
/// 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,19 +2,11 @@ use crate::args::Args;
|
|||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::bresenham::bresenham_line;
|
use crate::bresenham::bresenham_line;
|
||||||
use crate::coordinate_system::cartesian::XZPoint;
|
use crate::coordinate_system::cartesian::XZPoint;
|
||||||
use crate::deterministic_rng::element_rng;
|
use crate::floodfill::flood_fill_area;
|
||||||
use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
|
||||||
use crate::floodfill_cache::FloodFillCache;
|
|
||||||
use crate::osm_parser::ProcessedElement;
|
use crate::osm_parser::ProcessedElement;
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
pub fn generate_amenities(
|
pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
||||||
editor: &mut WorldEditor,
|
|
||||||
element: &ProcessedElement,
|
|
||||||
args: &Args,
|
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
) {
|
|
||||||
// Skip if 'layer' or 'level' is negative in the tags
|
// Skip if 'layer' or 'level' is negative in the tags
|
||||||
if let Some(layer) = element.tags().get("layer") {
|
if let Some(layer) = element.tags().get("layer") {
|
||||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
@@ -50,14 +42,18 @@ pub fn generate_amenities(
|
|||||||
let ground_block: Block = OAK_PLANKS;
|
let ground_block: Block = OAK_PLANKS;
|
||||||
let roof_block: Block = STONE_BLOCK_SLAB;
|
let roof_block: Block = STONE_BLOCK_SLAB;
|
||||||
|
|
||||||
// Use pre-computed flood fill from cache
|
let polygon_coords: Vec<(i32, i32)> = element
|
||||||
let floor_area: Vec<(i32, i32)> =
|
.nodes()
|
||||||
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
|
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||||
|
.collect();
|
||||||
|
|
||||||
if floor_area.is_empty() {
|
if polygon_coords.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let floor_area: Vec<(i32, i32)> =
|
||||||
|
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||||
|
|
||||||
// Fill the floor area
|
// Fill the floor area
|
||||||
for (x, z) in floor_area.iter() {
|
for (x, z) in floor_area.iter() {
|
||||||
editor.set_block(ground_block, *x, 0, *z, None, None);
|
editor.set_block(ground_block, *x, 0, *z, None, None);
|
||||||
@@ -84,10 +80,8 @@ pub fn generate_amenities(
|
|||||||
"bench" => {
|
"bench" => {
|
||||||
// Place a bench
|
// Place a bench
|
||||||
if let Some(pt) = first_node {
|
if let Some(pt) = first_node {
|
||||||
// Use deterministic RNG for consistent bench orientation across region boundaries
|
// 50% chance to 90 degrees rotate the bench using if
|
||||||
let mut rng = element_rng(element.id());
|
if rand::random::<bool>() {
|
||||||
// 50% chance to 90 degrees rotate the bench
|
|
||||||
if rng.gen_bool(0.5) {
|
|
||||||
editor.set_block(SMOOTH_STONE, pt.x, 1, pt.z, None, None);
|
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);
|
||||||
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" => {
|
"shelter" => {
|
||||||
let roof_block: Block = STONE_BRICK_SLAB;
|
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)> =
|
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
|
// Place fences and roof slabs at each corner node directly
|
||||||
for node in element.nodes() {
|
for node in element.nodes() {
|
||||||
|
|||||||
@@ -3,97 +3,37 @@ use crate::bresenham::bresenham_line;
|
|||||||
use crate::osm_parser::ProcessedWay;
|
use crate::osm_parser::ProcessedWay;
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
|
|
||||||
// TODO FIX - This handles ways with bridge=yes tag (e.g., highway bridges)
|
// TODO FIX
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay) {
|
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||||
if let Some(_bridge_type) = element.tags.get("bridge") {
|
if let Some(_bridge_type) = element.tags.get("bridge") {
|
||||||
let bridge_height = 3; // Height above the ground level
|
let bridge_height = 3; // Fixed height
|
||||||
|
|
||||||
// Get start and end node elevations and use MAX for level bridge deck
|
|
||||||
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
|
|
||||||
let bridge_deck_ground_y = if element.nodes.len() >= 2 {
|
|
||||||
let start_node = &element.nodes[0];
|
|
||||||
let end_node = &element.nodes[element.nodes.len() - 1];
|
|
||||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
|
||||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
|
||||||
start_y.max(end_y)
|
|
||||||
} else {
|
|
||||||
return; // Need at least 2 nodes for a bridge
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate total bridge length for ramp positioning
|
|
||||||
let total_length: f64 = element
|
|
||||||
.nodes
|
|
||||||
.windows(2)
|
|
||||||
.map(|pair| {
|
|
||||||
let dx = (pair[1].x - pair[0].x) as f64;
|
|
||||||
let dz = (pair[1].z - pair[0].z) as f64;
|
|
||||||
(dx * dx + dz * dz).sqrt()
|
|
||||||
})
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
if total_length == 0.0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut accumulated_length: f64 = 0.0;
|
|
||||||
|
|
||||||
for i in 1..element.nodes.len() {
|
for i in 1..element.nodes.len() {
|
||||||
let prev = &element.nodes[i - 1];
|
let prev = &element.nodes[i - 1];
|
||||||
let cur = &element.nodes[i];
|
let cur = &element.nodes[i];
|
||||||
|
|
||||||
let segment_dx = (cur.x - prev.x) as f64;
|
|
||||||
let segment_dz = (cur.z - prev.z) as f64;
|
|
||||||
let segment_length = (segment_dx * segment_dx + segment_dz * segment_dz).sqrt();
|
|
||||||
|
|
||||||
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
|
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
|
||||||
|
|
||||||
let ramp_length = (total_length * 0.15).clamp(6.0, 20.0) as usize; // 15% of bridge, min 6, max 20 blocks
|
let total_length = points.len();
|
||||||
|
let ramp_length = 6; // Length of ramp at each end
|
||||||
|
|
||||||
for (idx, (x, _, z)) in points.iter().enumerate() {
|
for (idx, (x, _, z)) in points.iter().enumerate() {
|
||||||
// Calculate progress along this segment
|
let height = if idx < ramp_length {
|
||||||
let segment_progress = if points.len() > 1 {
|
|
||||||
idx as f64 / (points.len() - 1) as f64
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate overall progress along the entire bridge
|
|
||||||
let point_distance = accumulated_length + segment_progress * segment_length;
|
|
||||||
let overall_progress = (point_distance / total_length).clamp(0.0, 1.0);
|
|
||||||
let total_len_usize = total_length as usize;
|
|
||||||
let overall_idx = (overall_progress * total_len_usize as f64) as usize;
|
|
||||||
|
|
||||||
// Calculate ramp height offset
|
|
||||||
let ramp_offset = if overall_idx < ramp_length {
|
|
||||||
// Start ramp (rising)
|
// Start ramp (rising)
|
||||||
(overall_idx as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
(idx * bridge_height) / ramp_length
|
||||||
} else if overall_idx >= total_len_usize.saturating_sub(ramp_length) {
|
} else if idx >= total_length - ramp_length {
|
||||||
// End ramp (descending)
|
// End ramp (descending)
|
||||||
let dist_from_end = total_len_usize - overall_idx;
|
((total_length - idx) * bridge_height) / ramp_length
|
||||||
(dist_from_end as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
|
||||||
} else {
|
} else {
|
||||||
// Middle section (constant height)
|
// Middle section (constant height)
|
||||||
bridge_height
|
bridge_height
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use fixed bridge deck height (max of endpoints) plus ramp offset
|
|
||||||
let bridge_y = bridge_deck_ground_y + ramp_offset;
|
|
||||||
|
|
||||||
// Place bridge blocks
|
// Place bridge blocks
|
||||||
for dx in -2..=2 {
|
for dx in -2..=2 {
|
||||||
editor.set_block_absolute(
|
editor.set_block(LIGHT_GRAY_CONCRETE, *x + dx, height as i32, *z, None, None);
|
||||||
LIGHT_GRAY_CONCRETE,
|
|
||||||
*x + dx,
|
|
||||||
bridge_y,
|
|
||||||
*z,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accumulated_length += segment_length;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ use crate::block_definitions::*;
|
|||||||
use crate::bresenham::bresenham_line;
|
use crate::bresenham::bresenham_line;
|
||||||
use crate::colors::color_text_to_rgb_tuple;
|
use crate::colors::color_text_to_rgb_tuple;
|
||||||
use crate::coordinate_system::cartesian::XZPoint;
|
use crate::coordinate_system::cartesian::XZPoint;
|
||||||
use crate::deterministic_rng::element_rng;
|
|
||||||
use crate::element_processing::subprocessor::buildings_interior::generate_building_interior;
|
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::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
@@ -29,7 +28,6 @@ pub fn generate_buildings(
|
|||||||
element: &ProcessedWay,
|
element: &ProcessedWay,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
relation_levels: Option<i32>,
|
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
|
// 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") {
|
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 scale_factor = args.scale;
|
||||||
let min_level_offset = multiply_scale(min_level * 4, scale_factor);
|
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)> =
|
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();
|
let cached_footprint_size = cached_floor_area.len();
|
||||||
|
|
||||||
// Use fixed starting Y coordinate based on maximum ground level when terrain is enabled
|
// 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 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 building_height: i32 = ((6.0 * scale_factor) as i32).max(3); // Default building height with scale and minimum
|
||||||
let mut is_tall_building = false;
|
let mut is_tall_building = false;
|
||||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
let mut rng = rand::thread_rng();
|
||||||
let mut rng = element_rng(element.id);
|
|
||||||
let use_vertical_windows = rng.gen_bool(0.7);
|
let use_vertical_windows = rng.gen_bool(0.7);
|
||||||
let use_accent_roof_line = rng.gen_bool(0.25);
|
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);
|
building_height = ((23.0 * scale_factor) as i32).max(3);
|
||||||
}
|
}
|
||||||
} else if building_type == "bridge" {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1486,7 +1484,6 @@ pub fn generate_building_from_relation(
|
|||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
relation: &ProcessedRelation,
|
relation: &ProcessedRelation,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
) {
|
) {
|
||||||
// Extract levels from relation tags
|
// Extract levels from relation tags
|
||||||
let relation_levels = relation
|
let relation_levels = relation
|
||||||
@@ -1498,13 +1495,7 @@ pub fn generate_building_from_relation(
|
|||||||
// Process the outer way to create the building walls
|
// Process the outer way to create the building walls
|
||||||
for member in &relation.members {
|
for member in &relation.members {
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
generate_buildings(
|
generate_buildings(editor, &member.way, args, Some(relation_levels));
|
||||||
editor,
|
|
||||||
&member.way,
|
|
||||||
args,
|
|
||||||
Some(relation_levels),
|
|
||||||
flood_fill_cache,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1525,18 +1516,52 @@ pub fn generate_building_from_relation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a bridge structure, paying attention to the "level" tag.
|
/// Generates a bridge structure, paying attention to the "level" tag.
|
||||||
/// Bridge deck is interpolated between start and end point elevations to avoid
|
|
||||||
/// being dragged down by valleys underneath.
|
|
||||||
fn generate_bridge(
|
fn generate_bridge(
|
||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
element: &ProcessedWay,
|
element: &ProcessedWay,
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
floodfill_timeout: Option<&Duration>,
|
floodfill_timeout: Option<&Duration>,
|
||||||
) {
|
) {
|
||||||
let floor_block: Block = STONE;
|
let floor_block: Block = STONE;
|
||||||
let railing_block: Block = STONE_BRICKS;
|
let railing_block: Block = STONE_BRICKS;
|
||||||
|
|
||||||
// Calculate bridge level offset based on the "level" tag
|
// 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)> =
|
||||||
|
bresenham_line(prev.0, bridge_y_offset, prev.1, x, bridge_y_offset, z);
|
||||||
|
|
||||||
|
for (bx, by, bz) in bridge_points {
|
||||||
|
// Place railing blocks
|
||||||
|
editor.set_block(railing_block, bx, by + 1, bz, None, None);
|
||||||
|
editor.set_block(railing_block, bx, by, bz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_node = Some((x, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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") {
|
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
|
||||||
if let Ok(level) = level_str.parse::<i32>() {
|
if let Ok(level) = level_str.parse::<i32>() {
|
||||||
(level * 3) + 1
|
(level * 3) + 1
|
||||||
@@ -1547,51 +1572,8 @@ fn generate_bridge(
|
|||||||
1 // Default elevation
|
1 // Default elevation
|
||||||
};
|
};
|
||||||
|
|
||||||
// Need at least 2 nodes to form a bridge
|
|
||||||
if element.nodes.len() < 2 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get start and end node elevations and use MAX for level bridge deck
|
|
||||||
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
|
|
||||||
let start_node = &element.nodes[0];
|
|
||||||
let end_node = &element.nodes[element.nodes.len() - 1];
|
|
||||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
|
||||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
|
||||||
let bridge_deck_ground_y = start_y.max(end_y);
|
|
||||||
|
|
||||||
// Process the nodes to create bridge pathways and railings
|
|
||||||
let mut previous_node: Option<(i32, i32)> = None;
|
|
||||||
|
|
||||||
for node in &element.nodes {
|
|
||||||
let x: i32 = node.x;
|
|
||||||
let z: i32 = node.z;
|
|
||||||
|
|
||||||
// Create bridge path using Bresenham's line
|
|
||||||
if let Some(prev) = previous_node {
|
|
||||||
let bridge_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, 0, prev.1, x, 0, z);
|
|
||||||
|
|
||||||
for (bx, _, bz) in bridge_points.iter() {
|
|
||||||
// Use fixed bridge deck height (max of endpoints)
|
|
||||||
let bridge_y = bridge_deck_ground_y + bridge_y_offset;
|
|
||||||
|
|
||||||
// Place railing blocks
|
|
||||||
editor.set_block_absolute(railing_block, *bx, bridge_y + 1, *bz, None, None);
|
|
||||||
editor.set_block_absolute(railing_block, *bx, bridge_y, *bz, None, None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Use the same level bridge deck height for filled areas
|
|
||||||
let floor_y = bridge_deck_ground_y + bridge_y_offset;
|
|
||||||
|
|
||||||
// Place floor blocks
|
// Place floor blocks
|
||||||
for (x, z) in bridge_area {
|
for (x, z) in bridge_area {
|
||||||
editor.set_block_absolute(floor_block, x, floor_y, z, None, None);
|
editor.set_block(floor_block, x, bridge_y_offset, z, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::args::Args;
|
|||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::bresenham::bresenham_line;
|
use crate::bresenham::bresenham_line;
|
||||||
use crate::coordinate_system::cartesian::XZPoint;
|
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::osm_parser::{ProcessedElement, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -10,24 +10,14 @@ use std::collections::HashMap;
|
|||||||
/// Type alias for highway connectivity map
|
/// Type alias for highway connectivity map
|
||||||
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||||
|
|
||||||
/// Minimum terrain dip (in blocks) below max endpoint elevation to classify a bridge as valley-spanning
|
|
||||||
const VALLEY_BRIDGE_THRESHOLD: i32 = 7;
|
|
||||||
|
|
||||||
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
||||||
pub fn generate_highways(
|
pub fn generate_highways(
|
||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
element: &ProcessedElement,
|
element: &ProcessedElement,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
highway_connectivity: &HighwayConnectivityMap,
|
highway_connectivity: &HighwayConnectivityMap,
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
) {
|
) {
|
||||||
generate_highways_internal(
|
generate_highways_internal(editor, element, args, highway_connectivity);
|
||||||
editor,
|
|
||||||
element,
|
|
||||||
args,
|
|
||||||
highway_connectivity,
|
|
||||||
flood_fill_cache,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||||
@@ -76,7 +66,6 @@ fn generate_highways_internal(
|
|||||||
element: &ProcessedElement,
|
element: &ProcessedElement,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>, // Maps node coordinates to list of layers that connect to this node
|
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 let Some(highway_type) = element.tags().get("highway") {
|
||||||
if highway_type == "street_lamp" {
|
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)> =
|
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 {
|
for (x, z) in filled_area {
|
||||||
editor.set_block(surface_block, x, 0, z, None, None);
|
editor.set_block(surface_block, x, 0, z, None, None);
|
||||||
@@ -163,11 +157,6 @@ fn generate_highways_internal(
|
|||||||
let mut add_outline = false;
|
let mut add_outline = false;
|
||||||
let scale_factor = args.scale;
|
let scale_factor = args.scale;
|
||||||
|
|
||||||
// Check if this is a bridge - bridges need special elevation handling
|
|
||||||
// to span across valleys instead of following terrain
|
|
||||||
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
|
|
||||||
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
|
|
||||||
|
|
||||||
// Parse the layer value for elevation calculation
|
// Parse the layer value for elevation calculation
|
||||||
let layer_value = element
|
let layer_value = element
|
||||||
.tags()
|
.tags()
|
||||||
@@ -257,7 +246,6 @@ fn generate_highways_internal(
|
|||||||
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
||||||
|
|
||||||
// Check if we need slopes at start and end
|
// Check if we need slopes at start and end
|
||||||
// This is used for overpasses that need ramps to ground-level roads
|
|
||||||
let needs_start_slope =
|
let needs_start_slope =
|
||||||
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
||||||
let needs_end_slope = should_add_slope_at_node(
|
let needs_end_slope = should_add_slope_at_node(
|
||||||
@@ -266,67 +254,10 @@ fn generate_highways_internal(
|
|||||||
highway_connectivity,
|
highway_connectivity,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate total way length for slope distribution (needed before valley bridge check)
|
// Calculate total way length for slope distribution
|
||||||
let total_way_length = calculate_way_length(way);
|
let total_way_length = calculate_way_length(way);
|
||||||
|
|
||||||
// For bridges: detect if this spans a valley by checking terrain profile
|
// Check if this is a short isolated elevated segment - if so, treat as ground level
|
||||||
// A valley bridge has terrain that dips significantly below the endpoints
|
|
||||||
// Skip valley detection entirely if terrain is disabled (no valleys in flat terrain)
|
|
||||||
// Skip very short bridges (< 25 blocks) as they're unlikely to span significant valleys
|
|
||||||
let terrain_enabled = editor
|
|
||||||
.get_ground()
|
|
||||||
.map(|g| g.elevation_enabled)
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
let (is_valley_bridge, bridge_deck_y) =
|
|
||||||
if is_bridge && terrain_enabled && way.nodes.len() >= 2 && total_way_length >= 25 {
|
|
||||||
let start_node = &way.nodes[0];
|
|
||||||
let end_node = &way.nodes[way.nodes.len() - 1];
|
|
||||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
|
||||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
|
||||||
let max_endpoint_y = start_y.max(end_y);
|
|
||||||
|
|
||||||
// Sample terrain at middle nodes only (excluding endpoints we already have)
|
|
||||||
// This avoids redundant get_ground_level() calls
|
|
||||||
let middle_nodes = &way.nodes[1..way.nodes.len().saturating_sub(1)];
|
|
||||||
let sampled_min = if middle_nodes.is_empty() {
|
|
||||||
// No middle nodes, just use endpoints
|
|
||||||
start_y.min(end_y)
|
|
||||||
} else {
|
|
||||||
// Sample up to 3 middle points (5 total with endpoints) for performance
|
|
||||||
// Valleys are wide terrain features, so sparse sampling is sufficient
|
|
||||||
let sample_count = middle_nodes.len().min(3);
|
|
||||||
let step = if sample_count > 1 {
|
|
||||||
(middle_nodes.len() - 1) / (sample_count - 1)
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
};
|
|
||||||
|
|
||||||
middle_nodes
|
|
||||||
.iter()
|
|
||||||
.step_by(step.max(1))
|
|
||||||
.map(|node| editor.get_ground_level(node.x, node.z))
|
|
||||||
.min()
|
|
||||||
.unwrap_or(max_endpoint_y)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Include endpoint elevations in the minimum calculation
|
|
||||||
let min_terrain_y = sampled_min.min(start_y).min(end_y);
|
|
||||||
|
|
||||||
// If ANY sampled point along the bridge is significantly lower than the max endpoint,
|
|
||||||
// treat as valley bridge
|
|
||||||
let is_valley = min_terrain_y < max_endpoint_y - VALLEY_BRIDGE_THRESHOLD;
|
|
||||||
|
|
||||||
if is_valley {
|
|
||||||
(true, max_endpoint_y)
|
|
||||||
} else {
|
|
||||||
(false, 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(false, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if this is a short isolated elevated segment (layer > 0), if so, treat as ground level
|
|
||||||
let is_short_isolated_elevated =
|
let is_short_isolated_elevated =
|
||||||
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
||||||
|
|
||||||
@@ -363,28 +294,17 @@ fn generate_highways_internal(
|
|||||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||||
|
|
||||||
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
||||||
// Calculate Y elevation for this point
|
// Calculate Y elevation for this point based on slopes and layer
|
||||||
// For valley bridges: use fixed deck height (max of endpoints) to stay level
|
let current_y = calculate_point_elevation(
|
||||||
// For overpasses and regular roads: use terrain-relative elevation with slopes
|
segment_index,
|
||||||
let (current_y, use_absolute_y) = if is_valley_bridge {
|
point_index,
|
||||||
// Valley bridge deck is level at the maximum endpoint elevation
|
segment_length,
|
||||||
// Don't add base_elevation - the layer tag indicates it's above water/road,
|
total_segments,
|
||||||
// not that it should be higher than the terrain endpoints
|
effective_elevation,
|
||||||
(bridge_deck_y, true)
|
effective_start_slope,
|
||||||
} else {
|
effective_end_slope,
|
||||||
// Regular road or overpass: use terrain-relative calculation with ramps
|
slope_length,
|
||||||
let y = calculate_point_elevation(
|
);
|
||||||
segment_index,
|
|
||||||
point_index,
|
|
||||||
segment_length,
|
|
||||||
total_segments,
|
|
||||||
effective_elevation,
|
|
||||||
effective_start_slope,
|
|
||||||
effective_end_slope,
|
|
||||||
slope_length,
|
|
||||||
);
|
|
||||||
(y, false)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Draw the road surface for the entire width
|
// Draw the road surface for the entire width
|
||||||
for dx in -block_range..=block_range {
|
for dx in -block_range..=block_range {
|
||||||
@@ -400,32 +320,12 @@ fn generate_highways_internal(
|
|||||||
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
|
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
|
||||||
if is_horizontal {
|
if is_horizontal {
|
||||||
if set_x % 2 < 1 {
|
if set_x % 2 < 1 {
|
||||||
if use_absolute_y {
|
editor.set_block(
|
||||||
editor.set_block_absolute(
|
WHITE_CONCRETE,
|
||||||
WHITE_CONCRETE,
|
|
||||||
set_x,
|
|
||||||
current_y,
|
|
||||||
set_z,
|
|
||||||
Some(&[BLACK_CONCRETE]),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editor.set_block(
|
|
||||||
WHITE_CONCRETE,
|
|
||||||
set_x,
|
|
||||||
current_y,
|
|
||||||
set_z,
|
|
||||||
Some(&[BLACK_CONCRETE]),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if use_absolute_y {
|
|
||||||
editor.set_block_absolute(
|
|
||||||
BLACK_CONCRETE,
|
|
||||||
set_x,
|
set_x,
|
||||||
current_y,
|
current_y,
|
||||||
set_z,
|
set_z,
|
||||||
None,
|
Some(&[BLACK_CONCRETE]),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -439,32 +339,12 @@ fn generate_highways_internal(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if set_z % 2 < 1 {
|
} else if set_z % 2 < 1 {
|
||||||
if use_absolute_y {
|
editor.set_block(
|
||||||
editor.set_block_absolute(
|
WHITE_CONCRETE,
|
||||||
WHITE_CONCRETE,
|
|
||||||
set_x,
|
|
||||||
current_y,
|
|
||||||
set_z,
|
|
||||||
Some(&[BLACK_CONCRETE]),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editor.set_block(
|
|
||||||
WHITE_CONCRETE,
|
|
||||||
set_x,
|
|
||||||
current_y,
|
|
||||||
set_z,
|
|
||||||
Some(&[BLACK_CONCRETE]),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if use_absolute_y {
|
|
||||||
editor.set_block_absolute(
|
|
||||||
BLACK_CONCRETE,
|
|
||||||
set_x,
|
set_x,
|
||||||
current_y,
|
current_y,
|
||||||
set_z,
|
set_z,
|
||||||
None,
|
Some(&[BLACK_CONCRETE]),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -477,15 +357,6 @@ fn generate_highways_internal(
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if use_absolute_y {
|
|
||||||
editor.set_block_absolute(
|
|
||||||
block_type,
|
|
||||||
set_x,
|
|
||||||
current_y,
|
|
||||||
set_z,
|
|
||||||
None,
|
|
||||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
editor.set_block(
|
editor.set_block(
|
||||||
block_type,
|
block_type,
|
||||||
@@ -497,53 +368,30 @@ fn generate_highways_internal(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add stone brick foundation underneath elevated highways/bridges for thickness
|
// Add stone brick foundation underneath elevated highways for thickness
|
||||||
if (effective_elevation > 0 || use_absolute_y) && current_y > 0 {
|
if effective_elevation > 0 && current_y > 0 {
|
||||||
// Add 1 layer of stone bricks underneath the highway surface
|
// Add 1 layer of stone bricks underneath the highway surface
|
||||||
if use_absolute_y {
|
editor.set_block(
|
||||||
editor.set_block_absolute(
|
STONE_BRICKS,
|
||||||
STONE_BRICKS,
|
set_x,
|
||||||
set_x,
|
current_y - 1,
|
||||||
current_y - 1,
|
set_z,
|
||||||
set_z,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editor.set_block(
|
|
||||||
STONE_BRICKS,
|
|
||||||
set_x,
|
|
||||||
current_y - 1,
|
|
||||||
set_z,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add support pillars for elevated highways/bridges
|
// Add support pillars for elevated highways
|
||||||
if (effective_elevation != 0 || use_absolute_y) && current_y > 0 {
|
if effective_elevation != 0 && current_y > 0 {
|
||||||
if use_absolute_y {
|
add_highway_support_pillar(
|
||||||
add_highway_support_pillar_absolute(
|
editor,
|
||||||
editor,
|
set_x,
|
||||||
set_x,
|
current_y,
|
||||||
current_y,
|
set_z,
|
||||||
set_z,
|
dx,
|
||||||
dx,
|
dz,
|
||||||
dz,
|
block_range,
|
||||||
block_range,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
add_highway_support_pillar(
|
|
||||||
editor,
|
|
||||||
set_x,
|
|
||||||
current_y,
|
|
||||||
set_z,
|
|
||||||
dx,
|
|
||||||
dz,
|
|
||||||
block_range,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,49 +402,27 @@ fn generate_highways_internal(
|
|||||||
for dz in -block_range..=block_range {
|
for dz in -block_range..=block_range {
|
||||||
let outline_x = x - block_range - 1;
|
let outline_x = x - block_range - 1;
|
||||||
let outline_z = z + dz;
|
let outline_z = z + dz;
|
||||||
if use_absolute_y {
|
editor.set_block(
|
||||||
editor.set_block_absolute(
|
LIGHT_GRAY_CONCRETE,
|
||||||
LIGHT_GRAY_CONCRETE,
|
outline_x,
|
||||||
outline_x,
|
current_y,
|
||||||
current_y,
|
outline_z,
|
||||||
outline_z,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editor.set_block(
|
|
||||||
LIGHT_GRAY_CONCRETE,
|
|
||||||
outline_x,
|
|
||||||
current_y,
|
|
||||||
outline_z,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Right outline
|
// Right outline
|
||||||
for dz in -block_range..=block_range {
|
for dz in -block_range..=block_range {
|
||||||
let outline_x = x + block_range + 1;
|
let outline_x = x + block_range + 1;
|
||||||
let outline_z = z + dz;
|
let outline_z = z + dz;
|
||||||
if use_absolute_y {
|
editor.set_block(
|
||||||
editor.set_block_absolute(
|
LIGHT_GRAY_CONCRETE,
|
||||||
LIGHT_GRAY_CONCRETE,
|
outline_x,
|
||||||
outline_x,
|
current_y,
|
||||||
current_y,
|
outline_z,
|
||||||
outline_z,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editor.set_block(
|
|
||||||
LIGHT_GRAY_CONCRETE,
|
|
||||||
outline_x,
|
|
||||||
current_y,
|
|
||||||
outline_z,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,25 +431,14 @@ fn generate_highways_internal(
|
|||||||
if stripe_length < dash_length {
|
if stripe_length < dash_length {
|
||||||
let stripe_x: i32 = *x;
|
let stripe_x: i32 = *x;
|
||||||
let stripe_z: i32 = *z;
|
let stripe_z: i32 = *z;
|
||||||
if use_absolute_y {
|
editor.set_block(
|
||||||
editor.set_block_absolute(
|
WHITE_CONCRETE,
|
||||||
WHITE_CONCRETE,
|
stripe_x,
|
||||||
stripe_x,
|
current_y,
|
||||||
current_y,
|
stripe_z,
|
||||||
stripe_z,
|
Some(&[BLACK_CONCRETE]),
|
||||||
Some(&[BLACK_CONCRETE]),
|
None,
|
||||||
None,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editor.set_block(
|
|
||||||
WHITE_CONCRETE,
|
|
||||||
stripe_x,
|
|
||||||
current_y,
|
|
||||||
stripe_z,
|
|
||||||
Some(&[BLACK_CONCRETE]),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment stripe_length and reset after completing a dash and gap
|
// Increment stripe_length and reset after completing a dash and gap
|
||||||
@@ -767,46 +582,6 @@ fn add_highway_support_pillar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add support pillars for bridges using absolute Y coordinates
|
|
||||||
/// Pillars extend from ground level up to the bridge deck
|
|
||||||
fn add_highway_support_pillar_absolute(
|
|
||||||
editor: &mut WorldEditor,
|
|
||||||
x: i32,
|
|
||||||
bridge_deck_y: i32,
|
|
||||||
z: i32,
|
|
||||||
dx: i32,
|
|
||||||
dz: i32,
|
|
||||||
_block_range: i32, // Keep for future use
|
|
||||||
) {
|
|
||||||
// Only add pillars at specific intervals and positions
|
|
||||||
if dx == 0 && dz == 0 && (x + z) % 8 == 0 {
|
|
||||||
// Get the actual ground level at this position
|
|
||||||
let ground_y = editor.get_ground_level(x, z);
|
|
||||||
|
|
||||||
// Add pillar from ground up to bridge deck
|
|
||||||
// Only if the bridge is actually above the ground
|
|
||||||
if bridge_deck_y > ground_y {
|
|
||||||
for y in (ground_y + 1)..bridge_deck_y {
|
|
||||||
editor.set_block_absolute(STONE_BRICKS, x, y, z, None, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add pillar base at ground level
|
|
||||||
for base_dx in -1..=1 {
|
|
||||||
for base_dz in -1..=1 {
|
|
||||||
editor.set_block_absolute(
|
|
||||||
STONE_BRICKS,
|
|
||||||
x + base_dx,
|
|
||||||
ground_y,
|
|
||||||
z + base_dz,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a siding using stone brick slabs
|
/// Generates a siding using stone brick slabs
|
||||||
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||||
let mut previous_node: Option<XZPoint> = None;
|
let mut previous_node: Option<XZPoint> = None;
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::deterministic_rng::element_rng;
|
|
||||||
use crate::element_processing::tree::Tree;
|
use crate::element_processing::tree::Tree;
|
||||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
use crate::floodfill::flood_fill_area;
|
||||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
pub fn generate_landuse(
|
pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
|
||||||
editor: &mut WorldEditor,
|
|
||||||
element: &ProcessedWay,
|
|
||||||
args: &Args,
|
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
building_footprints: &BuildingFootprintBitmap,
|
|
||||||
) {
|
|
||||||
// Determine block type based on landuse tag
|
// Determine block type based on landuse tag
|
||||||
let binding: String = "".to_string();
|
let binding: String = "".to_string();
|
||||||
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
|
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
|
||||||
|
|
||||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
|
||||||
let mut rng = element_rng(element.id);
|
|
||||||
|
|
||||||
let block_type = match landuse_tag.as_str() {
|
let block_type = match landuse_tag.as_str() {
|
||||||
"greenfield" | "meadow" | "grass" | "orchard" | "forest" => GRASS_BLOCK,
|
"greenfield" | "meadow" | "grass" | "orchard" | "forest" => GRASS_BLOCK,
|
||||||
"farmland" => FARMLAND,
|
"farmland" => FARMLAND,
|
||||||
@@ -32,13 +22,13 @@ pub fn generate_landuse(
|
|||||||
if residential_tag == "rural" {
|
if residential_tag == "rural" {
|
||||||
GRASS_BLOCK
|
GRASS_BLOCK
|
||||||
} else {
|
} else {
|
||||||
STONE_BRICKS // Placeholder, will be randomized per-block
|
STONE_BRICKS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"commercial" => SMOOTH_STONE, // Placeholder, will be randomized per-block
|
"commercial" => SMOOTH_STONE,
|
||||||
"education" => POLISHED_ANDESITE,
|
"education" => POLISHED_ANDESITE,
|
||||||
"religious" => POLISHED_ANDESITE,
|
"religious" => POLISHED_ANDESITE,
|
||||||
"industrial" => STONE, // Placeholder, will be randomized per-block
|
"industrial" => COBBLESTONE,
|
||||||
"military" => GRAY_CONCRETE,
|
"military" => GRAY_CONCRETE,
|
||||||
"railway" => GRAVEL,
|
"railway" => GRAVEL,
|
||||||
"landfill" => {
|
"landfill" => {
|
||||||
@@ -54,56 +44,19 @@ pub fn generate_landuse(
|
|||||||
_ => GRASS_BLOCK,
|
_ => GRASS_BLOCK,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the area of the landuse element using cache
|
// Get the area of the landuse element
|
||||||
let floor_area: Vec<(i32, i32)> =
|
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||||
|
|
||||||
|
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||||
|
|
||||||
for (x, z) in floor_area {
|
for (x, z) in floor_area {
|
||||||
// Apply per-block randomness for certain landuse types
|
|
||||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
|
||||||
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
|
|
||||||
let random_value = rng.gen_range(0..100);
|
|
||||||
if random_value < 72 {
|
|
||||||
STONE_BRICKS
|
|
||||||
} else if random_value < 87 {
|
|
||||||
CRACKED_STONE_BRICKS
|
|
||||||
} else if random_value < 92 {
|
|
||||||
STONE
|
|
||||||
} else {
|
|
||||||
COBBLESTONE
|
|
||||||
}
|
|
||||||
} else if landuse_tag == "commercial" {
|
|
||||||
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
|
|
||||||
let random_value = rng.gen_range(0..100);
|
|
||||||
if random_value < 40 {
|
|
||||||
SMOOTH_STONE
|
|
||||||
} else if random_value < 70 {
|
|
||||||
STONE_BRICKS
|
|
||||||
} else if random_value < 90 {
|
|
||||||
STONE
|
|
||||||
} else {
|
|
||||||
COBBLESTONE
|
|
||||||
}
|
|
||||||
} else if landuse_tag == "industrial" {
|
|
||||||
// Industrial: primarily stone, with some stone bricks and smooth stone
|
|
||||||
let random_value = rng.gen_range(0..100);
|
|
||||||
if random_value < 70 {
|
|
||||||
STONE
|
|
||||||
} else if random_value < 90 {
|
|
||||||
STONE_BRICKS
|
|
||||||
} else {
|
|
||||||
SMOOTH_STONE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
block_type
|
|
||||||
};
|
|
||||||
|
|
||||||
if landuse_tag == "traffic_island" {
|
if landuse_tag == "traffic_island" {
|
||||||
editor.set_block(actual_block, x, 1, z, None, None);
|
editor.set_block(block_type, x, 1, z, None, None);
|
||||||
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
||||||
editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));
|
editor.set_block(block_type, x, 0, z, None, Some(&[SPONGE]));
|
||||||
} else {
|
} else {
|
||||||
editor.set_block(actual_block, x, 0, z, None, None);
|
editor.set_block(block_type, x, 0, z, None, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add specific features for different landuse types
|
// Add specific features for different landuse types
|
||||||
@@ -131,7 +84,7 @@ pub fn generate_landuse(
|
|||||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||||
}
|
}
|
||||||
} else if random_choice < 33 {
|
} else if random_choice < 33 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
} else if random_choice < 35 {
|
} else if random_choice < 35 {
|
||||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||||
}
|
}
|
||||||
@@ -141,7 +94,7 @@ pub fn generate_landuse(
|
|||||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||||
let random_choice: i32 = rng.gen_range(0..30);
|
let random_choice: i32 = rng.gen_range(0..30);
|
||||||
if random_choice == 20 {
|
if random_choice == 20 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
} else if random_choice == 2 {
|
} else if random_choice == 2 {
|
||||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
let flower_block: Block = match rng.gen_range(1..=5) {
|
||||||
1 => OAK_LEAVES,
|
1 => OAK_LEAVES,
|
||||||
@@ -152,11 +105,7 @@ pub fn generate_landuse(
|
|||||||
};
|
};
|
||||||
editor.set_block(flower_block, x, 1, z, None, None);
|
editor.set_block(flower_block, x, 1, z, None, None);
|
||||||
} else if random_choice <= 12 {
|
} else if random_choice <= 12 {
|
||||||
if rng.gen_range(0..100) < 12 {
|
editor.set_block(GRASS, x, 1, z, None, None);
|
||||||
editor.set_block(FERN, x, 1, z, None, None);
|
|
||||||
} else {
|
|
||||||
editor.set_block(GRASS, x, 1, z, None, None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,8 +207,7 @@ pub fn generate_landuse(
|
|||||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||||
match rng.gen_range(0..200) {
|
match rng.gen_range(0..200) {
|
||||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||||
1..=8 => editor.set_block(FERN, x, 1, z, None, None),
|
1..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||||
9..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,8 +216,7 @@ pub fn generate_landuse(
|
|||||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||||
match rng.gen_range(0..200) {
|
match rng.gen_range(0..200) {
|
||||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
1..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||||
3..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,13 +225,11 @@ pub fn generate_landuse(
|
|||||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||||
let random_choice: i32 = rng.gen_range(0..1001);
|
let random_choice: i32 = rng.gen_range(0..1001);
|
||||||
if random_choice < 5 {
|
if random_choice < 5 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
} else if random_choice < 6 {
|
} else if random_choice < 6 {
|
||||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||||
} else if random_choice < 9 {
|
} else if random_choice < 9 {
|
||||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||||
} else if random_choice < 40 {
|
|
||||||
editor.set_block(FERN, x, 1, z, None, None);
|
|
||||||
} else if random_choice < 800 {
|
} else if random_choice < 800 {
|
||||||
editor.set_block(GRASS, x, 1, z, None, None);
|
editor.set_block(GRASS, x, 1, z, None, None);
|
||||||
}
|
}
|
||||||
@@ -292,12 +237,11 @@ pub fn generate_landuse(
|
|||||||
}
|
}
|
||||||
"orchard" => {
|
"orchard" => {
|
||||||
if x % 18 == 0 && z % 10 == 0 {
|
if x % 18 == 0 && z % 10 == 0 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||||
match rng.gen_range(0..100) {
|
match rng.gen_range(0..100) {
|
||||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
1..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||||
3..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,20 +275,12 @@ pub fn generate_landuse_from_relation(
|
|||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
rel: &ProcessedRelation,
|
rel: &ProcessedRelation,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
building_footprints: &BuildingFootprintBitmap,
|
|
||||||
) {
|
) {
|
||||||
if rel.tags.contains_key("landuse") {
|
if rel.tags.contains_key("landuse") {
|
||||||
// Generate individual ways with their original tags
|
// Generate individual ways with their original tags
|
||||||
for member in &rel.members {
|
for member in &rel.members {
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
generate_landuse(
|
generate_landuse(editor, &member.way.clone(), args);
|
||||||
editor,
|
|
||||||
&member.way.clone(),
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,13 +302,7 @@ pub fn generate_landuse_from_relation(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate landuse area from combined way
|
// Generate landuse area from combined way
|
||||||
generate_landuse(
|
generate_landuse(editor, &combined_way, args);
|
||||||
editor,
|
|
||||||
&combined_way,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::bresenham::bresenham_line;
|
use crate::bresenham::bresenham_line;
|
||||||
use crate::deterministic_rng::element_rng;
|
|
||||||
use crate::element_processing::tree::Tree;
|
use crate::element_processing::tree::Tree;
|
||||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
use crate::floodfill::flood_fill_area;
|
||||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
pub fn generate_leisure(
|
pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
|
||||||
editor: &mut WorldEditor,
|
|
||||||
element: &ProcessedWay,
|
|
||||||
args: &Args,
|
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
building_footprints: &BuildingFootprintBitmap,
|
|
||||||
) {
|
|
||||||
if let Some(leisure_type) = element.tags.get("leisure") {
|
if let Some(leisure_type) = element.tags.get("leisure") {
|
||||||
let mut previous_node: Option<(i32, i32)> = None;
|
let mut previous_node: Option<(i32, i32)> = None;
|
||||||
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
|
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
|
||||||
@@ -81,13 +74,15 @@ pub fn generate_leisure(
|
|||||||
previous_node = Some((node.x, node.z));
|
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) {
|
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)> =
|
let filled_area: Vec<(i32, i32)> =
|
||||||
flood_fill_cache.get_or_compute(element, 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(element.id);
|
|
||||||
|
|
||||||
for (x, z) in filled_area {
|
for (x, z) in filled_area {
|
||||||
editor.set_block(block_type, x, 0, z, Some(&[GRASS_BLOCK]), None);
|
editor.set_block(block_type, x, 0, z, Some(&[GRASS_BLOCK]), None);
|
||||||
@@ -96,6 +91,7 @@ pub fn generate_leisure(
|
|||||||
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
|
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
|
||||||
&& editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK]))
|
&& 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);
|
let random_choice: i32 = rng.gen_range(0..1000);
|
||||||
|
|
||||||
match random_choice {
|
match random_choice {
|
||||||
@@ -119,7 +115,7 @@ pub fn generate_leisure(
|
|||||||
}
|
}
|
||||||
105..120 => {
|
105..120 => {
|
||||||
// Tree
|
// Tree
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -127,6 +123,7 @@ pub fn generate_leisure(
|
|||||||
|
|
||||||
// Add playground or recreation ground features
|
// Add playground or recreation ground features
|
||||||
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
|
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);
|
let random_choice: i32 = rng.gen_range(0..5000);
|
||||||
|
|
||||||
match random_choice {
|
match random_choice {
|
||||||
@@ -179,20 +176,12 @@ pub fn generate_leisure_from_relation(
|
|||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
rel: &ProcessedRelation,
|
rel: &ProcessedRelation,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
building_footprints: &BuildingFootprintBitmap,
|
|
||||||
) {
|
) {
|
||||||
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||||
// First generate individual ways with their original tags
|
// First generate individual ways with their original tags
|
||||||
for member in &rel.members {
|
for member in &rel.members {
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
generate_leisure(
|
generate_leisure(editor, &member.way, args);
|
||||||
editor,
|
|
||||||
&member.way,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,12 +201,6 @@ pub fn generate_leisure_from_relation(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate leisure area from combined way
|
// Generate leisure area from combined way
|
||||||
generate_leisure(
|
generate_leisure(editor, &combined_way, args);
|
||||||
editor,
|
|
||||||
&combined_way,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::bresenham::bresenham_line;
|
use crate::bresenham::bresenham_line;
|
||||||
use crate::deterministic_rng::element_rng;
|
|
||||||
use crate::element_processing::tree::Tree;
|
use crate::element_processing::tree::Tree;
|
||||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
use crate::floodfill::flood_fill_area;
|
||||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
pub fn generate_natural(
|
pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
||||||
editor: &mut WorldEditor,
|
|
||||||
element: &ProcessedElement,
|
|
||||||
args: &Args,
|
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
building_footprints: &BuildingFootprintBitmap,
|
|
||||||
) {
|
|
||||||
if let Some(natural_type) = element.tags().get("natural") {
|
if let Some(natural_type) = element.tags().get("natural") {
|
||||||
if natural_type == "tree" {
|
if natural_type == "tree" {
|
||||||
if let ProcessedElement::Node(node) = element {
|
if let ProcessedElement::Node(node) = element {
|
||||||
let x: i32 = node.x;
|
let x: i32 = node.x;
|
||||||
let z: i32 = node.z;
|
let z: i32 = node.z;
|
||||||
|
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut previous_node: Option<(i32, i32)> = None;
|
let mut previous_node: Option<(i32, i32)> = None;
|
||||||
@@ -76,13 +69,17 @@ pub fn generate_natural(
|
|||||||
previous_node = Some((x, z));
|
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) {
|
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)> =
|
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: rand::prelude::ThreadRng = rand::thread_rng();
|
||||||
let mut rng = element_rng(way.id);
|
|
||||||
|
|
||||||
for (x, z) in filled_area {
|
for (x, z) in filled_area {
|
||||||
editor.set_block(block_type, x, 0, z, None, None);
|
editor.set_block(block_type, x, 0, z, None, None);
|
||||||
@@ -135,7 +132,7 @@ pub fn generate_natural(
|
|||||||
}
|
}
|
||||||
let random_choice = rng.gen_range(0..500);
|
let random_choice = rng.gen_range(0..500);
|
||||||
if random_choice == 0 {
|
if random_choice == 0 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
} else if random_choice == 1 {
|
} else if random_choice == 1 {
|
||||||
let flower_block = match rng.gen_range(1..=4) {
|
let flower_block = match rng.gen_range(1..=4) {
|
||||||
1 => RED_FLOWER,
|
1 => RED_FLOWER,
|
||||||
@@ -164,7 +161,7 @@ pub fn generate_natural(
|
|||||||
}
|
}
|
||||||
let random_choice: i32 = rng.gen_range(0..30);
|
let random_choice: i32 = rng.gen_range(0..30);
|
||||||
if random_choice == 0 {
|
if random_choice == 0 {
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
} else if random_choice == 1 {
|
} else if random_choice == 1 {
|
||||||
let flower_block = match rng.gen_range(1..=4) {
|
let flower_block = match rng.gen_range(1..=4) {
|
||||||
1 => RED_FLOWER,
|
1 => RED_FLOWER,
|
||||||
@@ -223,11 +220,7 @@ pub fn generate_natural(
|
|||||||
// TODO implement mangrove
|
// TODO implement mangrove
|
||||||
let random_choice: i32 = rng.gen_range(0..40);
|
let random_choice: i32 = rng.gen_range(0..40);
|
||||||
if random_choice == 0 {
|
if random_choice == 0 {
|
||||||
Tree::create(
|
Tree::create(editor, (x, 1, z));
|
||||||
editor,
|
|
||||||
(x, 1, z),
|
|
||||||
Some(building_footprints),
|
|
||||||
);
|
|
||||||
} else if random_choice < 35 {
|
} else if random_choice < 35 {
|
||||||
editor.set_block(GRASS, x, 1, z, None, None);
|
editor.set_block(GRASS, x, 1, z, None, None);
|
||||||
}
|
}
|
||||||
@@ -311,7 +304,6 @@ pub fn generate_natural(
|
|||||||
Tree::create(
|
Tree::create(
|
||||||
editor,
|
editor,
|
||||||
(cluster_x, 1, cluster_z),
|
(cluster_x, 1, cluster_z),
|
||||||
Some(building_footprints),
|
|
||||||
);
|
);
|
||||||
} else if vegetation_chance < 15 {
|
} else if vegetation_chance < 15 {
|
||||||
// 15% chance for grass
|
// 15% chance for grass
|
||||||
@@ -424,7 +416,7 @@ pub fn generate_natural(
|
|||||||
let hill_chance = rng.gen_range(0..1000);
|
let hill_chance = rng.gen_range(0..1000);
|
||||||
if hill_chance == 0 {
|
if hill_chance == 0 {
|
||||||
// 0.1% chance for rare trees
|
// 0.1% chance for rare trees
|
||||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
Tree::create(editor, (x, 1, z));
|
||||||
} else if hill_chance < 50 {
|
} else if hill_chance < 50 {
|
||||||
// 5% chance for flowers
|
// 5% chance for flowers
|
||||||
let flower_block = match rng.gen_range(1..=4) {
|
let flower_block = match rng.gen_range(1..=4) {
|
||||||
@@ -456,20 +448,12 @@ pub fn generate_natural_from_relation(
|
|||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
rel: &ProcessedRelation,
|
rel: &ProcessedRelation,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
building_footprints: &BuildingFootprintBitmap,
|
|
||||||
) {
|
) {
|
||||||
if rel.tags.contains_key("natural") {
|
if rel.tags.contains_key("natural") {
|
||||||
// Generate individual ways with their original tags
|
// Generate individual ways with their original tags
|
||||||
for member in &rel.members {
|
for member in &rel.members {
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
if member.role == ProcessedMemberRole::Outer {
|
||||||
generate_natural(
|
generate_natural(editor, &ProcessedElement::Way(member.way.clone()), args);
|
||||||
editor,
|
|
||||||
&ProcessedElement::Way((*member.way).clone()),
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,13 +475,7 @@ pub fn generate_natural_from_relation(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate natural area from combined way
|
// Generate natural area from combined way
|
||||||
generate_natural(
|
generate_natural(editor, &ProcessedElement::Way(combined_way), args);
|
||||||
editor,
|
|
||||||
&ProcessedElement::Way(combined_way),
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::deterministic_rng::coord_rng;
|
|
||||||
use crate::floodfill_cache::BuildingFootprintBitmap;
|
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
@@ -109,25 +107,7 @@ pub struct Tree<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tree<'_> {
|
impl Tree<'_> {
|
||||||
/// Creates a tree at the specified coordinates.
|
pub fn create(editor: &mut WorldEditor, (x, y, z): Coord) {
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `editor` - The world editor to place blocks
|
|
||||||
/// * `(x, y, z)` - The base coordinates for the tree
|
|
||||||
/// * `building_footprints` - Optional bitmap of (x, z) coordinates that are inside buildings.
|
|
||||||
/// If provided, trees will not be placed at coordinates within this bitmap.
|
|
||||||
pub fn create(
|
|
||||||
editor: &mut WorldEditor,
|
|
||||||
(x, y, z): Coord,
|
|
||||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
|
||||||
) {
|
|
||||||
// Skip if this coordinate is inside a building
|
|
||||||
if let Some(footprints) = building_footprints {
|
|
||||||
if footprints.contains(x, z) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut blacklist: Vec<Block> = Vec::new();
|
let mut blacklist: Vec<Block> = Vec::new();
|
||||||
blacklist.extend(Self::get_building_wall_blocks());
|
blacklist.extend(Self::get_building_wall_blocks());
|
||||||
blacklist.extend(Self::get_building_floor_blocks());
|
blacklist.extend(Self::get_building_floor_blocks());
|
||||||
@@ -135,9 +115,7 @@ impl Tree<'_> {
|
|||||||
blacklist.extend(Self::get_functional_blocks());
|
blacklist.extend(Self::get_functional_blocks());
|
||||||
blacklist.push(WATER);
|
blacklist.push(WATER);
|
||||||
|
|
||||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
let mut rng = rand::thread_rng();
|
||||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
|
||||||
let mut rng = coord_rng(x, z, 0);
|
|
||||||
|
|
||||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
||||||
1 => TreeType::Oak,
|
1 => TreeType::Oak,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use geo::orient::{Direction, Orient};
|
use geo::orient::{Direction, Orient};
|
||||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::clipping::clip_water_ring_to_bbox;
|
use crate::clipping::clip_water_ring_to_bbox;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -14,13 +15,15 @@ pub fn generate_water_area_from_way(
|
|||||||
element: &ProcessedWay,
|
element: &ProcessedWay,
|
||||||
_xzbbox: &XZBBox,
|
_xzbbox: &XZBBox,
|
||||||
) {
|
) {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let outers = [element.nodes.clone()];
|
let outers = [element.nodes.clone()];
|
||||||
if !verify_closed_rings(&outers) {
|
if !verify_closed_rings(&outers) {
|
||||||
println!("Skipping way {} due to invalid polygon", element.id);
|
println!("Skipping way {} due to invalid polygon", element.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_water_areas(editor, &outers, &[]);
|
generate_water_areas(editor, &outers, &[], start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_water_areas_from_relation(
|
pub fn generate_water_areas_from_relation(
|
||||||
@@ -28,6 +31,8 @@ pub fn generate_water_areas_from_relation(
|
|||||||
element: &ProcessedRelation,
|
element: &ProcessedRelation,
|
||||||
xzbbox: &XZBBox,
|
xzbbox: &XZBBox,
|
||||||
) {
|
) {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
// Check if this is a water relation (either with water tag or natural=water)
|
// Check if this is a water relation (either with water tag or natural=water)
|
||||||
let is_water = element.tags.contains_key("water")
|
let is_water = element.tags.contains_key("water")
|
||||||
|| element
|
|| element
|
||||||
@@ -118,13 +123,14 @@ pub fn generate_water_areas_from_relation(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_water_areas(editor, &outers, &inners);
|
generate_water_areas(editor, &outers, &inners, start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_water_areas(
|
fn generate_water_areas(
|
||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
outers: &[Vec<ProcessedNode>],
|
outers: &[Vec<ProcessedNode>],
|
||||||
inners: &[Vec<ProcessedNode>],
|
inners: &[Vec<ProcessedNode>],
|
||||||
|
start_time: Instant,
|
||||||
) {
|
) {
|
||||||
// Calculate polygon bounding box to limit fill area
|
// Calculate polygon bounding box to limit fill area
|
||||||
let mut poly_min_x = i32::MAX;
|
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<_>>())
|
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||||
.collect();
|
.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.
|
/// Merges way segments that share endpoints into closed rings.
|
||||||
@@ -300,6 +308,7 @@ fn inverse_floodfill(
|
|||||||
outers: Vec<Vec<XZPoint>>,
|
outers: Vec<Vec<XZPoint>>,
|
||||||
inners: Vec<Vec<XZPoint>>,
|
inners: Vec<Vec<XZPoint>>,
|
||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
|
start_time: Instant,
|
||||||
) {
|
) {
|
||||||
// Convert to geo Polygons with normalized winding order
|
// Convert to geo Polygons with normalized winding order
|
||||||
let inners: Vec<_> = inners
|
let inners: Vec<_> = inners
|
||||||
@@ -332,7 +341,14 @@ fn inverse_floodfill(
|
|||||||
})
|
})
|
||||||
.collect();
|
.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(
|
fn inverse_floodfill_recursive(
|
||||||
@@ -341,11 +357,12 @@ fn inverse_floodfill_recursive(
|
|||||||
outers: &[Polygon],
|
outers: &[Polygon],
|
||||||
inners: &[Polygon],
|
inners: &[Polygon],
|
||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
|
start_time: Instant,
|
||||||
) {
|
) {
|
||||||
// Check if we've exceeded 40 seconds
|
// Check if we've exceeded 25 seconds
|
||||||
// if start_time.elapsed().as_secs() > 40 {
|
if start_time.elapsed().as_secs() > 25 {
|
||||||
// println!("Water area generation exceeded 40 seconds, continuing anyway");
|
println!("Water area generation exceeded 25 seconds, continuing anyway");
|
||||||
// }
|
}
|
||||||
|
|
||||||
const ITERATIVE_THRES: i64 = 10_000;
|
const ITERATIVE_THRES: i64 = 10_000;
|
||||||
|
|
||||||
@@ -400,6 +417,7 @@ fn inverse_floodfill_recursive(
|
|||||||
&outers_intersects,
|
&outers_intersects,
|
||||||
&inners_intersects,
|
&inners_intersects,
|
||||||
editor,
|
editor,
|
||||||
|
start_time,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
|
use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance};
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
use crate::telemetry::{send_log, LogLevel};
|
use crate::telemetry::{send_log, LogLevel};
|
||||||
use crate::{
|
|
||||||
coordinate_system::{geographic::LLBBox, transformation::geo_distance},
|
|
||||||
progress::emit_gui_progress_update,
|
|
||||||
};
|
|
||||||
use image::Rgb;
|
use image::Rgb;
|
||||||
use rayon::prelude::*;
|
use std::path::Path;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
/// Maximum Y coordinate in Minecraft (build height limit)
|
/// Maximum Y coordinate in Minecraft (build height limit)
|
||||||
const MAX_Y: i32 = 319;
|
const MAX_Y: i32 = 319;
|
||||||
|
/// Scale factor for converting real elevation to Minecraft heights
|
||||||
|
const BASE_HEIGHT_SCALE: f64 = 0.7;
|
||||||
/// AWS S3 Terrarium tiles endpoint (no API key required)
|
/// AWS S3 Terrarium tiles endpoint (no API key required)
|
||||||
const AWS_TERRARIUM_URL: &str =
|
const AWS_TERRARIUM_URL: &str =
|
||||||
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
|
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
|
||||||
@@ -19,10 +17,6 @@ const TERRARIUM_OFFSET: f64 = 32768.0;
|
|||||||
const MIN_ZOOM: u8 = 10;
|
const MIN_ZOOM: u8 = 10;
|
||||||
/// Maximum zoom level for terrain tiles
|
/// Maximum zoom level for terrain tiles
|
||||||
const MAX_ZOOM: u8 = 15;
|
const MAX_ZOOM: u8 = 15;
|
||||||
/// Maximum concurrent tile downloads to be respectful to AWS
|
|
||||||
const MAX_CONCURRENT_DOWNLOADS: usize = 8;
|
|
||||||
/// Maximum age for cached tiles in days before they are cleaned up
|
|
||||||
const TILE_CACHE_MAX_AGE_DAYS: u64 = 7;
|
|
||||||
|
|
||||||
/// Holds processed elevation data and metadata
|
/// Holds processed elevation data and metadata
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -35,93 +29,6 @@ pub struct ElevationData {
|
|||||||
pub(crate) height: usize,
|
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>;
|
|
||||||
|
|
||||||
/// Cleans up old cached tiles from the tile cache directory.
|
|
||||||
/// Only deletes .png files within the arnis-tile-cache directory that are older than TILE_CACHE_MAX_AGE_DAYS.
|
|
||||||
/// This function is safe and will not delete files outside the cache directory or fail on errors.
|
|
||||||
pub fn cleanup_old_cached_tiles() {
|
|
||||||
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
|
|
||||||
|
|
||||||
if !tile_cache_dir.exists() || !tile_cache_dir.is_dir() {
|
|
||||||
return; // Nothing to clean up
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_age = std::time::Duration::from_secs(TILE_CACHE_MAX_AGE_DAYS * 24 * 60 * 60);
|
|
||||||
let now = std::time::SystemTime::now();
|
|
||||||
let mut deleted_count = 0;
|
|
||||||
let mut error_count = 0;
|
|
||||||
|
|
||||||
// Read directory entries
|
|
||||||
let entries = match std::fs::read_dir(&tile_cache_dir) {
|
|
||||||
Ok(entries) => entries,
|
|
||||||
Err(_) => {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
// Safety check: only process .png files within the cache directory
|
|
||||||
if !path.is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the file is a .png and follows our naming pattern (z{zoom}_x{x}_y{y}.png)
|
|
||||||
let file_name = match path.file_name().and_then(|n| n.to_str()) {
|
|
||||||
Some(name) => name,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !file_name.ends_with(".png") || !file_name.starts_with('z') {
|
|
||||||
continue; // Skip files that don't match our tile naming pattern
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file age
|
|
||||||
let metadata = match std::fs::metadata(&path) {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let modified = match metadata.modified() {
|
|
||||||
Ok(time) => time,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let age = match now.duration_since(modified) {
|
|
||||||
Ok(duration) => duration,
|
|
||||||
Err(_) => continue, // File modified in the future? Skip it.
|
|
||||||
};
|
|
||||||
|
|
||||||
if age > max_age {
|
|
||||||
match std::fs::remove_file(&path) {
|
|
||||||
Ok(()) => deleted_count += 1,
|
|
||||||
Err(e) => {
|
|
||||||
// Log but don't fail, this is a best-effort cleanup
|
|
||||||
if error_count == 0 {
|
|
||||||
eprintln!(
|
|
||||||
"Warning: Failed to delete old cached tile {}: {e}",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
error_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if deleted_count > 0 {
|
|
||||||
println!("Cleaned up {deleted_count} old cached elevation tiles (older than {TILE_CACHE_MAX_AGE_DAYS} days)");
|
|
||||||
}
|
|
||||||
if error_count > 1 {
|
|
||||||
eprintln!("Warning: Failed to delete {error_count} old cached tiles");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates appropriate zoom level for the given bounding box
|
/// Calculates appropriate zoom level for the given bounding box
|
||||||
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
|
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
|
||||||
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
|
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
|
||||||
@@ -139,131 +46,28 @@ fn lat_lng_to_tile(lat: f64, lng: f64, zoom: u8) -> (u32, u32) {
|
|||||||
(x, y)
|
(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum number of retry attempts for tile downloads
|
/// Downloads a tile from AWS Terrain Tiles service
|
||||||
const TILE_DOWNLOAD_MAX_RETRIES: u32 = 3;
|
|
||||||
|
|
||||||
/// Base delay in milliseconds for exponential backoff between retries
|
|
||||||
const TILE_DOWNLOAD_RETRY_BASE_DELAY_MS: u64 = 500;
|
|
||||||
|
|
||||||
/// Downloads a tile from AWS Terrain Tiles service with retry logic
|
|
||||||
fn download_tile(
|
fn download_tile(
|
||||||
client: &reqwest::blocking::Client,
|
client: &reqwest::blocking::Client,
|
||||||
tile_x: u32,
|
tile_x: u32,
|
||||||
tile_y: u32,
|
tile_y: u32,
|
||||||
zoom: u8,
|
zoom: u8,
|
||||||
tile_path: &Path,
|
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");
|
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||||
let url: String = AWS_TERRARIUM_URL
|
let url: String = AWS_TERRARIUM_URL
|
||||||
.replace("{z}", &zoom.to_string())
|
.replace("{z}", &zoom.to_string())
|
||||||
.replace("{x}", &tile_x.to_string())
|
.replace("{x}", &tile_x.to_string())
|
||||||
.replace("{y}", &tile_y.to_string());
|
.replace("{y}", &tile_y.to_string());
|
||||||
|
|
||||||
let mut last_error: String = String::new();
|
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
||||||
|
response.error_for_status_ref()?;
|
||||||
for attempt in 0..TILE_DOWNLOAD_MAX_RETRIES {
|
let bytes = response.bytes()?;
|
||||||
if attempt > 0 {
|
std::fs::write(tile_path, &bytes)?;
|
||||||
// Exponential backoff: 500ms, 1000ms, 2000ms...
|
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
||||||
let delay_ms = TILE_DOWNLOAD_RETRY_BASE_DELAY_MS * (1 << (attempt - 1));
|
|
||||||
eprintln!(
|
|
||||||
"Retry attempt {}/{} for tile x={},y={},z={} after {}ms delay",
|
|
||||||
attempt,
|
|
||||||
TILE_DOWNLOAD_MAX_RETRIES - 1,
|
|
||||||
tile_x,
|
|
||||||
tile_y,
|
|
||||||
zoom,
|
|
||||||
delay_ms
|
|
||||||
);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
match download_tile_once(client, &url, tile_path) {
|
|
||||||
Ok(img) => return Ok(img),
|
|
||||||
Err(e) => {
|
|
||||||
last_error = e;
|
|
||||||
if attempt < TILE_DOWNLOAD_MAX_RETRIES - 1 {
|
|
||||||
eprintln!(
|
|
||||||
"Tile download failed for x={},y={},z={}: {}",
|
|
||||||
tile_x, tile_y, zoom, last_error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!(
|
|
||||||
"Failed to download tile x={},y={},z={} after {} attempts: {}",
|
|
||||||
tile_x, tile_y, zoom, TILE_DOWNLOAD_MAX_RETRIES, last_error
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Single download attempt for a tile (no retries)
|
|
||||||
fn download_tile_once(
|
|
||||||
client: &reqwest::blocking::Client,
|
|
||||||
url: &str,
|
|
||||||
tile_path: &Path,
|
|
||||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
|
||||||
let response = client.get(url).send().map_err(|e| e.to_string())?;
|
|
||||||
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())?;
|
|
||||||
Ok(img.to_rgb8())
|
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() {
|
|
||||||
// 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(
|
pub fn fetch_elevation_data(
|
||||||
bbox: &LLBBox,
|
bbox: &LLBBox,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
@@ -287,69 +91,106 @@ pub fn fetch_elevation_data(
|
|||||||
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
|
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 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() {
|
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
|
// Fetch and process each tile
|
||||||
let client = reqwest::blocking::Client::new();
|
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 rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
|
||||||
let num_tiles = tiles.len();
|
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||||
println!(
|
let file_size = match std::fs::metadata(&tile_path) {
|
||||||
"Downloading {num_tiles} elevation tiles (up to {MAX_CONCURRENT_DOWNLOADS} concurrent)..."
|
Ok(metadata) => metadata.len(),
|
||||||
);
|
Err(_) => 0,
|
||||||
|
};
|
||||||
|
|
||||||
// Use a custom thread pool to limit concurrent downloads
|
if file_size < 1000 {
|
||||||
let thread_pool = rayon::ThreadPoolBuilder::new()
|
eprintln!(
|
||||||
.num_threads(MAX_CONCURRENT_DOWNLOADS)
|
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||||
.build()
|
tile_path.display(),
|
||||||
.map_err(|e| format!("Failed to create thread pool: {e}"))?;
|
file_size
|
||||||
|
);
|
||||||
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}");
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
send_log(
|
send_log(
|
||||||
LogLevel::Warning,
|
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());
|
|
||||||
emit_gui_progress_update(15.0, "Processing elevation...");
|
|
||||||
|
|
||||||
// Process tiles sequentially (writes to shared height_grid)
|
|
||||||
for ((tile_x, tile_y), rgb_img) in successful_tiles {
|
|
||||||
// Only process pixels that fall within the requested bbox
|
// Only process pixels that fall within the requested bbox
|
||||||
for (y, row) in rgb_img.rows().enumerate() {
|
for (y, row) in rgb_img.rows().enumerate() {
|
||||||
for (x, pixel) in row.enumerate() {
|
for (x, pixel) in row.enumerate() {
|
||||||
// Convert tile pixel coordinates back to geographic coordinates
|
// 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
|
* 360.0
|
||||||
- 180.0;
|
- 180.0;
|
||||||
let pixel_lat_rad = std::f64::consts::PI
|
let pixel_lat_rad = std::f64::consts::PI
|
||||||
* (1.0
|
* (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();
|
let pixel_lat = pixel_lat_rad.sinh().atan().to_degrees();
|
||||||
|
|
||||||
// Skip pixels outside the requested bounding box
|
// Skip pixels outside the requested bounding box
|
||||||
@@ -410,61 +251,58 @@ pub fn fetch_elevation_data(
|
|||||||
filter_elevation_outliers(&mut height_grid);
|
filter_elevation_outliers(&mut height_grid);
|
||||||
|
|
||||||
// Calculate blur sigma based on grid resolution
|
// Calculate blur sigma based on grid resolution
|
||||||
// Use sqrt scaling to maintain consistent relative smoothing across different area sizes.
|
// Reference points for tuning:
|
||||||
// This prevents larger generation areas from appearing noisier than smaller ones.
|
const SMALL_GRID_REF: f64 = 100.0; // Reference grid size
|
||||||
// Reference: 100x100 grid uses sigma=5 (5% relative blur)
|
const SMALL_SIGMA_REF: f64 = 15.0; // Sigma for 100x100 grid
|
||||||
const BASE_GRID_REF: f64 = 100.0;
|
const LARGE_GRID_REF: f64 = 1000.0; // Reference grid size
|
||||||
const BASE_SIGMA_REF: f64 = 5.0;
|
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);
|
let grid_size: f64 = (grid_width.min(grid_height) as f64).max(1.0);
|
||||||
|
|
||||||
// Sqrt scaling provides a good balance:
|
let sigma: f64 = if grid_size <= SMALL_GRID_REF {
|
||||||
// - 100x100: sigma = 5 (5% relative)
|
// Linear scaling for small grids
|
||||||
// - 500x500: sigma ≈ 11.2 (2.2% relative)
|
SMALL_SIGMA_REF * (grid_size / SMALL_GRID_REF)
|
||||||
// - 1000x1000: sigma ≈ 15.8 (1.6% relative)
|
} else {
|
||||||
// This smooths terrain proportionally while preserving more detail.
|
// Logarithmic scaling for larger grids
|
||||||
let sigma: f64 = BASE_SIGMA_REF * (grid_size / BASE_GRID_REF).sqrt();
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
//let blur_percentage: f64 = (sigma / grid_size) * 100.0;
|
/* eprintln!(
|
||||||
/*eprintln!(
|
"Grid: {}x{}, Blur sigma: {:.2}",
|
||||||
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
|
grid_width, grid_height, sigma
|
||||||
grid_width, grid_height, sigma, blur_percentage
|
); */
|
||||||
);*/
|
|
||||||
|
|
||||||
// Continue with the existing blur and conversion to Minecraft heights...
|
// Continue with the existing blur and conversion to Minecraft heights...
|
||||||
let blurred_heights: Vec<Vec<f64>> = apply_gaussian_blur(&height_grid, sigma);
|
let blurred_heights: Vec<Vec<f64>> = apply_gaussian_blur(&height_grid, sigma);
|
||||||
|
|
||||||
// Release raw height grid
|
let mut mc_heights: Vec<Vec<i32>> = Vec::with_capacity(blurred_heights.len());
|
||||||
drop(height_grid);
|
|
||||||
|
|
||||||
// Find min/max in raw data using parallel reduction
|
// Find min/max in raw data
|
||||||
let (min_height, max_height, extreme_low_count, extreme_high_count) = blurred_heights
|
let mut min_height: f64 = f64::MAX;
|
||||||
.par_iter()
|
let mut max_height: f64 = f64::MIN;
|
||||||
.map(|row| {
|
let mut extreme_low_count = 0;
|
||||||
let mut local_min = f64::MAX;
|
let mut extreme_high_count = 0;
|
||||||
let mut local_max = f64::MIN;
|
|
||||||
let mut local_low = 0usize;
|
for row in &blurred_heights {
|
||||||
let mut local_high = 0usize;
|
for &height in row {
|
||||||
for &height in row {
|
min_height = min_height.min(height);
|
||||||
local_min = local_min.min(height);
|
max_height = max_height.max(height);
|
||||||
local_max = local_max.max(height);
|
|
||||||
if height < -1000.0 {
|
// Count extreme values that might indicate data issues
|
||||||
local_low += 1;
|
if height < -1000.0 {
|
||||||
}
|
extreme_low_count += 1;
|
||||||
if height > 10000.0 {
|
|
||||||
local_high += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(local_min, local_max, local_low, local_high)
|
if height > 10000.0 {
|
||||||
})
|
extreme_high_count += 1;
|
||||||
.reduce(
|
}
|
||||||
|| (f64::MAX, f64::MIN, 0usize, 0usize),
|
}
|
||||||
|(min1, max1, low1, high1), (min2, max2, low2, high2)| {
|
}
|
||||||
(min1.min(min2), max1.max(max2), low1 + low2, high1 + high2)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
//eprintln!("Height data range: {min_height} to {max_height} m");
|
eprintln!("Height data range: {min_height} to {max_height} m");
|
||||||
if extreme_low_count > 0 {
|
if extreme_low_count > 0 {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"WARNING: Found {extreme_low_count} pixels with extremely low elevations (< -1000m)"
|
"WARNING: Found {extreme_low_count} pixels with extremely low elevations (< -1000m)"
|
||||||
@@ -477,63 +315,39 @@ pub fn fetch_elevation_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let height_range: f64 = max_height - min_height;
|
let height_range: f64 = max_height - min_height;
|
||||||
|
// Apply scale factor to height scaling
|
||||||
|
let mut height_scale: f64 = BASE_HEIGHT_SCALE * scale.sqrt(); // sqrt to make height scaling less extreme
|
||||||
|
let mut scaled_range: f64 = height_range * height_scale;
|
||||||
|
|
||||||
// Realistic height scaling: 1 meter of real elevation = scale blocks in Minecraft
|
// Adaptive scaling: ensure we don't exceed reasonable Y range
|
||||||
// At scale=1.0, 1 meter = 1 block (realistic 1:1 mapping)
|
let available_y_range = (MAX_Y - ground_level) as f64;
|
||||||
// At scale=2.0, 1 meter = 2 blocks (exaggerated for larger worlds)
|
let safety_margin = 0.9; // Use 90% of available range
|
||||||
let ideal_scaled_range: f64 = height_range * scale;
|
let max_allowed_range = available_y_range * safety_margin;
|
||||||
|
|
||||||
// Calculate available Y range in Minecraft (from ground_level to MAX_Y)
|
if scaled_range > max_allowed_range {
|
||||||
// Leave a buffer at the top for buildings, trees, and other structures
|
let adjustment_factor = max_allowed_range / scaled_range;
|
||||||
const TERRAIN_HEIGHT_BUFFER: i32 = 15;
|
height_scale *= adjustment_factor;
|
||||||
let available_y_range: f64 = (MAX_Y - TERRAIN_HEIGHT_BUFFER - ground_level) as f64;
|
scaled_range = height_range * height_scale;
|
||||||
|
|
||||||
// Determine final height scale:
|
|
||||||
// - Use realistic 1:1 (times scale) if terrain fits within Minecraft limits
|
|
||||||
// - Only compress if the terrain would exceed the build height
|
|
||||||
let scaled_range: f64 = if ideal_scaled_range <= available_y_range {
|
|
||||||
// Terrain fits! Use realistic scaling
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Realistic elevation: {:.1}m range fits in {} available blocks",
|
"Height range too large, applying scaling adjustment factor: {adjustment_factor:.3}"
|
||||||
height_range, available_y_range as i32
|
|
||||||
);
|
);
|
||||||
ideal_scaled_range
|
eprintln!("Adjusted scaled range: {scaled_range:.1} blocks");
|
||||||
} else {
|
}
|
||||||
// Terrain too tall, compress to fit within Minecraft limits
|
|
||||||
let compression_factor: f64 = available_y_range / height_range;
|
|
||||||
let compressed_range: f64 = height_range * compression_factor;
|
|
||||||
eprintln!(
|
|
||||||
"Elevation compressed: {:.1}m range -> {:.0} blocks ({:.2}:1 ratio, 1 block = {:.2}m)",
|
|
||||||
height_range,
|
|
||||||
compressed_range,
|
|
||||||
height_range / compressed_range,
|
|
||||||
compressed_range / height_range
|
|
||||||
);
|
|
||||||
compressed_range
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert to scaled Minecraft Y coordinates (parallelized across rows)
|
// Convert to scaled Minecraft Y coordinates
|
||||||
// Lowest real elevation maps to ground_level, highest maps to ground_level + scaled_range
|
for row in blurred_heights {
|
||||||
let mc_heights: Vec<Vec<i32>> = blurred_heights
|
let mc_row: Vec<i32> = row
|
||||||
.par_iter()
|
.iter()
|
||||||
.map(|row| {
|
.map(|&h| {
|
||||||
row.iter()
|
// Scale the height differences
|
||||||
.map(|&h| {
|
let relative_height: f64 = (h - min_height) / height_range;
|
||||||
// Calculate relative position within the elevation range (0.0 to 1.0)
|
let scaled_height: f64 = relative_height * scaled_range;
|
||||||
let relative_height: f64 = if height_range > 0.0 {
|
// With terrain enabled, ground_level is used as the MIN_Y for terrain
|
||||||
(h - min_height) / height_range
|
((ground_level as f64 + scaled_height).round() as i32).clamp(ground_level, MAX_Y)
|
||||||
} else {
|
})
|
||||||
0.0
|
.collect();
|
||||||
};
|
mc_heights.push(mc_row);
|
||||||
// Scale to Minecraft blocks and add to ground level
|
}
|
||||||
let scaled_height: f64 = relative_height * scaled_range;
|
|
||||||
// Clamp to valid Minecraft Y range (leave buffer at top for structures)
|
|
||||||
((ground_level as f64 + scaled_height).round() as i32)
|
|
||||||
.clamp(ground_level, MAX_Y - TERRAIN_HEIGHT_BUFFER)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut min_block_height: i32 = i32::MAX;
|
let mut min_block_height: i32 = i32::MAX;
|
||||||
let mut max_block_height: i32 = i32::MIN;
|
let mut max_block_height: i32 = i32::MIN;
|
||||||
@@ -543,7 +357,7 @@ pub fn fetch_elevation_data(
|
|||||||
max_block_height = max_block_height.max(height);
|
max_block_height = max_block_height.max(height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
|
eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
|
||||||
|
|
||||||
Ok(ElevationData {
|
Ok(ElevationData {
|
||||||
heights: mc_heights,
|
heights: mc_heights,
|
||||||
@@ -570,61 +384,48 @@ fn apply_gaussian_blur(heights: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
|
|||||||
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
|
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
|
||||||
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
|
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
|
||||||
|
|
||||||
let height_len = heights.len();
|
// Apply blur
|
||||||
let width = heights[0].len();
|
let mut blurred: Vec<Vec<f64>> = heights.to_owned();
|
||||||
|
|
||||||
// Horizontal pass - parallelize across rows (each row is independent)
|
// Horizontal pass
|
||||||
let after_horizontal: Vec<Vec<f64>> = heights
|
for row in blurred.iter_mut() {
|
||||||
.par_iter()
|
let mut temp: Vec<f64> = row.clone();
|
||||||
.map(|row| {
|
for (i, val) in temp.iter_mut().enumerate() {
|
||||||
let mut temp: Vec<f64> = vec![0.0; row.len()];
|
let mut sum: f64 = 0.0;
|
||||||
for (i, val) in temp.iter_mut().enumerate() {
|
let mut weight_sum: f64 = 0.0;
|
||||||
let mut sum: f64 = 0.0;
|
for (j, k) in kernel.iter().enumerate() {
|
||||||
let mut weight_sum: f64 = 0.0;
|
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
|
||||||
for (j, k) in kernel.iter().enumerate() {
|
if idx >= 0 && idx < row.len() as i32 {
|
||||||
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
|
sum += row[idx as usize] * k;
|
||||||
if idx >= 0 && idx < row.len() as i32 {
|
weight_sum += k;
|
||||||
sum += row[idx as usize] * k;
|
|
||||||
weight_sum += k;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*val = sum / weight_sum;
|
|
||||||
}
|
}
|
||||||
temp
|
*val = sum / weight_sum;
|
||||||
})
|
}
|
||||||
.collect();
|
*row = temp;
|
||||||
|
}
|
||||||
|
|
||||||
// Vertical pass - parallelize across columns (each column is independent)
|
// Vertical pass
|
||||||
// Process each column in parallel and collect results as column vectors
|
let height: usize = blurred.len();
|
||||||
let blurred_columns: Vec<Vec<f64>> = (0..width)
|
let width: usize = blurred[0].len();
|
||||||
.into_par_iter()
|
for x in 0..width {
|
||||||
.map(|x| {
|
let temp: Vec<_> = blurred
|
||||||
// Extract column from after_horizontal
|
.iter()
|
||||||
let column: Vec<f64> = after_horizontal.iter().map(|row| row[x]).collect();
|
.take(height)
|
||||||
|
.map(|row: &Vec<f64>| row[x])
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Apply vertical blur to this column
|
for (y, row) in blurred.iter_mut().enumerate().take(height) {
|
||||||
let mut blurred_column: Vec<f64> = vec![0.0; height_len];
|
let mut sum: f64 = 0.0;
|
||||||
for (y, val) in blurred_column.iter_mut().enumerate() {
|
let mut weight_sum: f64 = 0.0;
|
||||||
let mut sum: f64 = 0.0;
|
for (j, k) in kernel.iter().enumerate() {
|
||||||
let mut weight_sum: f64 = 0.0;
|
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
|
||||||
for (j, k) in kernel.iter().enumerate() {
|
if idx >= 0 && idx < height as i32 {
|
||||||
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
|
sum += temp[idx as usize] * k;
|
||||||
if idx >= 0 && idx < height_len as i32 {
|
weight_sum += k;
|
||||||
sum += column[idx as usize] * k;
|
|
||||||
weight_sum += k;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*val = sum / weight_sum;
|
|
||||||
}
|
}
|
||||||
blurred_column
|
row[x] = sum / weight_sum;
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Transpose columns back to row-major format
|
|
||||||
let mut blurred: Vec<Vec<f64>> = vec![vec![0.0; width]; height_len];
|
|
||||||
for (x, column) in blurred_columns.into_iter().enumerate() {
|
|
||||||
for (y, val) in column.into_iter().enumerate() {
|
|
||||||
blurred[y][x] = val;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,24 +507,17 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort to find percentiles
|
||||||
|
all_heights.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
let len = all_heights.len();
|
let len = all_heights.len();
|
||||||
|
|
||||||
// Use 1st and 99th percentiles to define reasonable bounds
|
// Use 1st and 99th percentiles to define reasonable bounds
|
||||||
// Using quickselect (select_nth_unstable) instead of full sort: O(n) vs O(n log n)
|
|
||||||
let p1_idx = (len as f64 * 0.01) as usize;
|
let p1_idx = (len as f64 * 0.01) as usize;
|
||||||
let p99_idx = ((len as f64 * 0.99) as usize).min(len - 1);
|
let p99_idx = (len as f64 * 0.99) as usize;
|
||||||
|
let min_reasonable = all_heights[p1_idx];
|
||||||
|
let max_reasonable = all_heights[p99_idx];
|
||||||
|
|
||||||
// Find p1 (1st percentile) - all elements before p1_idx will be <= p1
|
eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
||||||
let (_, p1_val, _) =
|
|
||||||
all_heights.select_nth_unstable_by(p1_idx, |a, b| a.partial_cmp(b).unwrap());
|
|
||||||
let min_reasonable = *p1_val;
|
|
||||||
|
|
||||||
// Find p99 (99th percentile) - need to search in remaining slice or use separate call
|
|
||||||
let (_, p99_val, _) =
|
|
||||||
all_heights.select_nth_unstable_by(p99_idx, |a, b| a.partial_cmp(b).unwrap());
|
|
||||||
let max_reasonable = *p99_val;
|
|
||||||
|
|
||||||
//eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
|
||||||
|
|
||||||
let mut outliers_filtered = 0;
|
let mut outliers_filtered = 0;
|
||||||
|
|
||||||
@@ -738,7 +532,7 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if outliers_filtered > 0 {
|
if outliers_filtered > 0 {
|
||||||
//eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
|
eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
|
||||||
// Re-run the NaN filling to interpolate the filtered values
|
// Re-run the NaN filling to interpolate the filtered values
|
||||||
fill_nan_values(height_grid);
|
fill_nan_values(height_grid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,340 +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::coordinate_system::cartesian::XZBBox;
|
|
||||||
use crate::floodfill::flood_fill_area;
|
|
||||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedWay};
|
|
||||||
use fnv::FnvHashMap;
|
|
||||||
use rayon::prelude::*;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// A memory-efficient bitmap for storing building footprint coordinates.
|
|
||||||
///
|
|
||||||
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
|
|
||||||
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
|
|
||||||
///
|
|
||||||
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
|
|
||||||
pub struct BuildingFootprintBitmap {
|
|
||||||
/// The bitmap data, where each bit represents one (x, z) coordinate
|
|
||||||
bits: Vec<u8>,
|
|
||||||
/// Minimum x coordinate (offset for indexing)
|
|
||||||
min_x: i32,
|
|
||||||
/// Minimum z coordinate (offset for indexing)
|
|
||||||
min_z: i32,
|
|
||||||
/// Width of the world (max_x - min_x + 1)
|
|
||||||
width: usize,
|
|
||||||
/// Height of the world (max_z - min_z + 1)
|
|
||||||
height: usize,
|
|
||||||
/// Number of coordinates marked as building footprints
|
|
||||||
count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BuildingFootprintBitmap {
|
|
||||||
/// Creates a new empty bitmap covering the given world bounds.
|
|
||||||
pub fn new(xzbbox: &XZBBox) -> Self {
|
|
||||||
let min_x = xzbbox.min_x();
|
|
||||||
let min_z = xzbbox.min_z();
|
|
||||||
// Use i64 to avoid overflow when world spans more than i32::MAX in either dimension
|
|
||||||
let width = (i64::from(xzbbox.max_x()) - i64::from(min_x) + 1) as usize;
|
|
||||||
let height = (i64::from(xzbbox.max_z()) - i64::from(min_z) + 1) as usize;
|
|
||||||
|
|
||||||
// Calculate number of bytes needed (round up to nearest byte)
|
|
||||||
let total_bits = width
|
|
||||||
.checked_mul(height)
|
|
||||||
.expect("BuildingFootprintBitmap: world size too large (width * height overflowed)");
|
|
||||||
let num_bytes = total_bits.div_ceil(8);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
bits: vec![0u8; num_bytes],
|
|
||||||
min_x,
|
|
||||||
min_z,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
count: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts (x, z) coordinate to bit index, returning None if out of bounds.
|
|
||||||
#[inline]
|
|
||||||
fn coord_to_index(&self, x: i32, z: i32) -> Option<usize> {
|
|
||||||
// Use i64 arithmetic to avoid overflow when coordinates span large ranges
|
|
||||||
let local_x = i64::from(x) - i64::from(self.min_x);
|
|
||||||
let local_z = i64::from(z) - i64::from(self.min_z);
|
|
||||||
|
|
||||||
if local_x < 0 || local_z < 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let local_x = local_x as usize;
|
|
||||||
let local_z = local_z as usize;
|
|
||||||
|
|
||||||
if local_x >= self.width || local_z >= self.height {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safe: bounds checks above ensure this won't overflow (max = total_bits - 1)
|
|
||||||
Some(local_z * self.width + local_x)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a coordinate as part of a building footprint.
|
|
||||||
#[inline]
|
|
||||||
pub fn set(&mut self, x: i32, z: i32) {
|
|
||||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
|
||||||
let byte_index = bit_index / 8;
|
|
||||||
let bit_offset = bit_index % 8;
|
|
||||||
|
|
||||||
// Safety: coord_to_index already validates bounds, so byte_index is always valid
|
|
||||||
let mask = 1u8 << bit_offset;
|
|
||||||
// Only increment count if bit wasn't already set
|
|
||||||
if self.bits[byte_index] & mask == 0 {
|
|
||||||
self.bits[byte_index] |= mask;
|
|
||||||
self.count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if a coordinate is part of a building footprint.
|
|
||||||
#[inline]
|
|
||||||
pub fn contains(&self, x: i32, z: i32) -> bool {
|
|
||||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
|
||||||
let byte_index = bit_index / 8;
|
|
||||||
let bit_offset = bit_index % 8;
|
|
||||||
|
|
||||||
// Safety: coord_to_index already validates bounds, so byte_index is always valid
|
|
||||||
return (self.bits[byte_index] >> bit_offset) & 1 == 1;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if no coordinates are marked.
|
|
||||||
#[must_use]
|
|
||||||
#[allow(dead_code)] // Standard API method for collection-like types
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.count == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A cache of pre-computed flood fill results, keyed by element ID.
|
|
||||||
pub struct FloodFillCache {
|
|
||||||
/// Cached results: element_id -> filled coordinates
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collects all building footprint coordinates from the pre-computed cache.
|
|
||||||
///
|
|
||||||
/// This should be called after precompute() and before elements are processed.
|
|
||||||
/// Returns a memory-efficient bitmap of all (x, z) coordinates that are part of buildings.
|
|
||||||
///
|
|
||||||
/// The bitmap uses only 1 bit per coordinate in the world bounds, compared to ~24 bytes
|
|
||||||
/// per entry in a HashSet, reducing memory usage by ~200x for large worlds.
|
|
||||||
pub fn collect_building_footprints(
|
|
||||||
&self,
|
|
||||||
elements: &[ProcessedElement],
|
|
||||||
xzbbox: &XZBBox,
|
|
||||||
) -> BuildingFootprintBitmap {
|
|
||||||
let mut footprints = BuildingFootprintBitmap::new(xzbbox);
|
|
||||||
|
|
||||||
for element in elements {
|
|
||||||
match element {
|
|
||||||
ProcessedElement::Way(way) => {
|
|
||||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
|
||||||
if let Some(cached) = self.way_cache.get(&way.id) {
|
|
||||||
for &(x, z) in cached {
|
|
||||||
footprints.set(x, z);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProcessedElement::Relation(rel) => {
|
|
||||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
|
||||||
for member in &rel.members {
|
|
||||||
// Only treat outer members as building footprints.
|
|
||||||
// Inner members represent courtyards/holes where trees can spawn.
|
|
||||||
if member.role == ProcessedMemberRole::Outer {
|
|
||||||
if let Some(cached) = self.way_cache.get(&member.way.id) {
|
|
||||||
for &(x, z) in cached {
|
|
||||||
footprints.set(x, z);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footprints
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes a way's cached flood fill result, freeing memory.
|
|
||||||
///
|
|
||||||
/// Call this after processing an element to release its cached data.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
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.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@ use crate::args::Args;
|
|||||||
use crate::coordinate_system::{cartesian::XZPoint, geographic::LLBBox};
|
use crate::coordinate_system::{cartesian::XZPoint, geographic::LLBBox};
|
||||||
use crate::elevation_data::{fetch_elevation_data, ElevationData};
|
use crate::elevation_data::{fetch_elevation_data, ElevationData};
|
||||||
use crate::progress::emit_gui_progress_update;
|
use crate::progress::emit_gui_progress_update;
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
use crate::telemetry::{send_log, LogLevel};
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use image::{Rgb, RgbImage};
|
use image::{Rgb, RgbImage};
|
||||||
|
|
||||||
@@ -33,11 +31,7 @@ impl Ground {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to fetch elevation data: {}", e);
|
eprintln!("Failed to fetch elevation data: {}", e);
|
||||||
#[cfg(feature = "gui")]
|
emit_gui_progress_update(15.0, "Elevation unavailable, using flat ground");
|
||||||
send_log(
|
|
||||||
LogLevel::Warning,
|
|
||||||
"Elevation unavailable, using flat ground",
|
|
||||||
);
|
|
||||||
// Graceful fallback: disable elevation and keep provided ground_level
|
// Graceful fallback: disable elevation and keep provided ground_level
|
||||||
Self {
|
Self {
|
||||||
elevation_enabled: false,
|
elevation_enabled: false,
|
||||||
@@ -147,7 +141,7 @@ impl Ground {
|
|||||||
pub fn generate_ground_data(args: &Args) -> Ground {
|
pub fn generate_ground_data(args: &Args) -> Ground {
|
||||||
if args.terrain {
|
if args.terrain {
|
||||||
println!("{} Fetching elevation...", "[3/7]".bold());
|
println!("{} Fetching elevation...", "[3/7]".bold());
|
||||||
emit_gui_progress_update(14.0, "Fetching elevation...");
|
emit_gui_progress_update(15.0, "Fetching elevation...");
|
||||||
let ground = Ground::new_enabled(&args.bbox, args.scale, args.ground_level);
|
let ground = Ground::new_enabled(&args.bbox, args.scale, args.ground_level);
|
||||||
if args.debug {
|
if args.debug {
|
||||||
ground.save_debug_image("elevation_debug");
|
ground.save_debug_image("elevation_debug");
|
||||||
|
|||||||
429
src/gui.rs
429
src/gui.rs
@@ -1,18 +1,15 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
use crate::coordinate_system::cartesian::XZPoint;
|
||||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||||
use crate::coordinate_system::transformation::CoordTransformer;
|
use crate::coordinate_system::transformation::CoordTransformer;
|
||||||
use crate::data_processing::{self, GenerationOptions};
|
use crate::data_processing;
|
||||||
use crate::ground::{self, Ground};
|
use crate::ground::{self, Ground};
|
||||||
use crate::map_transformation;
|
use crate::map_transformation;
|
||||||
use crate::osm_parser;
|
use crate::osm_parser;
|
||||||
use crate::parallel_processing::ParallelConfig;
|
use crate::progress;
|
||||||
use crate::progress::{self, emit_gui_progress_update};
|
|
||||||
use crate::retrieve_data;
|
use crate::retrieve_data;
|
||||||
use crate::telemetry::{self, send_log, LogLevel};
|
use crate::telemetry::{self, send_log, LogLevel};
|
||||||
use crate::version_check;
|
use crate::version_check;
|
||||||
use crate::world_editor::WorldFormat;
|
|
||||||
use colored::Colorize;
|
|
||||||
use fastnbt::Value;
|
use fastnbt::Value;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use fs2::FileExt;
|
use fs2::FileExt;
|
||||||
@@ -63,31 +60,7 @@ impl Drop for SessionLock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Desktop directory for Bedrock .mcworld file output.
|
|
||||||
fn get_bedrock_output_directory() -> PathBuf {
|
|
||||||
dirs::desktop_dir()
|
|
||||||
.or_else(dirs::home_dir)
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the area name for a given bounding box using the center point
|
|
||||||
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
|
||||||
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
|
||||||
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() {
|
pub fn run_gui() {
|
||||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
|
||||||
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
|
|
||||||
|
|
||||||
// Clean up old cached elevation tiles on startup
|
|
||||||
crate::elevation_data::cleanup_old_cached_tiles();
|
|
||||||
|
|
||||||
// Launch the UI
|
// Launch the UI
|
||||||
println!("Launching UI...");
|
println!("Launching UI...");
|
||||||
|
|
||||||
@@ -113,7 +86,7 @@ pub fn run_gui() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(
|
.plugin(
|
||||||
LogBuilder::default()
|
LogBuilder::default()
|
||||||
.level(LevelFilter::Info)
|
.level(LevelFilter::Warn)
|
||||||
.targets([
|
.targets([
|
||||||
Target::new(TargetKind::LogDir {
|
Target::new(TargetKind::LogDir {
|
||||||
file_name: Some("arnis".into()),
|
file_name: Some("arnis".into()),
|
||||||
@@ -128,8 +101,7 @@ pub fn run_gui() {
|
|||||||
gui_start_generation,
|
gui_start_generation,
|
||||||
gui_get_version,
|
gui_get_version,
|
||||||
gui_check_for_updates,
|
gui_check_for_updates,
|
||||||
gui_get_world_map_data,
|
gui_get_world_map_data
|
||||||
gui_show_in_folder
|
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_handle = app.handle();
|
let app_handle = app.handle();
|
||||||
@@ -169,20 +141,16 @@ fn gui_select_world(generate_new: bool) -> Result<String, i32> {
|
|||||||
|
|
||||||
if generate_new {
|
if generate_new {
|
||||||
// Handle new world generation
|
// Handle new world generation
|
||||||
// Try Minecraft saves directory first, fall back to current directory
|
if let Some(default_path) = &default_dir {
|
||||||
let target_path = if let Some(default_path) = &default_dir {
|
|
||||||
if default_path.exists() {
|
if default_path.exists() {
|
||||||
default_path.clone()
|
// Call create_new_world and return the result
|
||||||
|
create_new_world(default_path).map_err(|_| 1) // Error code 1: Minecraft directory not found
|
||||||
} else {
|
} else {
|
||||||
// Minecraft directory doesn't exist, use current directory
|
Err(1) // Error code 1: Minecraft directory not found
|
||||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No default directory configured, use current directory
|
Err(1) // Error code 1: Minecraft directory not found
|
||||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
}
|
||||||
};
|
|
||||||
|
|
||||||
create_new_world(&target_path).map_err(|_| 3) // Error code 3: Failed to create new world
|
|
||||||
} else {
|
} else {
|
||||||
// Handle existing world selection
|
// Handle existing world selection
|
||||||
// Open the directory picker dialog
|
// Open the directory picker dialog
|
||||||
@@ -430,7 +398,6 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
|||||||
if let Ok(compressed_data) = encoder.finish() {
|
if let Ok(compressed_data) = encoder.finish() {
|
||||||
if let Err(e) = std::fs::write(&level_path, compressed_data) {
|
if let Err(e) = std::fs::write(&level_path, compressed_data) {
|
||||||
eprintln!("Failed to update level.dat with area name: {e}");
|
eprintln!("Failed to update level.dat with area name: {e}");
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
send_log(
|
send_log(
|
||||||
LogLevel::Warning,
|
LogLevel::Warning,
|
||||||
"Failed to update level.dat with area name",
|
"Failed to update level.dat with area name",
|
||||||
@@ -449,20 +416,36 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
|||||||
world_path
|
world_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the default spawn point at X=1, Z=1 relative to the world origin.
|
// Function to update player position in level.dat based on spawn point coordinates
|
||||||
/// This is used when no spawn point is explicitly selected by the user.
|
fn update_player_position(
|
||||||
fn calculate_default_spawn(xzbbox: &XZBBox) -> (i32, i32) {
|
|
||||||
(xzbbox.min_x() + 1, xzbbox.min_z() + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the player spawn point in level.dat using Minecraft XZ coordinates.
|
|
||||||
/// The Y coordinate is set to a temporary value (150) and will be updated
|
|
||||||
/// after terrain generation by `update_player_spawn_y_after_generation`.
|
|
||||||
fn set_player_spawn_in_level_dat(
|
|
||||||
world_path: &str,
|
world_path: &str,
|
||||||
spawn_x: i32,
|
spawn_point: Option<(f64, f64)>,
|
||||||
spawn_z: i32,
|
bbox_text: String,
|
||||||
|
scale: f64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
use crate::coordinate_system::transformation::CoordTransformer;
|
||||||
|
|
||||||
|
let Some((lat, lng)) = spawn_point else {
|
||||||
|
return Ok(()); // No spawn point selected, exit early
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse geometrical point and bounding box
|
||||||
|
let llpoint =
|
||||||
|
LLPoint::new(lat, lng).map_err(|e| format!("Failed to parse spawn point:\n{e}"))?;
|
||||||
|
let llbbox = LLBBox::from_str(&bbox_text)
|
||||||
|
.map_err(|e| format!("Failed to parse bounding box for spawn point:\n{e}"))?;
|
||||||
|
|
||||||
|
// Check if spawn point is within the bbox
|
||||||
|
if !llbbox.contains(&llpoint) {
|
||||||
|
return Err("Spawn point is outside the selected area".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert lat/lng to Minecraft coordinates
|
||||||
|
let (transformer, _) = CoordTransformer::llbbox_to_xzbbox(&llbbox, scale)
|
||||||
|
.map_err(|e| format!("Failed to build transformation on coordinate systems:\n{e}"))?;
|
||||||
|
|
||||||
|
let xzpoint = transformer.transform_point(llpoint);
|
||||||
|
|
||||||
// Default y spawn position since terrain elevation cannot be determined yet
|
// Default y spawn position since terrain elevation cannot be determined yet
|
||||||
let y = 150.0;
|
let y = 150.0;
|
||||||
|
|
||||||
@@ -494,24 +477,21 @@ fn set_player_spawn_in_level_dat(
|
|||||||
if let Value::Compound(ref mut root) = nbt_data {
|
if let Value::Compound(ref mut root) = nbt_data {
|
||||||
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
||||||
// Set world spawn point
|
// Set world spawn point
|
||||||
data.insert("SpawnX".to_string(), Value::Int(spawn_x));
|
data.insert("SpawnX".to_string(), Value::Int(xzpoint.x));
|
||||||
data.insert("SpawnY".to_string(), Value::Int(y as i32));
|
data.insert("SpawnY".to_string(), Value::Int(y as i32));
|
||||||
data.insert("SpawnZ".to_string(), Value::Int(spawn_z));
|
data.insert("SpawnZ".to_string(), Value::Int(xzpoint.z));
|
||||||
|
|
||||||
// Update player position if Player compound exists
|
// Update player position
|
||||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||||
// Safely update position values with bounds checking
|
if let Value::Double(ref mut pos_x) = pos.get_mut(0).unwrap() {
|
||||||
if pos.len() >= 3 {
|
*pos_x = xzpoint.x as f64;
|
||||||
if let Some(Value::Double(ref mut pos_x)) = pos.get_mut(0) {
|
}
|
||||||
*pos_x = spawn_x as f64;
|
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
||||||
}
|
*pos_y = y;
|
||||||
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
}
|
||||||
*pos_y = y;
|
if let Value::Double(ref mut pos_z) = pos.get_mut(2).unwrap() {
|
||||||
}
|
*pos_z = xzpoint.z as f64;
|
||||||
if let Some(Value::Double(ref mut pos_z)) = pos.get_mut(2) {
|
|
||||||
*pos_z = spawn_z as f64;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,15 +523,19 @@ fn set_player_spawn_in_level_dat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to update player spawn Y coordinate based on terrain height after generation
|
// Function to update player spawn Y coordinate based on terrain height after generation
|
||||||
// This updates the spawn Y coordinate to be at terrain height + 3 blocks
|
|
||||||
pub fn update_player_spawn_y_after_generation(
|
pub fn update_player_spawn_y_after_generation(
|
||||||
world_path: &Path,
|
world_path: &Path,
|
||||||
|
spawn_point: Option<(f64, f64)>,
|
||||||
bbox_text: String,
|
bbox_text: String,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
ground: &Ground,
|
ground: &Ground,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
use crate::coordinate_system::transformation::CoordTransformer;
|
use crate::coordinate_system::transformation::CoordTransformer;
|
||||||
|
|
||||||
|
let Some((_lat, _lng)) = spawn_point else {
|
||||||
|
return Ok(()); // No spawn point selected, exit early
|
||||||
|
};
|
||||||
|
|
||||||
// Read the current level.dat file to get existing spawn coordinates
|
// Read the current level.dat file to get existing spawn coordinates
|
||||||
let level_path = PathBuf::from(world_path).join("level.dat");
|
let level_path = PathBuf::from(world_path).join("level.dat");
|
||||||
if !level_path.exists() {
|
if !level_path.exists() {
|
||||||
@@ -620,7 +604,7 @@ pub fn update_player_spawn_y_after_generation(
|
|||||||
let relative_z = existing_spawn_z - xzbbox.min_z();
|
let relative_z = existing_spawn_z - xzbbox.min_z();
|
||||||
let terrain_point = XZPoint::new(relative_x, relative_z);
|
let terrain_point = XZPoint::new(relative_x, relative_z);
|
||||||
|
|
||||||
ground.level(terrain_point) + 3 // Add 3 blocks above terrain for safety
|
ground.level(terrain_point) + 2
|
||||||
} else {
|
} else {
|
||||||
-61 // Default Y if no terrain
|
-61 // Default Y if no terrain
|
||||||
};
|
};
|
||||||
@@ -634,8 +618,8 @@ pub fn update_player_spawn_y_after_generation(
|
|||||||
// Update player position - only Y coordinate
|
// Update player position - only Y coordinate
|
||||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||||
// Safely update Y position with bounds checking
|
// Keep existing X and Z, only update Y
|
||||||
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
||||||
*pos_y = spawn_y as f64;
|
*pos_y = spawn_y as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -737,57 +721,6 @@ struct WorldMapData {
|
|||||||
max_lon: f64,
|
max_lon: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
|
||||||
#[tauri::command]
|
|
||||||
fn gui_show_in_folder(path: String) -> Result<(), String> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
// On Windows, try to open with default application (Minecraft Bedrock)
|
|
||||||
// If that fails, show in Explorer
|
|
||||||
if std::process::Command::new("cmd")
|
|
||||||
.args(["/C", "start", "", &path])
|
|
||||||
.spawn()
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
std::process::Command::new("explorer")
|
|
||||||
.args(["/select,", &path])
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to open explorer: {}", e))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
// On macOS, just reveal in Finder
|
|
||||||
std::process::Command::new("open")
|
|
||||||
.args(["-R", &path])
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to open Finder: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
// On Linux, just show in file manager
|
|
||||||
let path_parent = std::path::Path::new(&path)
|
|
||||||
.parent()
|
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
|
||||||
.unwrap_or_else(|| path.clone());
|
|
||||||
|
|
||||||
// Try nautilus with select first, then fall back to xdg-open on parent
|
|
||||||
if std::process::Command::new("nautilus")
|
|
||||||
.args(["--select", &path])
|
|
||||||
.spawn()
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
let _ = std::process::Command::new("xdg-open")
|
|
||||||
.arg(&path_parent)
|
|
||||||
.spawn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@@ -796,6 +729,7 @@ fn gui_start_generation(
|
|||||||
selected_world: String,
|
selected_world: String,
|
||||||
world_scale: f64,
|
world_scale: f64,
|
||||||
ground_level: i32,
|
ground_level: i32,
|
||||||
|
floodfill_timeout: u64,
|
||||||
terrain_enabled: bool,
|
terrain_enabled: bool,
|
||||||
skip_osm_objects: bool,
|
skip_osm_objects: bool,
|
||||||
interior_enabled: bool,
|
interior_enabled: bool,
|
||||||
@@ -804,7 +738,6 @@ fn gui_start_generation(
|
|||||||
is_new_world: bool,
|
is_new_world: bool,
|
||||||
spawn_point: Option<(f64, f64)>,
|
spawn_point: Option<(f64, f64)>,
|
||||||
telemetry_consent: bool,
|
telemetry_consent: bool,
|
||||||
world_format: String,
|
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
use progress::emit_gui_error;
|
use progress::emit_gui_error;
|
||||||
use LLBBox;
|
use LLBBox;
|
||||||
@@ -815,99 +748,48 @@ fn gui_start_generation(
|
|||||||
// Send generation click telemetry
|
// Send generation click telemetry
|
||||||
telemetry::send_generation_click();
|
telemetry::send_generation_click();
|
||||||
|
|
||||||
// For new Java worlds, set the spawn point in level.dat
|
// If spawn point was chosen and the world is new, check and set the spawn point
|
||||||
// Only update player position for Java worlds - Bedrock worlds don't have a pre-existing
|
if is_new_world && spawn_point.is_some() {
|
||||||
// level.dat to modify (the spawn point will be set when the .mcworld is created)
|
// Verify the spawn point is within bounds
|
||||||
if is_new_world && world_format != "bedrock" {
|
if let Some(coords) = spawn_point {
|
||||||
let llbbox = match LLBBox::from_str(&bbox_text) {
|
let llbbox = match LLBBox::from_str(&bbox_text) {
|
||||||
Ok(bbox) => bbox,
|
Ok(bbox) => bbox,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = format!("Failed to parse bounding box: {e}");
|
let error_msg = format!("Failed to parse bounding box: {e}");
|
||||||
eprintln!("{error_msg}");
|
|
||||||
emit_gui_error(&error_msg);
|
|
||||||
return Err(error_msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (transformer, xzbbox) = match CoordTransformer::llbbox_to_xzbbox(&llbbox, world_scale) {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(e) => {
|
|
||||||
let error_msg = format!("Failed to create coordinate transformer: {e}");
|
|
||||||
eprintln!("{error_msg}");
|
|
||||||
emit_gui_error(&error_msg);
|
|
||||||
return Err(error_msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (spawn_x, spawn_z) = if let Some(coords) = spawn_point {
|
|
||||||
// User selected a spawn point - verify it's within bounds and convert to XZ
|
|
||||||
let llpoint = LLPoint::new(coords.0, coords.1)
|
|
||||||
.map_err(|e| format!("Failed to parse spawn point: {e}"))?;
|
|
||||||
|
|
||||||
if llbbox.contains(&llpoint) {
|
|
||||||
let xzpoint = transformer.transform_point(llpoint);
|
|
||||||
(xzpoint.x, xzpoint.z)
|
|
||||||
} else {
|
|
||||||
// Spawn point outside bounds, use default
|
|
||||||
calculate_default_spawn(&xzbbox)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No user-selected spawn point - use default at X=1, Z=1 relative to world origin
|
|
||||||
calculate_default_spawn(&xzbbox)
|
|
||||||
};
|
|
||||||
|
|
||||||
set_player_spawn_in_level_dat(&selected_world, spawn_x, spawn_z)
|
|
||||||
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
if let Err(e) = tokio::task::spawn_blocking(move || {
|
|
||||||
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();
|
|
||||||
eprintln!("{error_msg}");
|
eprintln!("{error_msg}");
|
||||||
emit_gui_error(&error_msg);
|
emit_gui_error(&error_msg);
|
||||||
return Err(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
|
let llpoint = LLPoint::new(coords.0, coords.1)
|
||||||
// Session lock prevents Minecraft from having the world open during generation
|
.map_err(|e| format!("Failed to parse spawn point: {e}"))?;
|
||||||
// Bedrock worlds are generated as .mcworld files and don't need this lock
|
|
||||||
let _session_lock: Option<SessionLock> = if world_format == WorldFormat::JavaAnvil {
|
if llbbox.contains(&llpoint) {
|
||||||
match SessionLock::acquire(&world_path) {
|
// Spawn point is valid, update the player position
|
||||||
Ok(lock) => Some(lock),
|
update_player_position(
|
||||||
Err(e) => {
|
&selected_world,
|
||||||
let error_msg = format!("Failed to acquire session lock: {e}");
|
spawn_point,
|
||||||
eprintln!("{error_msg}");
|
bbox_text.clone(),
|
||||||
emit_gui_error(&error_msg);
|
world_scale,
|
||||||
return Err(error_msg);
|
)
|
||||||
}
|
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the bounding box from the text with proper error handling
|
// Parse the bounding box from the text with proper error handling
|
||||||
@@ -921,71 +803,19 @@ fn gui_start_generation(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine output path and level name based on format
|
// Add localized name to the world if user generated a new world
|
||||||
let (generation_path, level_name) = match world_format {
|
let updated_world_path = if is_new_world {
|
||||||
WorldFormat::JavaAnvil => {
|
add_localized_world_name(world_path, &bbox)
|
||||||
// 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 on Desktop with location-based name
|
|
||||||
let area_name = get_area_name_for_bedrock(&bbox);
|
|
||||||
let filename = format!("Arnis {}.mcworld", area_name);
|
|
||||||
let lvl_name = format!("Arnis World: {}", area_name);
|
|
||||||
|
|
||||||
let output_path = get_bedrock_output_directory().join(&filename);
|
|
||||||
(output_path, Some(lvl_name))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate MC spawn coordinates from lat/lng if spawn point was provided
|
|
||||||
// Otherwise, default to X=1, Z=1 (relative to xzbbox min coordinates)
|
|
||||||
let mc_spawn_point: Option<(i32, i32)> = if let Some((lat, lng)) = spawn_point {
|
|
||||||
if let Ok(llpoint) = LLPoint::new(lat, lng) {
|
|
||||||
if let Ok((transformer, _)) =
|
|
||||||
CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale)
|
|
||||||
{
|
|
||||||
let xzpoint = transformer.transform_point(llpoint);
|
|
||||||
Some((xzpoint.x, xzpoint.z))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Default spawn point: X=1, Z=1 relative to world origin
|
world_path
|
||||||
if let Ok((_, xzbbox)) = CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale) {
|
|
||||||
Some(calculate_default_spawn(&xzbbox))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create generation options
|
// Create an Args instance with the chosen bounding box and world directory path
|
||||||
let generation_options = GenerationOptions {
|
|
||||||
path: generation_path.clone(),
|
|
||||||
format: world_format,
|
|
||||||
level_name,
|
|
||||||
spawn_point: mc_spawn_point,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create an Args instance with the chosen bounding box
|
|
||||||
// Note: path is used for Java-specific features like spawn point update
|
|
||||||
let args: Args = Args {
|
let args: Args = Args {
|
||||||
bbox,
|
bbox,
|
||||||
file: None,
|
file: None,
|
||||||
save_json_file: None,
|
save_json_file: None,
|
||||||
path: if world_format == WorldFormat::JavaAnvil {
|
path: updated_world_path,
|
||||||
generation_path
|
|
||||||
} else {
|
|
||||||
world_path
|
|
||||||
},
|
|
||||||
downloader: "requests".to_string(),
|
downloader: "requests".to_string(),
|
||||||
scale: world_scale,
|
scale: world_scale,
|
||||||
ground_level,
|
ground_level,
|
||||||
@@ -994,11 +824,9 @@ fn gui_start_generation(
|
|||||||
roof: roof_enabled,
|
roof: roof_enabled,
|
||||||
fillground: fillground_enabled,
|
fillground: fillground_enabled,
|
||||||
debug: false,
|
debug: false,
|
||||||
timeout: Some(std::time::Duration::from_secs(40)),
|
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
|
||||||
threads: 0, // Auto-detect thread count
|
generate_map: true,
|
||||||
region_batch_size: 2, // Four regions per unit (default)
|
spawn_point,
|
||||||
no_parallel: true, // Use sequential processing (parallel has bugs)
|
|
||||||
force_parallel: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
||||||
@@ -1012,30 +840,14 @@ fn gui_start_generation(
|
|||||||
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
|
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
|
||||||
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
|
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
|
||||||
|
|
||||||
let _ = data_processing::generate_world_with_options(
|
let _ = data_processing::generate_world(
|
||||||
parsed_elements,
|
parsed_elements,
|
||||||
xzbbox.clone(),
|
xzbbox,
|
||||||
args.bbox,
|
args.bbox,
|
||||||
ground,
|
ground,
|
||||||
&args,
|
&args,
|
||||||
generation_options.clone(),
|
|
||||||
ParallelConfig::default(), // Use parallel processing
|
|
||||||
);
|
);
|
||||||
// Explicitly release session lock before showing Done message
|
// Session lock will be automatically released when _session_lock goes out of scope
|
||||||
// so Minecraft can open the world immediately
|
|
||||||
drop(_session_lock);
|
|
||||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
|
||||||
println!("{}", "Done! World generation completed.".green().bold());
|
|
||||||
|
|
||||||
// Start map preview generation silently in background (Java only)
|
|
||||||
if world_format == WorldFormat::JavaAnvil {
|
|
||||||
let preview_info = data_processing::MapPreviewInfo::new(
|
|
||||||
generation_options.path.clone(),
|
|
||||||
&xzbbox,
|
|
||||||
);
|
|
||||||
data_processing::start_map_preview_generation(preview_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,36 +878,21 @@ fn gui_start_generation(
|
|||||||
&mut ground,
|
&mut ground,
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = data_processing::generate_world_with_options(
|
let _ = data_processing::generate_world(
|
||||||
parsed_elements,
|
parsed_elements,
|
||||||
xzbbox.clone(),
|
xzbbox,
|
||||||
args.bbox,
|
args.bbox,
|
||||||
ground,
|
ground,
|
||||||
&args,
|
&args,
|
||||||
generation_options.clone(),
|
|
||||||
ParallelConfig::default(), // Use parallel processing
|
|
||||||
);
|
);
|
||||||
// Explicitly release session lock before showing Done message
|
// Session lock will be automatically released when _session_lock goes out of scope
|
||||||
// so Minecraft can open the world immediately
|
|
||||||
drop(_session_lock);
|
|
||||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
|
||||||
println!("{}", "Done! World generation completed.".green().bold());
|
|
||||||
|
|
||||||
// Start map preview generation silently in background (Java only)
|
|
||||||
if world_format == WorldFormat::JavaAnvil {
|
|
||||||
let preview_info = data_processing::MapPreviewInfo::new(
|
|
||||||
generation_options.path.clone(),
|
|
||||||
&xzbbox,
|
|
||||||
);
|
|
||||||
data_processing::start_map_preview_generation(preview_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
emit_gui_error(&e.to_string());
|
let error_msg = format!("Failed to fetch data: {e}");
|
||||||
|
emit_gui_error(&error_msg);
|
||||||
// Session lock will be automatically released when _session_lock goes out of scope
|
// Session lock will be automatically released when _session_lock goes out of scope
|
||||||
Err(e.to_string())
|
Err(error_msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
11
src/gui/css/bbox.css
vendored
11
src/gui/css/bbox.css
vendored
@@ -8,9 +8,13 @@ body,
|
|||||||
font-family: "Courier New", Courier, monospace;
|
font-family: "Courier New", Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the BBOX coordinates display at bottom of map */
|
|
||||||
#info-box {
|
#info-box {
|
||||||
display: none;
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
bottom: 0;
|
||||||
|
border: 0 0 7px 0;
|
||||||
|
z-index: 10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#coord-format {
|
#coord-format {
|
||||||
@@ -347,8 +351,7 @@ body,
|
|||||||
background-position: -31px -2px;
|
background-position: -31px -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled,
|
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled {
|
||||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.editing-mode {
|
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
BIN
src/gui/css/maps/images/spritesheet-2x.png
vendored
BIN
src/gui/css/maps/images/spritesheet-2x.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.0 KiB |
5
src/gui/css/maps/leaflet.draw.css
vendored
5
src/gui/css/maps/leaflet.draw.css
vendored
@@ -158,13 +158,12 @@
|
|||||||
background-position: -182px -2px;
|
background-position: -182px -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disabled states reuse same sprites; opacity indicates disabled */
|
|
||||||
.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-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 {
|
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
|
||||||
background-position: -182px -2px;
|
background-position: -242px -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
|
|||||||
250
src/gui/css/styles.css
vendored
250
src/gui/css/styles.css
vendored
@@ -32,12 +32,9 @@ p {
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 6em;
|
height: 6em;
|
||||||
padding-top: 0.4em;
|
padding: 1.5em;
|
||||||
padding-bottom: 0.5em;
|
|
||||||
will-change: filter;
|
will-change: filter;
|
||||||
transition: 0.75s;
|
transition: 0.75s;
|
||||||
max-width: 950px;
|
|
||||||
max-height: 600px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo.arnis:hover {
|
.logo.arnis:hover {
|
||||||
@@ -62,11 +59,11 @@ a:hover {
|
|||||||
|
|
||||||
.flex-container {
|
.flex-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 20px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
min-height: 70vh;
|
min-height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
@@ -78,70 +75,34 @@ a:hover {
|
|||||||
|
|
||||||
.map-box,
|
.map-box,
|
||||||
.controls-box {
|
.controls-box {
|
||||||
|
width: 45%;
|
||||||
|
background: #575757;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-box {
|
.map-box {
|
||||||
width: 63%;
|
min-height: 400px;
|
||||||
min-height: 420px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #575757;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-box {
|
|
||||||
width: 32%;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-content {
|
.controls-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-box .progress-section {
|
.controls-box .progress-section {
|
||||||
margin-top: 0;
|
margin-top: auto;
|
||||||
}
|
|
||||||
|
|
||||||
.controls-top {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbox-selection-text {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #ffffff;
|
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
|
||||||
min-height: 2.5em;
|
|
||||||
line-height: 1.25em;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-info-text {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #ececec;
|
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
|
||||||
min-height: 1.5em;
|
|
||||||
line-height: 1.25em;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-container {
|
.map-container {
|
||||||
border: none;
|
border: 2px solid #e0e0e0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 100%;
|
min-height: 300px;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h2 {
|
.section h2 {
|
||||||
@@ -181,25 +142,18 @@ button:hover {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-row {
|
.progress-section h2 {
|
||||||
display: flex;
|
margin-bottom: 8px;
|
||||||
align-items: center;
|
text-align: center;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
margin-top: 8px;
|
||||||
|
|
||||||
#progress-detail {
|
|
||||||
min-width: 40px;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
@@ -209,6 +163,15 @@ button:hover {
|
|||||||
transition: width 0.4s;
|
transition: width 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Left and right alignment for "Saving world..." text */
|
||||||
|
.progress-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -227,7 +190,7 @@ button:hover {
|
|||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
color: #f6f6f6;
|
color: #f6f6f6;
|
||||||
background-color: #333333;
|
background-color: #2f2f2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@@ -259,73 +222,10 @@ button:hover {
|
|||||||
width: 100%;
|
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;
|
|
||||||
margin-top: 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 */
|
/* Customization Settings */
|
||||||
.modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 20001;
|
z-index: 1000;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -338,7 +238,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: #717171;
|
background-color: #797979;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #797979;
|
border: 1px solid #797979;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -458,20 +358,6 @@ button:hover {
|
|||||||
box-shadow: 0 0 5px #fecc44;
|
box-shadow: 0 0 5px #fecc44;
|
||||||
}
|
}
|
||||||
|
|
||||||
#save-path {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid #fecc44;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save-path:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #fecc44;
|
|
||||||
box-shadow: 0 0 5px #fecc44;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Settings Modal Layout */
|
/* Settings Modal Layout */
|
||||||
.settings-row {
|
.settings-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -483,75 +369,6 @@ button:hover {
|
|||||||
.settings-row label {
|
.settings-row label {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tooltip icon (question mark in circle) */
|
|
||||||
.tooltip-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(254, 204, 68, 0.3);
|
|
||||||
color: #fecc44;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: help;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-icon:hover {
|
|
||||||
background-color: rgba(254, 204, 68, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Arnis-styled tooltip box */
|
|
||||||
.tooltip-icon::after {
|
|
||||||
content: attr(data-tooltip);
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: calc(100% + 8px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
color: #fecc44;
|
|
||||||
padding: 6px 14px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
border: 1px solid #fecc44;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tooltip arrow */
|
|
||||||
.tooltip-icon::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: calc(100% + 2px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border: 6px solid transparent;
|
|
||||||
border-top-color: #fecc44;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
|
||||||
z-index: 1001;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-icon:hover::after,
|
|
||||||
.tooltip-icon:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-control {
|
.settings-control {
|
||||||
@@ -727,12 +544,9 @@ button:hover {
|
|||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button svg {
|
.settings-button .gear-icon::before {
|
||||||
stroke: white;
|
content: "⚙️";
|
||||||
width: 22px;
|
font-size: 18px;
|
||||||
height: 22px;
|
|
||||||
min-width: 22px;
|
|
||||||
min-height: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo Animation */
|
/* Logo Animation */
|
||||||
|
|||||||
140
src/gui/index.html
vendored
140
src/gui/index.html
vendored
@@ -20,52 +20,49 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<!-- Left Box: Map -->
|
<!-- Left Box: Map and BBox Input -->
|
||||||
<section class="section map-box">
|
<section class="section map-box" style="margin-bottom: 0; padding-bottom: 0;">
|
||||||
<iframe src="maps.html" width="100%" height="100%" class="map-container" title="Map Picker"></iframe>
|
<h2 data-localize="select_location">Select Location</h2>
|
||||||
|
<span id="bbox-text" style="font-size: 1.0em; display: block; margin-top: -8px; margin-bottom: 3px;" data-localize="zoom_in_and_choose">
|
||||||
|
Zoom in and choose your area using the rectangle tool
|
||||||
|
</span>
|
||||||
|
<iframe src="maps.html" width="100%" height="300" class="map-container" title="Map Picker"></iframe>
|
||||||
|
|
||||||
|
<span id="bbox-info"
|
||||||
|
style="font-size: 0.75em; color: #7bd864; display: block; margin-bottom: 4px; font-weight: bold; min-height: 2em;"></span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Right Box: Directory Selection, Start Button, and Progress Bar -->
|
<!-- Right Box: Directory Selection, Start Button, and Progress Bar -->
|
||||||
<section class="section controls-box">
|
<section class="section controls-box">
|
||||||
<div class="controls-content">
|
<div class="controls-content">
|
||||||
<div class="controls-top">
|
<h2 data-localize="select_world">Select World</h2>
|
||||||
<!-- World Selection Container -->
|
|
||||||
<div class="world-selection-container">
|
<!-- Updated Tooltip Structure -->
|
||||||
<div class="tooltip" style="width: 100%;">
|
<div class="tooltip" style="width: 100%;">
|
||||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
<button type="button" onclick="openWorldPicker()" style="padding: 10px; line-height: 1.2; width: 100%;">
|
||||||
<span id="choose_world">Choose World</span>
|
<span id="choose_world">Choose World</span>
|
||||||
<br>
|
<br>
|
||||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||||
No world selected
|
No world selected
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<!-- World Format Toggle -->
|
|
||||||
<div class="format-toggle-container">
|
|
||||||
<button type="button" id="format-java" class="format-toggle-btn format-active" onclick="setWorldFormat('java')">
|
|
||||||
Java
|
|
||||||
</button>
|
|
||||||
<button type="button" id="format-bedrock" class="format-toggle-btn" onclick="setWorldFormat('bedrock')">
|
|
||||||
Bedrock
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-container">
|
|
||||||
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
|
|
||||||
<button type="button" class="settings-button" onclick="openSettings()" aria-label="Settings">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span id="bbox-selection-info" class="bbox-selection-text" data-localize="select_area_prompt">Select an area on the map using the tools.</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
|
||||||
|
<button type="button" class="settings-button" onclick="openSettings()">
|
||||||
|
<i class="gear-icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
<div class="progress-section">
|
<div class="progress-section">
|
||||||
<span id="progress-info" class="progress-info-text"></span>
|
<h2 data-localize="progress">Progress</h2>
|
||||||
<div class="progress-row">
|
<div class="progress-bar-container">
|
||||||
<div class="progress-bar-container">
|
<div class="progress-bar" id="progress-bar"></div>
|
||||||
<div class="progress-bar" id="progress-bar"></div>
|
</div>
|
||||||
</div>
|
<div class="progress-status">
|
||||||
|
<span id="progress-message"></span>
|
||||||
<span id="progress-detail">0%</span>
|
<span id="progress-detail">0%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,10 +89,7 @@
|
|||||||
|
|
||||||
<!-- Generation Mode Dropdown -->
|
<!-- Generation Mode Dropdown -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="generation-mode-select">
|
<label for="generation-mode-select" data-localize="generation_mode">Generation Mode</label>
|
||||||
<span data-localize="generation_mode">Generation Mode</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Choose what to generate: buildings/roads with terrain, just objects, or terrain only">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<select id="generation-mode-select" name="generation-mode-select" class="generation-mode-dropdown">
|
<select id="generation-mode-select" name="generation-mode-select" class="generation-mode-dropdown">
|
||||||
<option value="geo-terrain" data-localize="mode_geo_terrain">Objects + Terrain</option>
|
<option value="geo-terrain" data-localize="mode_geo_terrain">Objects + Terrain</option>
|
||||||
@@ -105,34 +99,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Interior Toggle Button -->
|
||||||
|
<div class="settings-row">
|
||||||
|
<label for="interior-toggle" data-localize="interior">Interior Generation</label>
|
||||||
|
<div class="settings-control">
|
||||||
|
<input type="checkbox" id="interior-toggle" name="interior-toggle" checked>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Roof Toggle Button -->
|
<!-- Roof Toggle Button -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="roof-toggle">
|
<label for="roof-toggle" data-localize="roof">Roof Generation</label>
|
||||||
<span data-localize="roof">Roof Generation</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Generate roofs on buildings">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<input type="checkbox" id="roof-toggle" name="roof-toggle" checked>
|
<input type="checkbox" id="roof-toggle" name="roof-toggle" checked>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Interior Toggle Button -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="interior-toggle">
|
|
||||||
<span data-localize="interior">Interior Generation</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Generate interior details inside buildings">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="checkbox" id="interior-toggle" name="interior-toggle">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fill ground Toggle Button -->
|
<!-- Fill ground Toggle Button -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="fillground-toggle">
|
<label for="fillground-toggle" data-localize="fillground">Fill Ground</label>
|
||||||
<span data-localize="fillground">Fill Ground</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Fill the ground below the surface">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<input type="checkbox" id="fillground-toggle" name="fillground-toggle">
|
<input type="checkbox" id="fillground-toggle" name="fillground-toggle">
|
||||||
</div>
|
</div>
|
||||||
@@ -140,10 +125,7 @@
|
|||||||
|
|
||||||
<!-- World Scale Slider -->
|
<!-- World Scale Slider -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="scale-value-slider">
|
<label for="scale-value-slider" data-localize="world_scale">World Scale</label>
|
||||||
<span data-localize="world_scale">World Scale</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Scale factor for the generated world (1.0 = real-world scale)">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.30" max="2.5" step="0.1" value="1">
|
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.30" max="2.5" step="0.1" value="1">
|
||||||
<span id="slider-value">1.00</span>
|
<span id="slider-value">1.00</span>
|
||||||
@@ -152,21 +134,23 @@
|
|||||||
|
|
||||||
<!-- Bounding Box Input -->
|
<!-- Bounding Box Input -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="bbox-coords">
|
<label for="bbox-coords" data-localize="custom_bounding_box">Custom Bounding Box</label>
|
||||||
<span data-localize="custom_bounding_box">Custom Bounding Box</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Manually enter coordinates (lat,lng,lat,lng) or use map selection">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" placeholder="Format: lat,lng,lat,lng">
|
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" placeholder="Format: lat,lng,lat,lng">
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Map Theme Selector -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="tile-theme-select">
|
<label for="tile-theme-select" data-localize="map_theme">Map Theme</label>
|
||||||
<span data-localize="map_theme">Map Theme</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Visual style of the map picker">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<select id="tile-theme-select" name="tile-theme-select" class="theme-dropdown">
|
<select id="tile-theme-select" name="tile-theme-select" class="theme-dropdown">
|
||||||
<option value="osm">Standard</option>
|
<option value="osm">Standard</option>
|
||||||
@@ -180,10 +164,7 @@
|
|||||||
|
|
||||||
<!-- Language Selector -->
|
<!-- Language Selector -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="language-select">
|
<label for="language-select" data-localize="language">Language</label>
|
||||||
<span data-localize="language">Language</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Interface language">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<select id="language-select" name="language-select" class="language-dropdown">
|
<select id="language-select" name="language-select" class="language-dropdown">
|
||||||
<option value="en">English</option>
|
<option value="en">English</option>
|
||||||
@@ -207,10 +188,7 @@
|
|||||||
|
|
||||||
<!-- Telemetry Consent Toggle -->
|
<!-- Telemetry Consent Toggle -->
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="telemetry-toggle" style="white-space: nowrap;">
|
<label for="telemetry-toggle">Anonymous Crash Reports</label>
|
||||||
<span>Anonymous Crash Reports</span>
|
|
||||||
<span class="tooltip-icon" data-tooltip="Send anonymous crash data to help improve Arnis">?</span>
|
|
||||||
</label>
|
|
||||||
<div class="settings-control">
|
<div class="settings-control">
|
||||||
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
|
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
55
src/gui/js/bbox.js
vendored
55
src/gui/js/bbox.js
vendored
@@ -529,7 +529,7 @@ $(document).ready(function () {
|
|||||||
failureCount++;
|
failureCount++;
|
||||||
|
|
||||||
// After a few failures, try HTTP fallback
|
// 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);
|
console.log('HTTPS tile loading failed, attempting HTTP fallback for', themeKey);
|
||||||
this._httpFallbackAttempted = true;
|
this._httpFallbackAttempted = true;
|
||||||
|
|
||||||
@@ -564,7 +564,6 @@ $(document).ready(function () {
|
|||||||
var worldOverlayEnabled = false;
|
var worldOverlayEnabled = false;
|
||||||
var worldPreviewAvailable = false;
|
var worldPreviewAvailable = false;
|
||||||
var sliderControl = null;
|
var sliderControl = null;
|
||||||
var worldOverlayHiddenForEdit = false; // Track if we hid the overlay for edit/delete mode
|
|
||||||
|
|
||||||
// Create the opacity slider as a proper Leaflet control
|
// Create the opacity slider as a proper Leaflet control
|
||||||
var SliderControl = L.Control.extend({
|
var SliderControl = L.Control.extend({
|
||||||
@@ -723,32 +722,6 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily hide the overlay (for edit/delete mode)
|
|
||||||
function hideWorldOverlayTemporarily() {
|
|
||||||
if (worldOverlay && worldOverlayEnabled) {
|
|
||||||
worldOverlayHiddenForEdit = true;
|
|
||||||
map.removeLayer(worldOverlay);
|
|
||||||
}
|
|
||||||
// Also visually disable the preview button during edit/delete mode
|
|
||||||
var btn = document.getElementById('world-preview-btn');
|
|
||||||
if (btn) {
|
|
||||||
btn.classList.add('editing-mode');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore the overlay after edit/delete mode ends
|
|
||||||
function restoreWorldOverlay() {
|
|
||||||
if (worldOverlayHiddenForEdit && worldOverlay && worldOverlayEnabled) {
|
|
||||||
worldOverlay.addTo(map);
|
|
||||||
worldOverlayHiddenForEdit = false;
|
|
||||||
}
|
|
||||||
// Re-enable the preview button
|
|
||||||
var btn = document.getElementById('world-preview-btn');
|
|
||||||
if (btn) {
|
|
||||||
btn.classList.remove('editing-mode');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for messages from parent window
|
// Listen for messages from parent window
|
||||||
window.addEventListener('message', function(event) {
|
window.addEventListener('message', function(event) {
|
||||||
if (event.data && event.data.type === 'changeTileTheme') {
|
if (event.data && event.data.type === 'changeTileTheme') {
|
||||||
@@ -926,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
|
// Check if it's a rectangle and set proper styles before adding it to the layer
|
||||||
if (e.layerType === 'rectangle') {
|
if (e.layerType === 'rectangle') {
|
||||||
e.layer.setStyle({
|
e.layer.setStyle({
|
||||||
@@ -1026,23 +990,6 @@ $(document).ready(function () {
|
|||||||
map.fitBounds(bounds.getBounds());
|
map.fitBounds(bounds.getBounds());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hide world preview overlay when entering edit or delete mode
|
|
||||||
map.on('draw:editstart', function() {
|
|
||||||
hideWorldOverlayTemporarily();
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('draw:deletestart', function() {
|
|
||||||
hideWorldOverlayTemporarily();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restore world preview overlay when exiting edit or delete mode
|
|
||||||
map.on('draw:editstop', function() {
|
|
||||||
restoreWorldOverlay();
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on('draw:deletestop', function() {
|
|
||||||
restoreWorldOverlay();
|
|
||||||
});
|
|
||||||
function display() {
|
function display() {
|
||||||
$('#boxbounds').text(formatBounds(bounds.getBounds(), '4326'));
|
$('#boxbounds').text(formatBounds(bounds.getBounds(), '4326'));
|
||||||
$('#boxboundsmerc').text(formatBounds(bounds.getBounds(), currentproj));
|
$('#boxboundsmerc').text(formatBounds(bounds.getBounds(), currentproj));
|
||||||
|
|||||||
4
src/gui/js/license.js
vendored
4
src/gui/js/license.js
vendored
@@ -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.
|
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>
|
<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>
|
<p><b>Privacy Policy:</b></p>
|
||||||
If you consent to telemetry data collection, please review our Privacy Policy at:
|
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>.
|
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">https://arnismc.com/privacypolicy.html</a>.
|
||||||
|
|||||||
218
src/gui/js/main.js
vendored
218
src/gui/js/main.js
vendored
@@ -12,25 +12,6 @@ if (window.__TAURI__) {
|
|||||||
|
|
||||||
const DEFAULT_LOCALE_PATH = `./locales/en.json`;
|
const DEFAULT_LOCALE_PATH = `./locales/en.json`;
|
||||||
|
|
||||||
// Track current bbox selection info localization key for language changes
|
|
||||||
let currentBboxSelectionKey = "select_area_prompt";
|
|
||||||
let currentBboxSelectionColor = "#ffffff";
|
|
||||||
|
|
||||||
// Helper function to set bbox selection info text and track it for language changes
|
|
||||||
async function setBboxSelectionInfo(bboxSelectionElement, localizationKey, color) {
|
|
||||||
currentBboxSelectionKey = localizationKey;
|
|
||||||
currentBboxSelectionColor = color;
|
|
||||||
|
|
||||||
// Ensure localization is available
|
|
||||||
let localization = window.localization;
|
|
||||||
if (!localization) {
|
|
||||||
localization = await getLocalization();
|
|
||||||
}
|
|
||||||
|
|
||||||
localizeElement(localization, { element: bboxSelectionElement }, localizationKey);
|
|
||||||
bboxSelectionElement.style.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize elements and start the demo progress
|
// Initialize elements and start the demo progress
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
registerMessageEvent();
|
registerMessageEvent();
|
||||||
@@ -43,7 +24,6 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
handleBboxInput();
|
handleBboxInput();
|
||||||
const localization = await getLocalization();
|
const localization = await getLocalization();
|
||||||
await applyLocalization(localization);
|
await applyLocalization(localization);
|
||||||
updateFormatToggleUI(selectedWorldFormat);
|
|
||||||
initFooter();
|
initFooter();
|
||||||
await checkForUpdates();
|
await checkForUpdates();
|
||||||
});
|
});
|
||||||
@@ -85,7 +65,7 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
|||||||
const attribute = localizedStringKey.startsWith("placeholder_") ? "placeholder" : "textContent";
|
const attribute = localizedStringKey.startsWith("placeholder_") ? "placeholder" : "textContent";
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
if (json && localizedStringKey in json) {
|
if (localizedStringKey in json) {
|
||||||
element[attribute] = json[localizedStringKey];
|
element[attribute] = json[localizedStringKey];
|
||||||
} else {
|
} else {
|
||||||
// Fallback to default (English) string
|
// Fallback to default (English) string
|
||||||
@@ -97,15 +77,20 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
|||||||
|
|
||||||
async function applyLocalization(localization) {
|
async function applyLocalization(localization) {
|
||||||
const localizationElements = {
|
const localizationElements = {
|
||||||
|
"h2[data-localize='select_location']": "select_location",
|
||||||
|
"#bbox-text": "zoom_in_and_choose",
|
||||||
|
"h2[data-localize='select_world']": "select_world",
|
||||||
"span[id='choose_world']": "choose_world",
|
"span[id='choose_world']": "choose_world",
|
||||||
"#selected-world": "no_world_selected",
|
"#selected-world": "no_world_selected",
|
||||||
"#start-button": "start_generation",
|
"#start-button": "start_generation",
|
||||||
|
"h2[data-localize='progress']": "progress",
|
||||||
"h2[data-localize='choose_world_modal_title']": "choose_world_modal_title",
|
"h2[data-localize='choose_world_modal_title']": "choose_world_modal_title",
|
||||||
"button[data-localize='select_existing_world']": "select_existing_world",
|
"button[data-localize='select_existing_world']": "select_existing_world",
|
||||||
"button[data-localize='generate_new_world']": "generate_new_world",
|
"button[data-localize='generate_new_world']": "generate_new_world",
|
||||||
"h2[data-localize='customization_settings']": "customization_settings",
|
"h2[data-localize='customization_settings']": "customization_settings",
|
||||||
"label[data-localize='world_scale']": "world_scale",
|
"label[data-localize='world_scale']": "world_scale",
|
||||||
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
|
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
|
||||||
|
"label[data-localize='floodfill_timeout']": "floodfill_timeout",
|
||||||
// DEPRECATED: Ground level localization removed
|
// DEPRECATED: Ground level localization removed
|
||||||
// "label[data-localize='ground_level']": "ground_level",
|
// "label[data-localize='ground_level']": "ground_level",
|
||||||
"label[data-localize='language']": "language",
|
"label[data-localize='language']": "language",
|
||||||
@@ -124,6 +109,7 @@ async function applyLocalization(localization) {
|
|||||||
|
|
||||||
// Placeholder strings
|
// Placeholder strings
|
||||||
"input[id='bbox-coords']": "placeholder_bbox",
|
"input[id='bbox-coords']": "placeholder_bbox",
|
||||||
|
"input[id='floodfill-timeout']": "placeholder_floodfill",
|
||||||
// DEPRECATED: Ground level placeholder removed
|
// DEPRECATED: Ground level placeholder removed
|
||||||
// "input[id='ground-level']": "placeholder_ground"
|
// "input[id='ground-level']": "placeholder_ground"
|
||||||
};
|
};
|
||||||
@@ -132,13 +118,6 @@ async function applyLocalization(localization) {
|
|||||||
localizeElement(localization, { selector: selector }, localizationElements[selector]);
|
localizeElement(localization, { selector: selector }, localizationElements[selector]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-apply current bbox selection info text with new language
|
|
||||||
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
|
|
||||||
if (bboxSelectionInfo && currentBboxSelectionKey) {
|
|
||||||
localizeElement(localization, { element: bboxSelectionInfo }, currentBboxSelectionKey);
|
|
||||||
bboxSelectionInfo.style.color = currentBboxSelectionColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update error messages
|
// Update error messages
|
||||||
window.localization = localization;
|
window.localization = localization;
|
||||||
}
|
}
|
||||||
@@ -187,7 +166,7 @@ async function checkForUpdates() {
|
|||||||
updateMessage.style.textDecoration = "none";
|
updateMessage.style.textDecoration = "none";
|
||||||
|
|
||||||
localizeElement(window.localization, { element: updateMessage }, "new_version_available");
|
localizeElement(window.localization, { element: updateMessage }, "new_version_available");
|
||||||
footer.style.marginTop = "10px";
|
footer.style.marginTop = "15px";
|
||||||
footer.appendChild(updateMessage);
|
footer.appendChild(updateMessage);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -210,7 +189,7 @@ function registerMessageEvent() {
|
|||||||
// Function to set up the progress bar listener
|
// Function to set up the progress bar listener
|
||||||
function setupProgressListener() {
|
function setupProgressListener() {
|
||||||
const progressBar = document.getElementById("progress-bar");
|
const progressBar = document.getElementById("progress-bar");
|
||||||
const progressInfo = document.getElementById("progress-info");
|
const progressMessage = document.getElementById("progress-message");
|
||||||
const progressDetail = document.getElementById("progress-detail");
|
const progressDetail = document.getElementById("progress-detail");
|
||||||
|
|
||||||
window.__TAURI__.event.listen("progress-update", (event) => {
|
window.__TAURI__.event.listen("progress-update", (event) => {
|
||||||
@@ -222,16 +201,16 @@ function setupProgressListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message != "") {
|
if (message != "") {
|
||||||
progressInfo.textContent = message;
|
progressMessage.textContent = message;
|
||||||
|
|
||||||
if (message.startsWith("Error!")) {
|
if (message.startsWith("Error!")) {
|
||||||
progressInfo.style.color = "#fa7878";
|
progressMessage.style.color = "#fa7878";
|
||||||
generationButtonEnabled = true;
|
generationButtonEnabled = true;
|
||||||
} else if (message.startsWith("Done!")) {
|
} else if (message.startsWith("Done!")) {
|
||||||
progressInfo.style.color = "#7bd864";
|
progressMessage.style.color = "#7bd864";
|
||||||
generationButtonEnabled = true;
|
generationButtonEnabled = true;
|
||||||
} else {
|
} else {
|
||||||
progressInfo.style.color = "#ececec";
|
progressMessage.style.color = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -241,17 +220,6 @@ function setupProgressListener() {
|
|||||||
console.log("Map preview ready event received");
|
console.log("Map preview ready event received");
|
||||||
showWorldPreviewButton();
|
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() {
|
function initSettings() {
|
||||||
@@ -272,20 +240,6 @@ function initSettings() {
|
|||||||
settingsModal.style.display = "none";
|
settingsModal.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close settings and license modals on escape key
|
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
if (settingsModal.style.display === "flex") {
|
|
||||||
closeSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
const licenseModal = document.getElementById("license-modal");
|
|
||||||
if (licenseModal && licenseModal.style.display === "flex") {
|
|
||||||
closeLicense();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.openSettings = openSettings;
|
window.openSettings = openSettings;
|
||||||
window.closeSettings = closeSettings;
|
window.closeSettings = closeSettings;
|
||||||
|
|
||||||
@@ -294,9 +248,6 @@ function initSettings() {
|
|||||||
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
|
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// World format toggle (Java/Bedrock)
|
|
||||||
initWorldFormatToggle();
|
|
||||||
|
|
||||||
// Language selector
|
// Language selector
|
||||||
const languageSelect = document.getElementById("language-select");
|
const languageSelect = document.getElementById("language-select");
|
||||||
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
||||||
@@ -399,72 +350,6 @@ function initSettings() {
|
|||||||
window.closeLicense = closeLicense;
|
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)
|
// Telemetry consent (first run only)
|
||||||
function initTelemetryConsent() {
|
function initTelemetryConsent() {
|
||||||
const key = 'telemetry-consent'; // values: 'true' | 'false'
|
const key = 'telemetry-consent'; // values: 'true' | 'false'
|
||||||
@@ -535,7 +420,7 @@ function initWorldPicker() {
|
|||||||
*/
|
*/
|
||||||
function handleBboxInput() {
|
function handleBboxInput() {
|
||||||
const inputBox = document.getElementById("bbox-coords");
|
const inputBox = document.getElementById("bbox-coords");
|
||||||
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
|
const bboxInfo = document.getElementById("bbox-info");
|
||||||
|
|
||||||
inputBox.addEventListener("input", function () {
|
inputBox.addEventListener("input", function () {
|
||||||
const input = inputBox.value.trim();
|
const input = inputBox.value.trim();
|
||||||
@@ -547,12 +432,11 @@ function handleBboxInput() {
|
|||||||
|
|
||||||
// Clear the info text only if no map selection exists
|
// Clear the info text only if no map selection exists
|
||||||
if (!mapSelectedBBox) {
|
if (!mapSelectedBBox) {
|
||||||
setBboxSelectionInfo(bboxSelectionInfo, "select_area_prompt", "#ffffff");
|
bboxInfo.textContent = "";
|
||||||
|
bboxInfo.style.color = "";
|
||||||
} else {
|
} else {
|
||||||
// Restore map selection info display but don't update input field
|
// Restore map selection display
|
||||||
const [lng1, lat1, lng2, lat2] = mapSelectedBBox.split(" ").map(Number);
|
displayBboxInfoText(mapSelectedBBox);
|
||||||
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
|
||||||
displayBboxSizeStatus(bboxSelectionInfo, selectedSize);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -588,7 +472,8 @@ function handleBboxInput() {
|
|||||||
// Update the info text and mark custom input as valid
|
// Update the info text and mark custom input as valid
|
||||||
customBBoxValid = true;
|
customBBoxValid = true;
|
||||||
selectedBBox = bboxText.replace(/,/g, ' '); // Convert to space format for consistency
|
selectedBBox = bboxText.replace(/,/g, ' '); // Convert to space format for consistency
|
||||||
setBboxSelectionInfo(bboxSelectionInfo, "custom_selection_confirmed", "#7bd864");
|
localizeElement(window.localization, { element: bboxInfo }, "custom_selection_confirmed");
|
||||||
|
bboxInfo.style.color = "#7bd864";
|
||||||
} else {
|
} else {
|
||||||
// Valid numbers but invalid order or range
|
// Valid numbers but invalid order or range
|
||||||
customBBoxValid = false;
|
customBBoxValid = false;
|
||||||
@@ -598,7 +483,8 @@ function handleBboxInput() {
|
|||||||
} else {
|
} else {
|
||||||
selectedBBox = mapSelectedBBox;
|
selectedBBox = mapSelectedBBox;
|
||||||
}
|
}
|
||||||
setBboxSelectionInfo(bboxSelectionInfo, "error_coordinates_out_of_range", "#fecc44");
|
localizeElement(window.localization, { element: bboxInfo }, "error_coordinates_out_of_range");
|
||||||
|
bboxInfo.style.color = "#fecc44";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Input doesn't match the required format
|
// Input doesn't match the required format
|
||||||
@@ -609,7 +495,8 @@ function handleBboxInput() {
|
|||||||
} else {
|
} else {
|
||||||
selectedBBox = mapSelectedBBox;
|
selectedBBox = mapSelectedBBox;
|
||||||
}
|
}
|
||||||
setBboxSelectionInfo(bboxSelectionInfo, "invalid_format", "#fecc44");
|
localizeElement(window.localization, { element: bboxInfo }, "invalid_format");
|
||||||
|
bboxInfo.style.color = "#fecc44";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -652,27 +539,12 @@ function normalizeLongitude(lon) {
|
|||||||
return ((lon + 180) % 360 + 360) % 360 - 180;
|
return ((lon + 180) % 360 + 360) % 360 - 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
const threshold1 = 44000000.00; // Yellow warning threshold (~6.2km x 7km)
|
const threshold1 = 30000000.00;
|
||||||
const threshold2 = 85000000.00; // Red error threshold (~8.7km x 9.8km)
|
const threshold2 = 45000000.00;
|
||||||
let selectedBBox = "";
|
let selectedBBox = "";
|
||||||
let mapSelectedBBox = ""; // Tracks bbox from map selection
|
let mapSelectedBBox = ""; // Tracks bbox from map selection
|
||||||
let customBBoxValid = false; // Tracks if custom input is valid
|
let customBBoxValid = false; // Tracks if custom input is valid
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the appropriate bbox size status message based on area thresholds
|
|
||||||
* @param {HTMLElement} bboxSelectionElement - The element to display the message in
|
|
||||||
* @param {number} selectedSize - The calculated bbox area in square meters
|
|
||||||
*/
|
|
||||||
function displayBboxSizeStatus(bboxSelectionElement, selectedSize) {
|
|
||||||
if (selectedSize > threshold2) {
|
|
||||||
setBboxSelectionInfo(bboxSelectionElement, "area_too_large", "#fa7878");
|
|
||||||
} else if (selectedSize > threshold1) {
|
|
||||||
setBboxSelectionInfo(bboxSelectionElement, "area_extensive", "#fecc44");
|
|
||||||
} else {
|
|
||||||
setBboxSelectionInfo(bboxSelectionElement, "selection_confirmed", "#7bd864");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle incoming bbox data
|
// Function to handle incoming bbox data
|
||||||
function displayBboxInfoText(bboxText) {
|
function displayBboxInfoText(bboxText) {
|
||||||
let [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
|
let [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
|
||||||
@@ -686,13 +558,11 @@ function displayBboxInfoText(bboxText) {
|
|||||||
selectedBBox = mapSelectedBBox;
|
selectedBBox = mapSelectedBBox;
|
||||||
customBBoxValid = false;
|
customBBoxValid = false;
|
||||||
|
|
||||||
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
|
const bboxInfo = document.getElementById("bbox-info");
|
||||||
const bboxCoordsInput = document.getElementById("bbox-coords");
|
|
||||||
|
|
||||||
// Reset the info text if the bbox is 0,0,0,0
|
// Reset the info text if the bbox is 0,0,0,0
|
||||||
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
|
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
|
||||||
setBboxSelectionInfo(bboxSelectionInfo, "select_area_prompt", "#ffffff");
|
bboxInfo.textContent = "";
|
||||||
bboxCoordsInput.value = "";
|
|
||||||
mapSelectedBBox = "";
|
mapSelectedBBox = "";
|
||||||
if (!customBBoxValid) {
|
if (!customBBoxValid) {
|
||||||
selectedBBox = "";
|
selectedBBox = "";
|
||||||
@@ -700,13 +570,19 @@ function displayBboxInfoText(bboxText) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the custom bbox input with the map selection (comma-separated format)
|
|
||||||
bboxCoordsInput.value = `${lng1},${lat1},${lng2},${lat2}`;
|
|
||||||
|
|
||||||
// Calculate the size of the selected bbox
|
// Calculate the size of the selected bbox
|
||||||
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
||||||
|
|
||||||
displayBboxSizeStatus(bboxSelectionInfo, selectedSize);
|
if (selectedSize > threshold2) {
|
||||||
|
localizeElement(window.localization, { element: bboxInfo }, "area_too_large");
|
||||||
|
bboxInfo.style.color = "#fa7878";
|
||||||
|
} else if (selectedSize > threshold1) {
|
||||||
|
localizeElement(window.localization, { element: bboxInfo }, "area_extensive");
|
||||||
|
bboxInfo.style.color = "#fecc44";
|
||||||
|
} else {
|
||||||
|
localizeElement(window.localization, { element: bboxInfo }, "selection_confirmed");
|
||||||
|
bboxInfo.style.color = "#7bd864";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let worldPath = "";
|
let worldPath = "";
|
||||||
@@ -796,13 +672,13 @@ async function startGeneration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
|
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
|
||||||
const bboxSelectionInfo = document.getElementById('bbox-selection-info');
|
const bboxInfo = document.getElementById('bbox-info');
|
||||||
setBboxSelectionInfo(bboxSelectionInfo, "select_location_first", "#fa7878");
|
localizeElement(window.localization, { element: bboxInfo }, "select_location_first");
|
||||||
|
bboxInfo.style.color = "#fa7878";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only require world selection for Java format (Bedrock generates a new .mcworld file)
|
if (!worldPath || worldPath === "") {
|
||||||
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
|
|
||||||
const selectedWorld = document.getElementById('selected-world');
|
const selectedWorld = document.getElementById('selected-world');
|
||||||
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
|
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
|
||||||
selectedWorld.style.color = "#fa7878";
|
selectedWorld.style.color = "#fa7878";
|
||||||
@@ -833,12 +709,14 @@ async function startGeneration() {
|
|||||||
var roof = document.getElementById("roof-toggle").checked;
|
var roof = document.getElementById("roof-toggle").checked;
|
||||||
var fill_ground = document.getElementById("fillground-toggle").checked;
|
var fill_ground = document.getElementById("fillground-toggle").checked;
|
||||||
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
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);
|
// var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
||||||
// DEPRECATED: Ground level input removed from UI
|
// DEPRECATED: Ground level input removed from UI
|
||||||
var ground_level = -62;
|
var ground_level = -62;
|
||||||
|
|
||||||
// Validate ground_level
|
// Validate floodfill_timeout and ground_level
|
||||||
ground_level = isNaN(ground_level) || ground_level < -62 ? -62 : 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)
|
// Get telemetry consent (defaults to false if not set)
|
||||||
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
|
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
|
||||||
@@ -849,6 +727,7 @@ async function startGeneration() {
|
|||||||
selectedWorld: worldPath,
|
selectedWorld: worldPath,
|
||||||
worldScale: scale,
|
worldScale: scale,
|
||||||
groundLevel: ground_level,
|
groundLevel: ground_level,
|
||||||
|
floodfillTimeout: floodfill_timeout,
|
||||||
terrainEnabled: terrain,
|
terrainEnabled: terrain,
|
||||||
skipOsmObjects: skipOsmObjects,
|
skipOsmObjects: skipOsmObjects,
|
||||||
interiorEnabled: interior,
|
interiorEnabled: interior,
|
||||||
@@ -856,8 +735,7 @@ async function startGeneration() {
|
|||||||
fillgroundEnabled: fill_ground,
|
fillgroundEnabled: fill_ground,
|
||||||
isNewWorld: isNewWorld,
|
isNewWorld: isNewWorld,
|
||||||
spawnPoint: spawnPoint,
|
spawnPoint: spawnPoint,
|
||||||
telemetryConsent: telemetryConsent || false,
|
telemetryConsent: telemetryConsent || false
|
||||||
worldFormat: selectedWorldFormat
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Generation process started.");
|
console.log("Generation process started.");
|
||||||
|
|||||||
8
src/gui/locales/ar.json
vendored
8
src/gui/locales/ar.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "اختيار موقع",
|
||||||
|
"zoom_in_and_choose": "قم بالتكبير واختر منطقتك باستخدام أداة المستطيل",
|
||||||
|
"select_world": "تحديد عالم",
|
||||||
"choose_world": "اختيار عالم",
|
"choose_world": "اختيار عالم",
|
||||||
"no_world_selected": "لم يتم تحديد عالم",
|
"no_world_selected": "لم يتم تحديد عالم",
|
||||||
"start_generation": "بدء البناء",
|
"start_generation": "بدء البناء",
|
||||||
|
"progress": "التقدم",
|
||||||
"custom_selection_confirmed": "تم تأكيد التحديد المخصص!",
|
"custom_selection_confirmed": "تم تأكيد التحديد المخصص!",
|
||||||
"error_coordinates_out_of_range": "خطأ: الإحداثيات خارج النطاق أو مرتبة بشكل غير صحيح (مطلوب خط العرض قبل خط الطول).",
|
"error_coordinates_out_of_range": "خطأ: الإحداثيات خارج النطاق أو مرتبة بشكل غير صحيح (مطلوب خط العرض قبل خط الطول).",
|
||||||
"invalid_format": "تنسيق غير صالح. استخدم 'lat,lng,lat,lng' أو 'lat lng lat lng'.",
|
"invalid_format": "تنسيق غير صالح. استخدم 'lat,lng,lat,lng' أو 'lat lng lat lng'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "تُعتبر هذه المنطقة كبيرة جدًا وقد تتجاوز حدود الحوسبة النموذجية.",
|
"area_too_large": "تُعتبر هذه المنطقة كبيرة جدًا وقد تتجاوز حدود الحوسبة النموذجية.",
|
||||||
"area_extensive": "المنطقة واسعة جدًا وقد تتطلب الكثير من الوقت والموارد.",
|
"area_extensive": "المنطقة واسعة جدًا وقد تتطلب الكثير من الوقت والموارد.",
|
||||||
"selection_confirmed": "تم تأكيد التحديد!",
|
"selection_confirmed": "تم تأكيد التحديد!",
|
||||||
"select_area_prompt": "حدد منطقة على الخريطة باستخدام الأدوات.",
|
|
||||||
"unknown_error": "خطأ غير معروف",
|
"unknown_error": "خطأ غير معروف",
|
||||||
"license_and_credits": "الرخصة والمساهمون",
|
"license_and_credits": "الرخصة والمساهمون",
|
||||||
"placeholder_bbox": "الصيغة: lat,lng,lat,lng",
|
"placeholder_bbox": "الصيغة: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "تضاريس فقط",
|
"mode_terrain_only": "تضاريس فقط",
|
||||||
"interior": "توليد الداخلية",
|
"interior": "توليد الداخلية",
|
||||||
"roof": "توليد السقف",
|
"roof": "توليد السقف",
|
||||||
"fillground": "ملء الأرض",
|
"fillground": "ملء الأرض"
|
||||||
"bedrock_use_java": "استخدم Java لاختيار العوالم"
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/gui/locales/de.json
vendored
8
src/gui/locales/de.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Standort auswählen",
|
||||||
|
"zoom_in_and_choose": "Zoome hinein und wähle dein Gebiet aus",
|
||||||
|
"select_world": "Welt auswählen",
|
||||||
"choose_world": "Welt wählen",
|
"choose_world": "Welt wählen",
|
||||||
"no_world_selected": "Keine Welt ausgewählt",
|
"no_world_selected": "Keine Welt ausgewählt",
|
||||||
"start_generation": "Generierung starten",
|
"start_generation": "Generierung starten",
|
||||||
|
"progress": "Fortschritt",
|
||||||
"custom_selection_confirmed": "Benutzerdefinierte Auswahl bestätigt!",
|
"custom_selection_confirmed": "Benutzerdefinierte Auswahl bestätigt!",
|
||||||
"error_coordinates_out_of_range": "Fehler: Koordinaten sind außerhalb des Bereichs oder falsch geordnet (Lat vor Lng erforderlich).",
|
"error_coordinates_out_of_range": "Fehler: Koordinaten sind außerhalb des Bereichs oder falsch geordnet (Lat vor Lng erforderlich).",
|
||||||
"invalid_format": "Ungültiges Format. Bitte verwende 'lat,lng,lat,lng' oder 'lat lng lat lng'.",
|
"invalid_format": "Ungültiges Format. Bitte verwende 'lat,lng,lat,lng' oder 'lat lng lat lng'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Dieses Gebiet ist sehr groß und könnte das Berechnungslimit überschreiten.",
|
"area_too_large": "Dieses Gebiet ist sehr groß und könnte das Berechnungslimit überschreiten.",
|
||||||
"area_extensive": "Diese Gebietsgröße könnte längere Zeit für die Generierung benötigen.",
|
"area_extensive": "Diese Gebietsgröße könnte längere Zeit für die Generierung benötigen.",
|
||||||
"selection_confirmed": "Auswahl bestätigt!",
|
"selection_confirmed": "Auswahl bestätigt!",
|
||||||
"select_area_prompt": "Wähle einen Bereich auf der Karte aus.",
|
|
||||||
"unknown_error": "Unbekannter Fehler",
|
"unknown_error": "Unbekannter Fehler",
|
||||||
"license_and_credits": "Lizenz und Credits",
|
"license_and_credits": "Lizenz und Credits",
|
||||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Nur Terrain",
|
"mode_terrain_only": "Nur Terrain",
|
||||||
"interior": "Innenraum Generierung",
|
"interior": "Innenraum Generierung",
|
||||||
"roof": "Dach Generierung",
|
"roof": "Dach Generierung",
|
||||||
"fillground": "Boden füllen",
|
"fillground": "Boden füllen"
|
||||||
"bedrock_use_java": "Java für Weltauswahl nutzen"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/en-US.json
vendored
8
src/gui/locales/en-US.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Select Location",
|
||||||
|
"zoom_in_and_choose": "Zoom in and choose your area using the rectangle tool",
|
||||||
|
"select_world": "Select World",
|
||||||
"choose_world": "Choose World",
|
"choose_world": "Choose World",
|
||||||
"no_world_selected": "No world selected",
|
"no_world_selected": "No world selected",
|
||||||
"start_generation": "Start Generation",
|
"start_generation": "Start Generation",
|
||||||
|
"progress": "Progress",
|
||||||
"custom_selection_confirmed": "Custom selection confirmed!",
|
"custom_selection_confirmed": "Custom selection confirmed!",
|
||||||
"error_coordinates_out_of_range": "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).",
|
"error_coordinates_out_of_range": "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).",
|
||||||
"invalid_format": "Invalid format. Please use 'lat,lng,lat,lng' or 'lat lng lat lng'.",
|
"invalid_format": "Invalid format. Please use 'lat,lng,lat,lng' or 'lat lng lat lng'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "This area is very large and could exceed typical computing limits.",
|
"area_too_large": "This area is very large and could exceed typical computing limits.",
|
||||||
"area_extensive": "The area is quite extensive and may take significant time and resources.",
|
"area_extensive": "The area is quite extensive and may take significant time and resources.",
|
||||||
"selection_confirmed": "Selection confirmed!",
|
"selection_confirmed": "Selection confirmed!",
|
||||||
"select_area_prompt": "Select an area on the map using the tools.",
|
|
||||||
"unknown_error": "Unknown error",
|
"unknown_error": "Unknown error",
|
||||||
"license_and_credits": "License and Credits",
|
"license_and_credits": "License and Credits",
|
||||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Terrain only",
|
"mode_terrain_only": "Terrain only",
|
||||||
"interior": "Interior Generation",
|
"interior": "Interior Generation",
|
||||||
"roof": "Roof Generation",
|
"roof": "Roof Generation",
|
||||||
"fillground": "Fill Ground",
|
"fillground": "Fill Ground"
|
||||||
"bedrock_use_java": "Use Java to select worlds"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/es.json
vendored
8
src/gui/locales/es.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Seleccionar ubicación",
|
||||||
|
"zoom_in_and_choose": "Acércate y elige tu área usando la herramienta de rectángulo",
|
||||||
|
"select_world": "Seleccionar mundo",
|
||||||
"choose_world": "Elegir mundo",
|
"choose_world": "Elegir mundo",
|
||||||
"no_world_selected": "Ningún mundo seleccionado",
|
"no_world_selected": "Ningún mundo seleccionado",
|
||||||
"start_generation": "Iniciar generación",
|
"start_generation": "Iniciar generación",
|
||||||
|
"progress": "Progreso",
|
||||||
"custom_selection_confirmed": "¡Selección personalizada confirmada!",
|
"custom_selection_confirmed": "¡Selección personalizada confirmada!",
|
||||||
"error_coordinates_out_of_range": "Error: Las coordenadas están fuera de rango o están ordenadas incorrectamente (Lat antes de Lng requerido).",
|
"error_coordinates_out_of_range": "Error: Las coordenadas están fuera de rango o están ordenadas incorrectamente (Lat antes de Lng requerido).",
|
||||||
"invalid_format": "Formato inválido. Por favor, use 'lat,lng,lat,lng' o 'lat lng lat lng'.",
|
"invalid_format": "Formato inválido. Por favor, use 'lat,lng,lat,lng' o 'lat lng lat lng'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Esta área es muy grande y podría exceder los límites típicos de computación.",
|
"area_too_large": "Esta área es muy grande y podría exceder los límites típicos de computación.",
|
||||||
"area_extensive": "El área es bastante extensa y puede requerir mucho tiempo y recursos.",
|
"area_extensive": "El área es bastante extensa y puede requerir mucho tiempo y recursos.",
|
||||||
"selection_confirmed": "¡Selección confirmada!",
|
"selection_confirmed": "¡Selección confirmada!",
|
||||||
"select_area_prompt": "Selecciona un área en el mapa usando las herramientas.",
|
|
||||||
"unknown_error": "Unknown error",
|
"unknown_error": "Unknown error",
|
||||||
"license_and_credits": "License and Credits",
|
"license_and_credits": "License and Credits",
|
||||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Solo Terreno",
|
"mode_terrain_only": "Solo Terreno",
|
||||||
"interior": "Generación Interior",
|
"interior": "Generación Interior",
|
||||||
"roof": "Generación de Tejado",
|
"roof": "Generación de Tejado",
|
||||||
"fillground": "Rellenar Suelo",
|
"fillground": "Rellenar Suelo"
|
||||||
"bedrock_use_java": "Usa Java para elegir mundos"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/fi.json
vendored
8
src/gui/locales/fi.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Valitse paikka",
|
||||||
|
"zoom_in_and_choose": "Zoomaa ja valitse paikka käyttämällä suorakulmatyökalua.",
|
||||||
|
"select_world": "Valitse maailma",
|
||||||
"choose_world": "Valitse maailma",
|
"choose_world": "Valitse maailma",
|
||||||
"no_world_selected": "Maailmaa ei valittu",
|
"no_world_selected": "Maailmaa ei valittu",
|
||||||
"start_generation": "Aloita generointi",
|
"start_generation": "Aloita generointi",
|
||||||
|
"progress": "Edistys",
|
||||||
"custom_selection_confirmed": "Mukautettu valinta vahvistettu!",
|
"custom_selection_confirmed": "Mukautettu valinta vahvistettu!",
|
||||||
"error_coordinates_out_of_range": "Virhe: Koordinaatit ovat kantaman ulkopuolella tai vääriin aseteltu (Lat ennen Lng vaadittu).",
|
"error_coordinates_out_of_range": "Virhe: Koordinaatit ovat kantaman ulkopuolella tai vääriin aseteltu (Lat ennen Lng vaadittu).",
|
||||||
"invalid_format": "Väärä formaatti. Käytä 'lat,lng,lat,lng' tai 'lat lng lat lng'.",
|
"invalid_format": "Väärä formaatti. Käytä 'lat,lng,lat,lng' tai 'lat lng lat lng'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Tämä alue on todella iso ja voi ylittää tyypilliset laskentarajat.",
|
"area_too_large": "Tämä alue on todella iso ja voi ylittää tyypilliset laskentarajat.",
|
||||||
"area_extensive": "Alue on aika laaja ja voi viedä pitkän ajan ja resursseja.",
|
"area_extensive": "Alue on aika laaja ja voi viedä pitkän ajan ja resursseja.",
|
||||||
"selection_confirmed": "Valinta vahvistettu!",
|
"selection_confirmed": "Valinta vahvistettu!",
|
||||||
"select_area_prompt": "Valitse alue kartalta työkaluilla.",
|
|
||||||
"unknown_error": "Tuntematon virhe",
|
"unknown_error": "Tuntematon virhe",
|
||||||
"license_and_credits": "Lisenssi ja krediitit",
|
"license_and_credits": "Lisenssi ja krediitit",
|
||||||
"placeholder_bbox": "Formaatti: lat,lng,lat,lng",
|
"placeholder_bbox": "Formaatti: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Vain maasto",
|
"mode_terrain_only": "Vain maasto",
|
||||||
"interior": "Sisätilan luonti",
|
"interior": "Sisätilan luonti",
|
||||||
"roof": "Katon luonti",
|
"roof": "Katon luonti",
|
||||||
"fillground": "Täytä maa",
|
"fillground": "Täytä maa"
|
||||||
"bedrock_use_java": "Käytä Javaa maailmojen valintaan"
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/gui/locales/fr-FR.json
vendored
8
src/gui/locales/fr-FR.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Sélectionner une localisation",
|
||||||
|
"zoom_in_and_choose": "Zoomez et choisissez votre zone avec l'outil rectangle",
|
||||||
|
"select_world": "Sélectionner un monde",
|
||||||
"choose_world": "Choisir un monde",
|
"choose_world": "Choisir un monde",
|
||||||
"no_world_selected": "Aucun monde sélectionné",
|
"no_world_selected": "Aucun monde sélectionné",
|
||||||
"start_generation": "Commencer la génération",
|
"start_generation": "Commencer la génération",
|
||||||
|
"progress": "Progrès",
|
||||||
"custom_selection_confirmed": "Sélection personnalisée confirmée !",
|
"custom_selection_confirmed": "Sélection personnalisée confirmée !",
|
||||||
"error_coordinates_out_of_range": "Erreur: Coordonnées hors de portée ou dans un ordre incorrect (besoin de la latitude avant la longitude).",
|
"error_coordinates_out_of_range": "Erreur: Coordonnées hors de portée ou dans un ordre incorrect (besoin de la latitude avant la longitude).",
|
||||||
"invalid_format": "Format invalide. Utilisez 'lat,lng,lat,lng' ou 'lat lng lat lng'.",
|
"invalid_format": "Format invalide. Utilisez 'lat,lng,lat,lng' ou 'lat lng lat lng'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Cette zone est très grande et pourrait dépasser les limites de calcul courantes.",
|
"area_too_large": "Cette zone est très grande et pourrait dépasser les limites de calcul courantes.",
|
||||||
"area_extensive": "Cette zone est très étendue et pourrait nécessiter beaucoup de ressources et de temps.",
|
"area_extensive": "Cette zone est très étendue et pourrait nécessiter beaucoup de ressources et de temps.",
|
||||||
"selection_confirmed": "Sélection confirmée !",
|
"selection_confirmed": "Sélection confirmée !",
|
||||||
"select_area_prompt": "Sélectionnez une zone sur la carte avec les outils.",
|
|
||||||
"unknown_error": "Erreur inconnue",
|
"unknown_error": "Erreur inconnue",
|
||||||
"license_and_credits": "Licence et crédits",
|
"license_and_credits": "Licence et crédits",
|
||||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Terrain uniquement",
|
"mode_terrain_only": "Terrain uniquement",
|
||||||
"interior": "Génération d'intérieur",
|
"interior": "Génération d'intérieur",
|
||||||
"roof": "Génération de toit",
|
"roof": "Génération de toit",
|
||||||
"fillground": "Remplir le sol",
|
"fillground": "Remplir le sol"
|
||||||
"bedrock_use_java": "Utilisez Java pour les mondes"
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/gui/locales/hu.json
vendored
8
src/gui/locales/hu.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Hely kiválasztása",
|
||||||
|
"zoom_in_and_choose": "Nagyíts és jelöld ki a területet a kijelölő eszközzel",
|
||||||
|
"select_world": "Világ kijelölése",
|
||||||
"choose_world": "Világ kiválasztása",
|
"choose_world": "Világ kiválasztása",
|
||||||
"no_world_selected": "Nincs világ kiválasztva",
|
"no_world_selected": "Nincs világ kiválasztva",
|
||||||
"start_generation": "Generálás indítása",
|
"start_generation": "Generálás indítása",
|
||||||
|
"progress": "Haladás",
|
||||||
"custom_selection_confirmed": "Egyéni kiválasztás megerősítve",
|
"custom_selection_confirmed": "Egyéni kiválasztás megerősítve",
|
||||||
"error_coordinates_out_of_range": "Hiba: A koordináták tartományon kívül vannak vagy hibásan rendezettek (a szélességi foknak a hosszúsági fok előtt kell lennie)",
|
"error_coordinates_out_of_range": "Hiba: A koordináták tartományon kívül vannak vagy hibásan rendezettek (a szélességi foknak a hosszúsági fok előtt kell lennie)",
|
||||||
"invalid_format": "Érvénytelen formátum. Kérjük, használja a 'lat,lng,lat,lng' vagy a 'lat lng lat lng' formátumot.'.",
|
"invalid_format": "Érvénytelen formátum. Kérjük, használja a 'lat,lng,lat,lng' vagy a 'lat lng lat lng' formátumot.'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Ez a terület nagyon nagy, és meghaladhatja a szokásos számítási korlátokat.",
|
"area_too_large": "Ez a terület nagyon nagy, és meghaladhatja a szokásos számítási korlátokat.",
|
||||||
"area_extensive": "A terület meglehetősen kiterjedt, és jelentős időt és erőforrásokat igényelhet.",
|
"area_extensive": "A terület meglehetősen kiterjedt, és jelentős időt és erőforrásokat igényelhet.",
|
||||||
"selection_confirmed": "Kiválasztás megerősítve",
|
"selection_confirmed": "Kiválasztás megerősítve",
|
||||||
"select_area_prompt": "Jelölj ki egy területet a térképen az eszközökkel.",
|
|
||||||
"unknown_error": "Ismeretlen hiba",
|
"unknown_error": "Ismeretlen hiba",
|
||||||
"license_and_credits": "Licenc és elismerés.",
|
"license_and_credits": "Licenc és elismerés.",
|
||||||
"placeholder_bbox": "Formátum: lat,lng,lat,lng",
|
"placeholder_bbox": "Formátum: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Csak terep",
|
"mode_terrain_only": "Csak terep",
|
||||||
"interior": "Belső generálás",
|
"interior": "Belső generálás",
|
||||||
"roof": "Tető generálás",
|
"roof": "Tető generálás",
|
||||||
"fillground": "Talaj feltöltése",
|
"fillground": "Talaj feltöltése"
|
||||||
"bedrock_use_java": "Java világválasztáshoz"
|
|
||||||
}
|
}
|
||||||
10
src/gui/locales/ko.json
vendored
10
src/gui/locales/ko.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "장소 선택",
|
||||||
|
"zoom_in_and_choose": "줌 인하고 직사각형 도구를 사용하여 영역을 선택하세요.",
|
||||||
|
"select_world": "세계 선택",
|
||||||
"choose_world": "세계 선택",
|
"choose_world": "세계 선택",
|
||||||
"no_world_selected": "선택된 세계 없음",
|
"no_world_selected": "선택된 세계 없음",
|
||||||
"start_generation": "생성 시작",
|
"start_generation": "생성 시작",
|
||||||
|
"progress": "진행",
|
||||||
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
||||||
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
||||||
"invalid_format": "잘못된 형식입니다. 'lat,lng,lat,lng' 또는 'lat lng lat lng' 형식을 사용하세요.",
|
"invalid_format": "잘못된 형식입니다. 'lat,lng,lat,lng' 또는 'lat lng lat lng' 형식을 사용하세요.",
|
||||||
@@ -24,9 +28,8 @@
|
|||||||
"select_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
"select_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
||||||
"select_location_first": "먼저 위치를 선택하세요!",
|
"select_location_first": "먼저 위치를 선택하세요!",
|
||||||
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
||||||
"area_extensive": "이 지역은 꽤 광범위하여 상당한 시간과 자원이 필요할 수 있습니다.",
|
"area_extensive": "이 지역은 꽤 광범위하여 значитель한 시간과 자원이 필요할 수 있습니다.",
|
||||||
"selection_confirmed": "선택이 확인되었습니다!",
|
"selection_confirmed": "선택이 확인되었습니다!",
|
||||||
"select_area_prompt": "도구를 사용하여 지도에서 영역을 선택하세요.",
|
|
||||||
"unknown_error": "Unknown error",
|
"unknown_error": "Unknown error",
|
||||||
"license_and_credits": "License and Credits",
|
"license_and_credits": "License and Credits",
|
||||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "지형만",
|
"mode_terrain_only": "지형만",
|
||||||
"interior": "내부 생성",
|
"interior": "내부 생성",
|
||||||
"roof": "지붕 생성",
|
"roof": "지붕 생성",
|
||||||
"fillground": "지면 채우기",
|
"fillground": "지면 채우기"
|
||||||
"bedrock_use_java": "Java로 세계 선택"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/lt.json
vendored
8
src/gui/locales/lt.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Vietos pasirinkimas",
|
||||||
|
"zoom_in_and_choose": "Pasididinkite žemėlapį ir pasirinkite plotą su kvadrato įrankiu",
|
||||||
|
"select_world": "Pasaulio pasirinkimas",
|
||||||
"choose_world": "Pasirinkti pasaulį",
|
"choose_world": "Pasirinkti pasaulį",
|
||||||
"no_world_selected": "Pasaulis nepasirinktas",
|
"no_world_selected": "Pasaulis nepasirinktas",
|
||||||
"start_generation": "Pradėti generaciją",
|
"start_generation": "Pradėti generaciją",
|
||||||
|
"progress": "Progresas",
|
||||||
"custom_selection_confirmed": "Rėmo pasirinkimas patvirtintas!",
|
"custom_selection_confirmed": "Rėmo pasirinkimas patvirtintas!",
|
||||||
"error_coordinates_out_of_range": "Klaida: Koordinatės yra už ribų arba neteisingai išdėstytos (plat turi būti prieš ilg).",
|
"error_coordinates_out_of_range": "Klaida: Koordinatės yra už ribų arba neteisingai išdėstytos (plat turi būti prieš ilg).",
|
||||||
"invalid_format": "Neteisingas formatas. Prašome naudoti 'plat,ilg,plat,ilg' arba 'plat ilg plat ilg'.",
|
"invalid_format": "Neteisingas formatas. Prašome naudoti 'plat,ilg,plat,ilg' arba 'plat ilg plat ilg'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Šis plotas yra labai didelis ir gali viršyti tipinius resursų limitus.",
|
"area_too_large": "Šis plotas yra labai didelis ir gali viršyti tipinius resursų limitus.",
|
||||||
"area_extensive": "Šis plotas yra pakankamai didelis kuriam reikėtų daug laiko ir resursų.",
|
"area_extensive": "Šis plotas yra pakankamai didelis kuriam reikėtų daug laiko ir resursų.",
|
||||||
"selection_confirmed": "Pasirinkimas patvirtintas!",
|
"selection_confirmed": "Pasirinkimas patvirtintas!",
|
||||||
"select_area_prompt": "Pasirinkite plotą žemėlapyje naudodami įrankius.",
|
|
||||||
"unknown_error": "Nežinoma klaida",
|
"unknown_error": "Nežinoma klaida",
|
||||||
"license_and_credits": "Licencija ir padėkos",
|
"license_and_credits": "Licencija ir padėkos",
|
||||||
"placeholder_bbox": "Formatas: plat,lyg,plat,lyg",
|
"placeholder_bbox": "Formatas: plat,lyg,plat,lyg",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Tik reljefas",
|
"mode_terrain_only": "Tik reljefas",
|
||||||
"interior": "Interjero generavimas",
|
"interior": "Interjero generavimas",
|
||||||
"roof": "Stogo generavimas",
|
"roof": "Stogo generavimas",
|
||||||
"fillground": "Užpildyti pagrindą",
|
"fillground": "Užpildyti pagrindą"
|
||||||
"bedrock_use_java": "Naudok Java pasauliams"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/lv.json
vendored
8
src/gui/locales/lv.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Izvēlēties atrašanās vietu",
|
||||||
|
"zoom_in_and_choose": "Pietuviniet un izvēlieties apgabalu",
|
||||||
|
"select_world": "Izvēlēties pasauli",
|
||||||
"choose_world": "Izvēlēties pasauli",
|
"choose_world": "Izvēlēties pasauli",
|
||||||
"no_world_selected": "Pasaulē nav izvēlēta",
|
"no_world_selected": "Pasaulē nav izvēlēta",
|
||||||
"start_generation": "Sākt ģenerēšanu",
|
"start_generation": "Sākt ģenerēšanu",
|
||||||
|
"progress": "Progress",
|
||||||
"custom_selection_confirmed": "Pielāgota izvēle apstiprināta!",
|
"custom_selection_confirmed": "Pielāgota izvēle apstiprināta!",
|
||||||
"error_coordinates_out_of_range": "Kļūda: koordinātas ir ārpus darbības zonas vai norādītas nepareizā secībā (vispirms platums, tad garums)",
|
"error_coordinates_out_of_range": "Kļūda: koordinātas ir ārpus darbības zonas vai norādītas nepareizā secībā (vispirms platums, tad garums)",
|
||||||
"invalid_format": "Nederīgs formāts. Izmantojiet 'platums,garums,platums,garums' vai 'platums garums platums garums'",
|
"invalid_format": "Nederīgs formāts. Izmantojiet 'platums,garums,platums,garums' vai 'platums garums platums garums'",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Šis apgabals ir pārāk liels un var pārsniegt tipiskos aprēķina ierobežojumus",
|
"area_too_large": "Šis apgabals ir pārāk liels un var pārsniegt tipiskos aprēķina ierobežojumus",
|
||||||
"area_extensive": "Apgabals ir diezgan plašs un var prasīt ievērojamu laiku un resursus",
|
"area_extensive": "Apgabals ir diezgan plašs un var prasīt ievērojamu laiku un resursus",
|
||||||
"selection_confirmed": "Izvēle apstiprināta!",
|
"selection_confirmed": "Izvēle apstiprināta!",
|
||||||
"select_area_prompt": "Izvēlieties apgabalu kartē, izmantojot rīkus.",
|
|
||||||
"unknown_error": "Nezināma kļūda",
|
"unknown_error": "Nezināma kļūda",
|
||||||
"license_and_credits": "Licence un autori",
|
"license_and_credits": "Licence un autori",
|
||||||
"placeholder_bbox": "Formāts: platums,garums,platums,garums",
|
"placeholder_bbox": "Formāts: platums,garums,platums,garums",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Tikai reljefs",
|
"mode_terrain_only": "Tikai reljefs",
|
||||||
"interior": "Interjera ģenerēšana",
|
"interior": "Interjera ģenerēšana",
|
||||||
"roof": "Jumta ģenerēšana",
|
"roof": "Jumta ģenerēšana",
|
||||||
"fillground": "Aizpildīt zemi",
|
"fillground": "Aizpildīt zemi"
|
||||||
"bedrock_use_java": "Izmanto Java pasaulēm"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/pl.json
vendored
8
src/gui/locales/pl.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Wybierz lokalizację",
|
||||||
|
"zoom_in_and_choose": "Przybliż i zaznacz obszar za pomocą prostokąta",
|
||||||
|
"select_world": "Wybierz świat",
|
||||||
"choose_world": "Wybierz świat",
|
"choose_world": "Wybierz świat",
|
||||||
"no_world_selected": "Nie wybrano świata",
|
"no_world_selected": "Nie wybrano świata",
|
||||||
"start_generation": "Rozpocznij generowanie",
|
"start_generation": "Rozpocznij generowanie",
|
||||||
|
"progress": "Postęp",
|
||||||
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
||||||
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub w złej kolejności (wymagana szerokość przed długością).",
|
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub w złej kolejności (wymagana szerokość przed długością).",
|
||||||
"invalid_format": "Nieprawidłowy format. Użyj 'szer.,dł.,szer.,dł.' lub 'szer. dł. szer. dł.'.",
|
"invalid_format": "Nieprawidłowy format. Użyj 'szer.,dł.,szer.,dł.' lub 'szer. dł. szer. dł.'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć limity obliczeniowe.",
|
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć limity obliczeniowe.",
|
||||||
"area_extensive": "Ten obszar jest rozległy i może pochłonąć dużo czasu oraz zasobów.",
|
"area_extensive": "Ten obszar jest rozległy i może pochłonąć dużo czasu oraz zasobów.",
|
||||||
"selection_confirmed": "Wybór potwierdzony!",
|
"selection_confirmed": "Wybór potwierdzony!",
|
||||||
"select_area_prompt": "Zaznacz obszar na mapie za pomocą narzędzi.",
|
|
||||||
"unknown_error": "Nieznany błąd",
|
"unknown_error": "Nieznany błąd",
|
||||||
"license_and_credits": "Licencja i autorzy",
|
"license_and_credits": "Licencja i autorzy",
|
||||||
"placeholder_bbox": "Format: szer,dł,szer,dł",
|
"placeholder_bbox": "Format: szer,dł,szer,dł",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Tylko teren",
|
"mode_terrain_only": "Tylko teren",
|
||||||
"interior": "Generowanie wnętrza",
|
"interior": "Generowanie wnętrza",
|
||||||
"roof": "Generowanie dachu",
|
"roof": "Generowanie dachu",
|
||||||
"fillground": "Wypełnij podłoże",
|
"fillground": "Wypełnij podłoże"
|
||||||
"bedrock_use_java": "Użyj Java do wyboru światów"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/ru.json
vendored
8
src/gui/locales/ru.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Выбрать местоположение",
|
||||||
|
"zoom_in_and_choose": "Приблизьте и выберите область",
|
||||||
|
"select_world": "Выбрать мир",
|
||||||
"choose_world": "Выбрать мир",
|
"choose_world": "Выбрать мир",
|
||||||
"no_world_selected": "Мир не выбран",
|
"no_world_selected": "Мир не выбран",
|
||||||
"start_generation": "Начать генерацию",
|
"start_generation": "Начать генерацию",
|
||||||
|
"progress": "Прогресс",
|
||||||
"custom_selection_confirmed": "Пользовательский выбор подтвержден!",
|
"custom_selection_confirmed": "Пользовательский выбор подтвержден!",
|
||||||
"error_coordinates_out_of_range": "Ошибка: Координаты находятся вне зоны действия или указаны в неправильном порядке (сначала широта, затем долгота)",
|
"error_coordinates_out_of_range": "Ошибка: Координаты находятся вне зоны действия или указаны в неправильном порядке (сначала широта, затем долгота)",
|
||||||
"invalid_format": "Неверный формат. Используйте 'широта,долгота,широта,долгота' или 'широта долгота широта долгота'",
|
"invalid_format": "Неверный формат. Используйте 'широта,долгота,широта,долгота' или 'широта долгота широта долгота'",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Эта область слишком велика и может превысить типичные вычислительные ограничения",
|
"area_too_large": "Эта область слишком велика и может превысить типичные вычислительные ограничения",
|
||||||
"area_extensive": "Область довольно обширна и может потребовать значительного времени и ресурсов",
|
"area_extensive": "Область довольно обширна и может потребовать значительного времени и ресурсов",
|
||||||
"selection_confirmed": "Выбор подтвержден!",
|
"selection_confirmed": "Выбор подтвержден!",
|
||||||
"select_area_prompt": "Выберите область на карте с помощью инструментов.",
|
|
||||||
"unknown_error": "Неизвестная ошибка",
|
"unknown_error": "Неизвестная ошибка",
|
||||||
"license_and_credits": "Лицензия и авторы",
|
"license_and_credits": "Лицензия и авторы",
|
||||||
"placeholder_bbox": "Формат: широта,долгота,широта,долгота",
|
"placeholder_bbox": "Формат: широта,долгота,широта,долгота",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Только Рельеф",
|
"mode_terrain_only": "Только Рельеф",
|
||||||
"interior": "Генерация Интерьера",
|
"interior": "Генерация Интерьера",
|
||||||
"roof": "Генерация Крыши",
|
"roof": "Генерация Крыши",
|
||||||
"fillground": "Заполнить Землю",
|
"fillground": "Заполнить Землю"
|
||||||
"bedrock_use_java": "Используйте Java для миров"
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/gui/locales/sv.json
vendored
8
src/gui/locales/sv.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Välj plats",
|
||||||
|
"zoom_in_and_choose": "Zooma in och välj ditt område med rektangulärt verktyg",
|
||||||
|
"select_world": "Välj värld",
|
||||||
"choose_world": "Välj värld",
|
"choose_world": "Välj värld",
|
||||||
"no_world_selected": "Ingen värld vald",
|
"no_world_selected": "Ingen värld vald",
|
||||||
"start_generation": "Starta generering",
|
"start_generation": "Starta generering",
|
||||||
|
"progress": "Framsteg",
|
||||||
"custom_selection_confirmed": "Anpassad markering bekräftad!",
|
"custom_selection_confirmed": "Anpassad markering bekräftad!",
|
||||||
"error_coordinates_out_of_range": "Fel: Koordinater är utanför området eller felaktigt ordnade (Lat före Lng krävs).",
|
"error_coordinates_out_of_range": "Fel: Koordinater är utanför området eller felaktigt ordnade (Lat före Lng krävs).",
|
||||||
"invalid_format": "Ogiltigt format. Använd 'lat,lng,lat,lng' eller 'lat lng lat lng'.",
|
"invalid_format": "Ogiltigt format. Använd 'lat,lng,lat,lng' eller 'lat lng lat lng'.",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Detta område är mycket stort och kan överskrida vanliga beräkningsgränser.",
|
"area_too_large": "Detta område är mycket stort och kan överskrida vanliga beräkningsgränser.",
|
||||||
"area_extensive": "Området är ganska extensivt och kan ta betydande tid och resurser.",
|
"area_extensive": "Området är ganska extensivt och kan ta betydande tid och resurser.",
|
||||||
"selection_confirmed": "Val bekräftat!",
|
"selection_confirmed": "Val bekräftat!",
|
||||||
"select_area_prompt": "Välj ett område på kartan med verktygen.",
|
|
||||||
"unknown_error": "Unknown error",
|
"unknown_error": "Unknown error",
|
||||||
"license_and_credits": "License and Credits",
|
"license_and_credits": "License and Credits",
|
||||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Endast terräng",
|
"mode_terrain_only": "Endast terräng",
|
||||||
"interior": "Interiörgenerering",
|
"interior": "Interiörgenerering",
|
||||||
"roof": "Takgenerering",
|
"roof": "Takgenerering",
|
||||||
"fillground": "Fyll mark",
|
"fillground": "Fyll mark"
|
||||||
"bedrock_use_java": "Använd Java för världar"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/ua.json
vendored
8
src/gui/locales/ua.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "Обрати локацію",
|
||||||
|
"zoom_in_and_choose": "Збільште і оберіть область за допомогою прямокутника",
|
||||||
|
"select_world": "Обрати світ",
|
||||||
"choose_world": "Обрати світ",
|
"choose_world": "Обрати світ",
|
||||||
"no_world_selected": "Світ не обрано",
|
"no_world_selected": "Світ не обрано",
|
||||||
"start_generation": "Почати генерацію",
|
"start_generation": "Почати генерацію",
|
||||||
|
"progress": "Прогрес",
|
||||||
"custom_selection_confirmed": "Користувацький вибір підтверджено!",
|
"custom_selection_confirmed": "Користувацький вибір підтверджено!",
|
||||||
"error_coordinates_out_of_range": "Помилка: Координати поза діапазоном або неправильно впорядковані (потрібно широта перед довгота)",
|
"error_coordinates_out_of_range": "Помилка: Координати поза діапазоном або неправильно впорядковані (потрібно широта перед довгота)",
|
||||||
"invalid_format": "Неправильний формат. Будь ласка, використовуйте 'широта,довгота,широта,довгота' або 'широта довгота широта довгота'",
|
"invalid_format": "Неправильний формат. Будь ласка, використовуйте 'широта,довгота,широта,довгота' або 'широта довгота широта довгота'",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "Ця область дуже велика і може перевищити типові обчислювальні межі",
|
"area_too_large": "Ця область дуже велика і може перевищити типові обчислювальні межі",
|
||||||
"area_extensive": "Область досить велика і може вимагати значного часу та ресурсів",
|
"area_extensive": "Область досить велика і може вимагати значного часу та ресурсів",
|
||||||
"selection_confirmed": "Вибір підтверджено!",
|
"selection_confirmed": "Вибір підтверджено!",
|
||||||
"select_area_prompt": "Виберіть область на карті за допомогою інструментів.",
|
|
||||||
"unknown_error": "Unknown error",
|
"unknown_error": "Unknown error",
|
||||||
"license_and_credits": "License and Credits",
|
"license_and_credits": "License and Credits",
|
||||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "Тільки рельєф",
|
"mode_terrain_only": "Тільки рельєф",
|
||||||
"interior": "Генерація інтер'єру",
|
"interior": "Генерація інтер'єру",
|
||||||
"roof": "Генерація даху",
|
"roof": "Генерація даху",
|
||||||
"fillground": "Заповнити землю",
|
"fillground": "Заповнити землю"
|
||||||
"bedrock_use_java": "Використовуй Java для світів"
|
|
||||||
}
|
}
|
||||||
8
src/gui/locales/zh-CN.json
vendored
8
src/gui/locales/zh-CN.json
vendored
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"select_location": "选择位置",
|
||||||
|
"zoom_in_and_choose": "放大并使用矩形工具选择您的区域",
|
||||||
|
"select_world": "选择世界",
|
||||||
"choose_world": "选择世界",
|
"choose_world": "选择世界",
|
||||||
"no_world_selected": "未选择世界",
|
"no_world_selected": "未选择世界",
|
||||||
"start_generation": "开始生成",
|
"start_generation": "开始生成",
|
||||||
|
"progress": "进度",
|
||||||
"custom_selection_confirmed": "自定义选择已确认!",
|
"custom_selection_confirmed": "自定义选择已确认!",
|
||||||
"error_coordinates_out_of_range": "错误:坐标超出范围或顺序不正确(需要先纬度后经度)。",
|
"error_coordinates_out_of_range": "错误:坐标超出范围或顺序不正确(需要先纬度后经度)。",
|
||||||
"invalid_format": "格式无效。请使用 'lat,lng,lat,lng' 或 'lat lng lat lng'。",
|
"invalid_format": "格式无效。请使用 'lat,lng,lat,lng' 或 'lat lng lat lng'。",
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
"area_too_large": "该区域非常大,可能会超出典型的计算限制。",
|
"area_too_large": "该区域非常大,可能会超出典型的计算限制。",
|
||||||
"area_extensive": "该区域相当广泛,可能需要大量时间和资源。",
|
"area_extensive": "该区域相当广泛,可能需要大量时间和资源。",
|
||||||
"selection_confirmed": "选择已确认!",
|
"selection_confirmed": "选择已确认!",
|
||||||
"select_area_prompt": "使用工具在地图上选择一个区域。",
|
|
||||||
"unknown_error": "未知错误",
|
"unknown_error": "未知错误",
|
||||||
"license_and_credits": "许可证和致谢",
|
"license_and_credits": "许可证和致谢",
|
||||||
"placeholder_bbox": "格式: lat,lng,lat,lng",
|
"placeholder_bbox": "格式: lat,lng,lat,lng",
|
||||||
@@ -41,6 +44,5 @@
|
|||||||
"mode_terrain_only": "仅地形",
|
"mode_terrain_only": "仅地形",
|
||||||
"interior": "内部生成",
|
"interior": "内部生成",
|
||||||
"roof": "屋顶生成",
|
"roof": "屋顶生成",
|
||||||
"fillground": "填充地面",
|
"fillground": "填充地面"
|
||||||
"bedrock_use_java": "使用Java选择世界"
|
|
||||||
}
|
}
|
||||||
2
src/gui/maps.html
vendored
2
src/gui/maps.html
vendored
@@ -26,7 +26,7 @@
|
|||||||
<div id="search-container">
|
<div id="search-container">
|
||||||
<div id="search-box">
|
<div id="search-box">
|
||||||
<input type="text" id="city-search" placeholder="Search for a city..." autocomplete="off" />
|
<input type="text" id="city-search" placeholder="Search for a city..." autocomplete="off" />
|
||||||
<button id="search-btn" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg></button>
|
<button id="search-btn">🔍</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-results"></div>
|
<div id="search-results"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -1,24 +1,19 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
#[cfg(feature = "bedrock")]
|
|
||||||
mod bedrock_block_map;
|
|
||||||
mod block_definitions;
|
mod block_definitions;
|
||||||
mod bresenham;
|
mod bresenham;
|
||||||
mod clipping;
|
mod clipping;
|
||||||
mod colors;
|
mod colors;
|
||||||
mod coordinate_system;
|
mod coordinate_system;
|
||||||
mod data_processing;
|
mod data_processing;
|
||||||
mod deterministic_rng;
|
|
||||||
mod element_processing;
|
mod element_processing;
|
||||||
mod elevation_data;
|
mod elevation_data;
|
||||||
mod floodfill;
|
mod floodfill;
|
||||||
mod floodfill_cache;
|
|
||||||
mod ground;
|
mod ground;
|
||||||
mod map_renderer;
|
mod map_renderer;
|
||||||
mod map_transformation;
|
mod map_transformation;
|
||||||
mod osm_parser;
|
mod osm_parser;
|
||||||
mod parallel_processing;
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
mod progress;
|
mod progress;
|
||||||
mod retrieve_data;
|
mod retrieve_data;
|
||||||
@@ -26,7 +21,6 @@ mod retrieve_data;
|
|||||||
mod telemetry;
|
mod telemetry;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_utilities;
|
mod test_utilities;
|
||||||
mod unit_processing;
|
|
||||||
mod version_check;
|
mod version_check;
|
||||||
mod world_editor;
|
mod world_editor;
|
||||||
|
|
||||||
@@ -44,7 +38,6 @@ mod progress {
|
|||||||
pub fn emit_gui_error(_message: &str) {}
|
pub fn emit_gui_error(_message: &str) {}
|
||||||
pub fn emit_gui_progress_update(_progress: f64, _message: &str) {}
|
pub fn emit_gui_progress_update(_progress: f64, _message: &str) {}
|
||||||
pub fn emit_map_preview_ready() {}
|
pub fn emit_map_preview_ready() {}
|
||||||
pub fn emit_open_mcworld_file(_path: &str) {}
|
|
||||||
pub fn is_running_with_gui() -> bool {
|
pub fn is_running_with_gui() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -53,12 +46,6 @@ mod progress {
|
|||||||
use windows::Win32::System::Console::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS};
|
use windows::Win32::System::Console::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS};
|
||||||
|
|
||||||
fn run_cli() {
|
fn run_cli() {
|
||||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
|
||||||
floodfill_cache::configure_rayon_thread_pool(0.9);
|
|
||||||
|
|
||||||
// Clean up old cached elevation tiles on startup
|
|
||||||
elevation_data::cleanup_old_cached_tiles();
|
|
||||||
|
|
||||||
let version: &str = env!("CARGO_PKG_VERSION");
|
let version: &str = env!("CARGO_PKG_VERSION");
|
||||||
let repository: &str = env!("CARGO_PKG_REPOSITORY");
|
let repository: &str = env!("CARGO_PKG_REPOSITORY");
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use crate::coordinate_system::transformation::CoordTransformer;
|
|||||||
use crate::progress::emit_gui_progress_update;
|
use crate::progress::emit_gui_progress_update;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
// Raw data from OSM
|
// Raw data from OSM
|
||||||
|
|
||||||
@@ -29,18 +29,9 @@ struct OsmElement {
|
|||||||
pub members: Vec<OsmMember>,
|
pub members: Vec<OsmMember>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct OsmData {
|
struct OsmData {
|
||||||
elements: Vec<OsmElement>,
|
pub 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SplitOsmData {
|
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
|
// End raw data
|
||||||
|
|
||||||
// Normalized data that we can use
|
// Normalized data that we can use
|
||||||
@@ -116,7 +112,7 @@ pub enum ProcessedMemberRole {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ProcessedMember {
|
pub struct ProcessedMember {
|
||||||
pub role: ProcessedMemberRole,
|
pub role: ProcessedMemberRole,
|
||||||
pub way: Arc<ProcessedWay>,
|
pub way: ProcessedWay,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -168,7 +164,7 @@ impl ProcessedElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_osm_data(
|
pub fn parse_osm_data(
|
||||||
osm_data: OsmData,
|
json_data: Value,
|
||||||
bbox: LLBBox,
|
bbox: LLBBox,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
@@ -178,7 +174,7 @@ pub fn parse_osm_data(
|
|||||||
emit_gui_progress_update(5.0, "Parsing data...");
|
emit_gui_progress_update(5.0, "Parsing data...");
|
||||||
|
|
||||||
// Deserialize the JSON data into the OSMData structure
|
// 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)
|
let (coord_transformer, xzbbox) = CoordTransformer::llbbox_to_xzbbox(&bbox, scale)
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
@@ -193,7 +189,7 @@ pub fn parse_osm_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut nodes_map: HashMap<u64, ProcessedNode> = HashMap::new();
|
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();
|
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();
|
let tags = element.tags.clone().unwrap_or_default();
|
||||||
|
|
||||||
// Store unclipped way for relation assembly (clipping happens after ring merging)
|
// Store unclipped way for relation assembly (clipping happens after ring merging)
|
||||||
let way = Arc::new(ProcessedWay {
|
ways_map.insert(
|
||||||
id: element.id,
|
element.id,
|
||||||
tags,
|
ProcessedWay {
|
||||||
nodes,
|
id: element.id,
|
||||||
});
|
tags: tags.clone(),
|
||||||
ways_map.insert(element.id, Arc::clone(&way));
|
nodes: nodes.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Clip way nodes for standalone way processing (not relations)
|
// 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)
|
// Skip ways that are completely outside the bbox (empty after clipping)
|
||||||
if clipped_nodes.is_empty() {
|
if clipped_nodes.is_empty() {
|
||||||
@@ -259,8 +257,8 @@ pub fn parse_osm_data(
|
|||||||
|
|
||||||
let processed: ProcessedWay = ProcessedWay {
|
let processed: ProcessedWay = ProcessedWay {
|
||||||
id: element.id,
|
id: element.id,
|
||||||
tags: way.tags.clone(),
|
tags: tags.clone(),
|
||||||
nodes: clipped_nodes,
|
nodes: clipped_nodes.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
processed_elements.push(ProcessedElement::Way(processed));
|
processed_elements.push(ProcessedElement::Way(processed));
|
||||||
@@ -296,8 +294,8 @@ pub fn parse_osm_data(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if the way exists in ways_map
|
// Check if the way exists in ways_map
|
||||||
let way = match ways_map.get(&mem.r#ref) {
|
let way: ProcessedWay = match ways_map.get(&mem.r#ref) {
|
||||||
Some(w) => Arc::clone(w),
|
Some(w) => w.clone(),
|
||||||
None => {
|
None => {
|
||||||
// Way was likely filtered out because it was completely outside the bbox
|
// Way was likely filtered out because it was completely outside the bbox
|
||||||
return None;
|
return None;
|
||||||
@@ -313,11 +311,11 @@ pub fn parse_osm_data(
|
|||||||
if clipped_nodes.is_empty() {
|
if clipped_nodes.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Arc::new(ProcessedWay {
|
ProcessedWay {
|
||||||
id: way.id,
|
id: way.id,
|
||||||
tags: way.tags.clone(),
|
tags: way.tags,
|
||||||
nodes: clipped_nodes,
|
nodes: clipped_nodes,
|
||||||
})
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(ProcessedMember {
|
Some(ProcessedMember {
|
||||||
@@ -336,10 +334,7 @@ pub fn parse_osm_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_gui_progress_update(14.0, "");
|
emit_gui_progress_update(15.0, "");
|
||||||
|
|
||||||
drop(nodes_map);
|
|
||||||
drop(ways_map);
|
|
||||||
|
|
||||||
(processed_elements, xzbbox)
|
(processed_elements, xzbbox)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,476 +0,0 @@
|
|||||||
//! Parallel region processing for improved memory efficiency and CPU utilization.
|
|
||||||
//!
|
|
||||||
//! This module splits the world generation into processing units (1 Minecraft region each),
|
|
||||||
//! processes them in parallel, and flushes each region to disk immediately after completion.
|
|
||||||
//!
|
|
||||||
//! Key benefits:
|
|
||||||
//! - Memory usage reduced by ~90% (only active regions in memory)
|
|
||||||
//! - Multi-core CPU utilization
|
|
||||||
//! - Consistent results via deterministic RNG
|
|
||||||
|
|
||||||
use crate::coordinate_system::cartesian::xzbbox::rectangle::XZBBoxRect;
|
|
||||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
|
||||||
use crate::floodfill_cache::BuildingFootprintBitmap;
|
|
||||||
use crate::ground::Ground;
|
|
||||||
use crate::osm_parser::{ProcessedElement, ProcessedNode};
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::element_processing::highways::HighwayConnectivityMap;
|
|
||||||
|
|
||||||
/// Size of a Minecraft region in blocks (32 chunks × 16 blocks per chunk)
|
|
||||||
pub const REGION_BLOCKS: i32 = 512;
|
|
||||||
|
|
||||||
/// A processing unit representing a single Minecraft region to be generated.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct ProcessingUnit {
|
|
||||||
/// Region X coordinate (in region space, not block space)
|
|
||||||
pub region_x: i32,
|
|
||||||
/// Region Z coordinate (in region space, not block space)
|
|
||||||
pub region_z: i32,
|
|
||||||
|
|
||||||
/// Minecraft coordinate bounds for this unit (512×512 blocks)
|
|
||||||
pub min_x: i32,
|
|
||||||
pub max_x: i32,
|
|
||||||
pub min_z: i32,
|
|
||||||
pub max_z: i32,
|
|
||||||
|
|
||||||
/// Expanded bounds for element fetching (includes buffer for boundary elements)
|
|
||||||
pub fetch_min_x: i32,
|
|
||||||
pub fetch_max_x: i32,
|
|
||||||
pub fetch_min_z: i32,
|
|
||||||
pub fetch_max_z: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessingUnit {
|
|
||||||
/// Creates a new processing unit for a specific region.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn new(region_x: i32, region_z: i32, global_bbox: &XZBBox, buffer_blocks: i32) -> Self {
|
|
||||||
Self::new_batched(region_x, region_z, region_x, region_z, global_bbox, buffer_blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a processing unit spanning multiple regions (for batching).
|
|
||||||
pub fn new_batched(
|
|
||||||
start_region_x: i32, start_region_z: i32,
|
|
||||||
end_region_x: i32, end_region_z: i32,
|
|
||||||
global_bbox: &XZBBox, buffer_blocks: i32
|
|
||||||
) -> Self {
|
|
||||||
// Calculate block bounds for these regions
|
|
||||||
let min_x = start_region_x * REGION_BLOCKS;
|
|
||||||
let max_x = (end_region_x + 1) * REGION_BLOCKS - 1;
|
|
||||||
let min_z = start_region_z * REGION_BLOCKS;
|
|
||||||
let max_z = (end_region_z + 1) * REGION_BLOCKS - 1;
|
|
||||||
|
|
||||||
// Add buffer for fetch bounds, clamped to global bbox
|
|
||||||
let fetch_min_x = (min_x - buffer_blocks).max(global_bbox.min_x());
|
|
||||||
let fetch_max_x = (max_x + buffer_blocks).min(global_bbox.max_x());
|
|
||||||
let fetch_min_z = (min_z - buffer_blocks).max(global_bbox.min_z());
|
|
||||||
let fetch_max_z = (max_z + buffer_blocks).min(global_bbox.max_z());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
region_x: start_region_x,
|
|
||||||
region_z: start_region_z,
|
|
||||||
min_x,
|
|
||||||
max_x,
|
|
||||||
min_z,
|
|
||||||
max_z,
|
|
||||||
fetch_min_x,
|
|
||||||
fetch_max_x,
|
|
||||||
fetch_min_z,
|
|
||||||
fetch_max_z,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the XZBBox for this unit's actual processing bounds (not fetch bounds).
|
|
||||||
pub fn bbox(&self) -> XZBBox {
|
|
||||||
XZBBox::Rect(
|
|
||||||
XZBBoxRect::new(
|
|
||||||
XZPoint::new(self.min_x, self.min_z),
|
|
||||||
XZPoint::new(self.max_x, self.max_z),
|
|
||||||
)
|
|
||||||
.expect("Invalid unit bbox bounds"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the XZBBox for this unit's fetch bounds (includes buffer).
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn fetch_bbox(&self) -> XZBBox {
|
|
||||||
XZBBox::Rect(
|
|
||||||
XZBBoxRect::new(
|
|
||||||
XZPoint::new(self.fetch_min_x, self.fetch_min_z),
|
|
||||||
XZPoint::new(self.fetch_max_x, self.fetch_max_z),
|
|
||||||
)
|
|
||||||
.expect("Invalid unit fetch bbox bounds"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if a point is within this unit's fetch bounds.
|
|
||||||
#[inline]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn contains_fetch(&self, x: i32, z: i32) -> bool {
|
|
||||||
x >= self.fetch_min_x
|
|
||||||
&& x <= self.fetch_max_x
|
|
||||||
&& z >= self.fetch_min_z
|
|
||||||
&& z <= self.fetch_max_z
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if an element's bounding box intersects with this unit's fetch bounds.
|
|
||||||
pub fn intersects_element(&self, element: &ProcessedElement) -> bool {
|
|
||||||
let (min_x, max_x, min_z, max_z) = element_bbox(element);
|
|
||||||
|
|
||||||
// Check for intersection
|
|
||||||
!(max_x < self.fetch_min_x
|
|
||||||
|| min_x > self.fetch_max_x
|
|
||||||
|| max_z < self.fetch_min_z
|
|
||||||
|| min_z > self.fetch_max_z)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes the bounding box of an element.
|
|
||||||
fn element_bbox(element: &ProcessedElement) -> (i32, i32, i32, i32) {
|
|
||||||
match element {
|
|
||||||
ProcessedElement::Node(node) => (node.x, node.x, node.z, node.z),
|
|
||||||
ProcessedElement::Way(way) => way_bbox(&way.nodes),
|
|
||||||
ProcessedElement::Relation(rel) => {
|
|
||||||
let mut min_x = i32::MAX;
|
|
||||||
let mut max_x = i32::MIN;
|
|
||||||
let mut min_z = i32::MAX;
|
|
||||||
let mut max_z = i32::MIN;
|
|
||||||
|
|
||||||
for member in &rel.members {
|
|
||||||
let (mx, mxx, mz, mxz) = way_bbox(&member.way.nodes);
|
|
||||||
min_x = min_x.min(mx);
|
|
||||||
max_x = max_x.max(mxx);
|
|
||||||
min_z = min_z.min(mz);
|
|
||||||
max_z = max_z.max(mxz);
|
|
||||||
}
|
|
||||||
|
|
||||||
(min_x, max_x, min_z, max_z)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes the bounding box of a way's nodes.
|
|
||||||
fn way_bbox(nodes: &[ProcessedNode]) -> (i32, i32, i32, i32) {
|
|
||||||
if nodes.is_empty() {
|
|
||||||
return (0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut min_x = i32::MAX;
|
|
||||||
let mut max_x = i32::MIN;
|
|
||||||
let mut min_z = i32::MAX;
|
|
||||||
let mut max_z = i32::MIN;
|
|
||||||
|
|
||||||
for node in nodes {
|
|
||||||
min_x = min_x.min(node.x);
|
|
||||||
max_x = max_x.max(node.x);
|
|
||||||
min_z = min_z.min(node.z);
|
|
||||||
max_z = max_z.max(node.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
(min_x, max_x, min_z, max_z)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes all processing units for a given world bounding box.
|
|
||||||
/// With batch_size=1, creates one unit per region.
|
|
||||||
/// With batch_size=2, creates one unit per 2x2 = 4 regions, etc.
|
|
||||||
pub fn compute_processing_units(
|
|
||||||
global_bbox: &XZBBox,
|
|
||||||
buffer_blocks: i32,
|
|
||||||
batch_size: usize,
|
|
||||||
) -> Vec<ProcessingUnit> {
|
|
||||||
// Calculate which regions are covered by the bbox
|
|
||||||
let min_region_x = global_bbox.min_x() >> 9; // divide by 512
|
|
||||||
let max_region_x = global_bbox.max_x() >> 9;
|
|
||||||
let min_region_z = global_bbox.min_z() >> 9;
|
|
||||||
let max_region_z = global_bbox.max_z() >> 9;
|
|
||||||
|
|
||||||
let mut units = Vec::new();
|
|
||||||
|
|
||||||
// Batch size determines how many regions are grouped together
|
|
||||||
// batch_size=1 -> 1 region per unit
|
|
||||||
// batch_size=2 -> 2x2=4 regions per unit
|
|
||||||
let batch = batch_size.max(1) as i32;
|
|
||||||
|
|
||||||
// Create units grouped by batch_size
|
|
||||||
let mut rx = min_region_x;
|
|
||||||
while rx <= max_region_x {
|
|
||||||
let mut rz = min_region_z;
|
|
||||||
while rz <= max_region_z {
|
|
||||||
// Create a unit spanning batch_size regions in each direction
|
|
||||||
units.push(ProcessingUnit::new_batched(
|
|
||||||
rx, rz,
|
|
||||||
(rx + batch - 1).min(max_region_x),
|
|
||||||
(rz + batch - 1).min(max_region_z),
|
|
||||||
global_bbox,
|
|
||||||
buffer_blocks
|
|
||||||
));
|
|
||||||
rz += batch;
|
|
||||||
}
|
|
||||||
rx += batch;
|
|
||||||
}
|
|
||||||
|
|
||||||
units
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Distributes elements to processing units based on spatial intersection.
|
|
||||||
///
|
|
||||||
/// Each element is assigned to all units whose fetch bounds it intersects.
|
|
||||||
/// This ensures elements at boundaries are processed by all relevant units.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn distribute_elements_to_units<'a>(
|
|
||||||
elements: &'a [ProcessedElement],
|
|
||||||
units: &[ProcessingUnit],
|
|
||||||
) -> Vec<Vec<&'a ProcessedElement>> {
|
|
||||||
let mut unit_elements: Vec<Vec<&ProcessedElement>> = vec![Vec::new(); units.len()];
|
|
||||||
|
|
||||||
for element in elements {
|
|
||||||
for (i, unit) in units.iter().enumerate() {
|
|
||||||
if unit.intersects_element(element) {
|
|
||||||
unit_elements[i].push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unit_elements
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Distributes elements to processing units, returning indices instead of references.
|
|
||||||
///
|
|
||||||
/// This is useful when elements need to be shared across threads via Arc.
|
|
||||||
pub fn distribute_elements_to_units_indices(
|
|
||||||
elements: &[ProcessedElement],
|
|
||||||
units: &[ProcessingUnit],
|
|
||||||
) -> Vec<Vec<usize>> {
|
|
||||||
let mut unit_indices: Vec<Vec<usize>> = vec![Vec::new(); units.len()];
|
|
||||||
|
|
||||||
for (idx, element) in elements.iter().enumerate() {
|
|
||||||
for (i, unit) in units.iter().enumerate() {
|
|
||||||
if unit.intersects_element(element) {
|
|
||||||
unit_indices[i].push(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unit_indices
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global shared data that must be computed once and shared across all processing units.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct GlobalSharedData {
|
|
||||||
/// Ground/elevation data (must be consistent across boundaries)
|
|
||||||
pub ground: Arc<Ground>,
|
|
||||||
/// Building footprints bitmap (prevents trees inside buildings at boundaries)
|
|
||||||
pub building_footprints: Arc<BuildingFootprintBitmap>,
|
|
||||||
/// Highway connectivity map (for intersection detection)
|
|
||||||
pub highway_connectivity: Arc<HighwayConnectivityMap>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Statistics from parallel processing.
|
|
||||||
#[derive(Default)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct ProcessingStats {
|
|
||||||
pub total_units: u64,
|
|
||||||
pub completed_units: AtomicU64,
|
|
||||||
pub total_elements: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessingStats {
|
|
||||||
pub fn new(total_units: usize, total_elements: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
total_units: total_units as u64,
|
|
||||||
completed_units: AtomicU64::new(0),
|
|
||||||
total_elements: total_elements as u64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn increment_completed(&self) -> u64 {
|
|
||||||
self.completed_units.fetch_add(1, Ordering::SeqCst) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn progress_percentage(&self) -> f64 {
|
|
||||||
let completed = self.completed_units.load(Ordering::SeqCst);
|
|
||||||
if self.total_units == 0 {
|
|
||||||
100.0
|
|
||||||
} else {
|
|
||||||
(completed as f64 / self.total_units as f64) * 100.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clips an element to a unit's actual processing bounds (not fetch bounds).
|
|
||||||
///
|
|
||||||
/// Returns Some(clipped_element) if the element has any part within the unit's bounds,
|
|
||||||
/// or None if the element is completely outside.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn clip_element_to_unit(
|
|
||||||
element: &ProcessedElement,
|
|
||||||
unit: &ProcessingUnit,
|
|
||||||
) -> Option<ProcessedElement> {
|
|
||||||
match element {
|
|
||||||
ProcessedElement::Node(node) => {
|
|
||||||
// Nodes are either fully inside or outside
|
|
||||||
if node.x >= unit.min_x
|
|
||||||
&& node.x <= unit.max_x
|
|
||||||
&& node.z >= unit.min_z
|
|
||||||
&& node.z <= unit.max_z
|
|
||||||
{
|
|
||||||
Some(element.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProcessedElement::Way(way) => {
|
|
||||||
// For ways, we keep the full way but let the WorldEditor handle bounds checking
|
|
||||||
// This ensures deterministic RNG produces the same results regardless of clipping
|
|
||||||
// The WorldEditor.set_block() already checks bounds via xzbbox.contains()
|
|
||||||
let (min_x, max_x, min_z, max_z) = way_bbox(&way.nodes);
|
|
||||||
|
|
||||||
// Check if way intersects unit bounds
|
|
||||||
if max_x < unit.min_x
|
|
||||||
|| min_x > unit.max_x
|
|
||||||
|| max_z < unit.min_z
|
|
||||||
|| min_z > unit.max_z
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(element.clone())
|
|
||||||
}
|
|
||||||
ProcessedElement::Relation(_rel) => {
|
|
||||||
// For relations, similar approach - keep full relation for consistent processing
|
|
||||||
let (min_x, max_x, min_z, max_z) = element_bbox(element);
|
|
||||||
|
|
||||||
// Check if relation intersects unit bounds
|
|
||||||
if max_x < unit.min_x
|
|
||||||
|| min_x > unit.max_x
|
|
||||||
|| max_z < unit.min_z
|
|
||||||
|| min_z > unit.max_z
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(element.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clips a collection of elements to a unit's bounds.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn clip_elements_to_unit(
|
|
||||||
elements: &[&ProcessedElement],
|
|
||||||
unit: &ProcessingUnit,
|
|
||||||
) -> Vec<ProcessedElement> {
|
|
||||||
elements
|
|
||||||
.iter()
|
|
||||||
.filter_map(|e| clip_element_to_unit(e, unit))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the number of parallel threads to use.
|
|
||||||
pub fn calculate_parallel_threads(requested: usize) -> usize {
|
|
||||||
let available = std::thread::available_parallelism()
|
|
||||||
.map(|n| n.get())
|
|
||||||
.unwrap_or(4);
|
|
||||||
|
|
||||||
if requested == 0 {
|
|
||||||
// Default: use all but one core
|
|
||||||
available.saturating_sub(1).max(1)
|
|
||||||
} else {
|
|
||||||
// Use requested amount, capped at available
|
|
||||||
requested.min(available).max(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for parallel processing
|
|
||||||
pub struct ParallelConfig {
|
|
||||||
/// Number of threads to use (0 = auto, uses available - 1)
|
|
||||||
pub num_threads: usize,
|
|
||||||
/// Buffer in blocks around each unit for element fetching
|
|
||||||
pub buffer_blocks: i32,
|
|
||||||
/// Whether to use parallel processing (false = sequential for debugging)
|
|
||||||
pub enabled: bool,
|
|
||||||
/// Number of regions to batch per unit (1 = single region, 2 = 2x2 = 4 regions)
|
|
||||||
pub region_batch_size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ParallelConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
num_threads: 0, // Auto-detect
|
|
||||||
buffer_blocks: 64, // Buffer for boundary elements
|
|
||||||
enabled: true,
|
|
||||||
region_batch_size: 2, // 2x2=4 regions per unit - optimal balance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParallelConfig {
|
|
||||||
pub fn sequential() -> Self {
|
|
||||||
Self {
|
|
||||||
enabled: false,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::coordinate_system::cartesian::xzbbox::rectangle::XZBBoxRect;
|
|
||||||
|
|
||||||
fn make_test_bbox(min_x: i32, max_x: i32, min_z: i32, max_z: i32) -> XZBBox {
|
|
||||||
XZBBox::Rect(
|
|
||||||
XZBBoxRect::new(XZPoint::new(min_x, min_z), XZPoint::new(max_x, max_z)).unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_processing_unit_creation() {
|
|
||||||
let global_bbox = make_test_bbox(0, 1023, 0, 1023);
|
|
||||||
let unit = ProcessingUnit::new(0, 0, &global_bbox, 64);
|
|
||||||
|
|
||||||
assert_eq!(unit.region_x, 0);
|
|
||||||
assert_eq!(unit.region_z, 0);
|
|
||||||
assert_eq!(unit.min_x, 0);
|
|
||||||
assert_eq!(unit.max_x, 511);
|
|
||||||
assert_eq!(unit.min_z, 0);
|
|
||||||
assert_eq!(unit.max_z, 511);
|
|
||||||
// Fetch bounds should be clamped to global bbox
|
|
||||||
assert_eq!(unit.fetch_min_x, 0); // Can't go below 0
|
|
||||||
assert_eq!(unit.fetch_max_x, 575); // 511 + 64
|
|
||||||
assert_eq!(unit.fetch_min_z, 0);
|
|
||||||
assert_eq!(unit.fetch_max_z, 575);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_compute_processing_units() {
|
|
||||||
// A 2x2 region area
|
|
||||||
let global_bbox = make_test_bbox(0, 1023, 0, 1023);
|
|
||||||
let units = compute_processing_units(&global_bbox, 64, 1);
|
|
||||||
|
|
||||||
assert_eq!(units.len(), 4); // 2x2 = 4 regions
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_compute_processing_units_batched() {
|
|
||||||
// A 2x2 region area with batch size 2 should create 1 unit
|
|
||||||
let global_bbox = make_test_bbox(0, 1023, 0, 1023);
|
|
||||||
let units = compute_processing_units(&global_bbox, 64, 2);
|
|
||||||
|
|
||||||
assert_eq!(units.len(), 1); // All 4 regions in one batch
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_calculate_parallel_threads() {
|
|
||||||
// Test default (0 = available - 1)
|
|
||||||
let threads = calculate_parallel_threads(0);
|
|
||||||
assert!(threads >= 1);
|
|
||||||
|
|
||||||
// Test explicit request
|
|
||||||
let threads = calculate_parallel_threads(2);
|
|
||||||
assert!(threads >= 1 && threads <= 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
use crate::coordinate_system::geographic::LLBBox;
|
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 crate::progress::{emit_gui_error, emit_gui_progress_update, is_running_with_gui};
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
use crate::telemetry::{send_log, LogLevel};
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use reqwest::blocking::ClientBuilder;
|
use reqwest::blocking::ClientBuilder;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, BufReader, Cursor, Write};
|
use std::io::{self, BufReader, Write};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -38,22 +34,19 @@ fn download_with_reqwest(url: &str, query: &str) -> Result<String, Box<dyn std::
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.is_timeout() {
|
if e.is_timeout() {
|
||||||
let msg = "Request timed out. Try selecting a smaller area.";
|
eprintln!(
|
||||||
eprintln!("{}", format!("Error! {msg}").red().bold());
|
"{}",
|
||||||
Err(msg.into())
|
"Error! Request timed out. Try selecting a smaller area."
|
||||||
} else if e.is_connect() {
|
.red()
|
||||||
let msg = "No internet connection.";
|
.bold()
|
||||||
eprintln!("{}", format!("Error! {msg}").red().bold());
|
|
||||||
Err(msg.into())
|
|
||||||
} else {
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
send_log(
|
|
||||||
LogLevel::Error,
|
|
||||||
&format!("Request error in download_with_reqwest: {e}"),
|
|
||||||
);
|
);
|
||||||
|
emit_gui_error("Request timed out. Try selecting a smaller area.");
|
||||||
|
} else {
|
||||||
eprintln!("{}", format!("Error! {e:.52}").red().bold());
|
eprintln!("{}", format!("Error! {e:.52}").red().bold());
|
||||||
Err(format!("{e:.52}").into())
|
emit_gui_error(&format!("{:.52}", e.to_string()));
|
||||||
}
|
}
|
||||||
|
// Always propagate errors
|
||||||
|
Err(e.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,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());
|
println!("{} Loading data from file...", "[1/7]".bold());
|
||||||
emit_gui_progress_update(1.0, "Loading data from file...");
|
emit_gui_progress_update(1.0, "Loading data from file...");
|
||||||
|
|
||||||
let file: File = File::open(file)?;
|
let file: File = File::open(file)?;
|
||||||
let reader: BufReader<File> = BufReader::new(file);
|
let reader: BufReader<File> = BufReader::new(file);
|
||||||
let mut deserializer = serde_json::Deserializer::from_reader(reader);
|
let data: Value = serde_json::from_reader(reader)?;
|
||||||
let data: OsmData = OsmData::deserialize(&mut deserializer)?;
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +95,7 @@ pub fn fetch_data_from_overpass(
|
|||||||
debug: bool,
|
debug: bool,
|
||||||
download_method: &str,
|
download_method: &str,
|
||||||
save_file: Option<&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());
|
println!("{} Fetching data...", "[1/7]".bold());
|
||||||
emit_gui_progress_update(1.0, "Fetching data...");
|
emit_gui_progress_update(1.0, "Fetching data...");
|
||||||
|
|
||||||
@@ -190,12 +182,14 @@ pub fn fetch_data_from_overpass(
|
|||||||
println!("API response saved to: {save_file}");
|
println!("API response saved to: {save_file}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut deserializer =
|
let data: Value = serde_json::from_str(&response)?;
|
||||||
serde_json::Deserializer::from_reader(Cursor::new(response.as_bytes()));
|
|
||||||
let data: OsmData = OsmData::deserialize(&mut deserializer)?;
|
|
||||||
|
|
||||||
if data.is_empty() {
|
if data["elements"]
|
||||||
if let Some(remark) = data.remark.as_deref() {
|
.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
|
// Check if the remark mentions memory or other runtime errors
|
||||||
if remark.contains("runtime error") && remark.contains("out of memory") {
|
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());
|
eprintln!("{}", "Error! The query ran out of memory on the Overpass API server. Try using a smaller area.".red().bold());
|
||||||
@@ -217,7 +211,7 @@ pub fn fetch_data_from_overpass(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
println!("Additional debug information: {data:?}");
|
println!("Additional debug information: {data}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_running_with_gui() {
|
if !is_running_with_gui() {
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ use crate::retrieve_data;
|
|||||||
// this is copied from main.rs
|
// this is copied from main.rs
|
||||||
pub fn generate_example(llbbox: LLBBox) -> (XZBBox, Vec<ProcessedElement>) {
|
pub fn generate_example(llbbox: LLBBox) -> (XZBBox, Vec<ProcessedElement>) {
|
||||||
// Fetch data
|
// Fetch data
|
||||||
let raw_data = retrieve_data::fetch_data_from_overpass(llbbox, false, "requests", None)
|
let raw_data: serde_json::Value =
|
||||||
.expect("Failed to fetch data");
|
retrieve_data::fetch_data_from_overpass(llbbox, false, "requests", None)
|
||||||
|
.expect("Failed to fetch data");
|
||||||
|
|
||||||
// Parse raw data
|
// Parse raw data
|
||||||
let (mut parsed_elements, xzbbox) = osm_parser::parse_osm_data(raw_data, llbbox, 1.0, false);
|
let (mut parsed_elements, xzbbox) = osm_parser::parse_osm_data(raw_data, llbbox, 1.0, false);
|
||||||
|
|||||||
@@ -1,328 +0,0 @@
|
|||||||
//! Per-unit processing logic for parallel world generation.
|
|
||||||
//!
|
|
||||||
//! This module contains the functions that process a single region unit,
|
|
||||||
//! generating all the elements within that unit's bounds.
|
|
||||||
|
|
||||||
use crate::args::Args;
|
|
||||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
|
||||||
use crate::coordinate_system::cartesian::XZBBox;
|
|
||||||
use crate::coordinate_system::geographic::LLBBox;
|
|
||||||
use crate::data_processing::MIN_Y;
|
|
||||||
use crate::element_processing::*;
|
|
||||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
|
||||||
use crate::ground::Ground;
|
|
||||||
use crate::osm_parser::ProcessedElement;
|
|
||||||
use crate::parallel_processing::ProcessingUnit;
|
|
||||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::element_processing::highways::HighwayConnectivityMap;
|
|
||||||
|
|
||||||
/// Shared data for unit processing - passed by reference to each unit
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct SharedProcessingData {
|
|
||||||
pub ground: Arc<Ground>,
|
|
||||||
pub highway_connectivity: Arc<HighwayConnectivityMap>,
|
|
||||||
pub building_footprints: Arc<BuildingFootprintBitmap>,
|
|
||||||
pub floodfill_cache: Arc<FloodFillCache>,
|
|
||||||
pub llbbox: LLBBox,
|
|
||||||
pub world_dir: PathBuf,
|
|
||||||
pub format: WorldFormat,
|
|
||||||
pub level_name: Option<String>,
|
|
||||||
pub terrain_enabled: bool,
|
|
||||||
pub ground_level: i32,
|
|
||||||
pub fill_ground: bool,
|
|
||||||
pub interior: bool,
|
|
||||||
pub roof: bool,
|
|
||||||
pub debug: bool,
|
|
||||||
pub timeout: Option<std::time::Duration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process a single unit with element references (no cloning).
|
|
||||||
/// The caller is responsible for saving and dropping the editor to free memory.
|
|
||||||
pub fn process_unit_refs<'a>(
|
|
||||||
unit: &ProcessingUnit,
|
|
||||||
elements: &[&ProcessedElement],
|
|
||||||
shared: &SharedProcessingData,
|
|
||||||
unit_bbox: &'a XZBBox,
|
|
||||||
args: &Args,
|
|
||||||
) -> WorldEditor<'a> {
|
|
||||||
// Create a WorldEditor for just this unit's bounds
|
|
||||||
let mut editor = WorldEditor::new_with_format_and_name(
|
|
||||||
shared.world_dir.clone(),
|
|
||||||
unit_bbox,
|
|
||||||
shared.llbbox,
|
|
||||||
shared.format,
|
|
||||||
shared.level_name.clone(),
|
|
||||||
None, // Spawn point not set per-unit
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set ground reference for elevation-aware block placement
|
|
||||||
editor.set_ground(Arc::clone(&shared.ground));
|
|
||||||
|
|
||||||
// Process all elements for this unit
|
|
||||||
for element in elements {
|
|
||||||
process_element(
|
|
||||||
&mut editor,
|
|
||||||
element,
|
|
||||||
&shared.highway_connectivity,
|
|
||||||
&shared.floodfill_cache,
|
|
||||||
&shared.building_footprints,
|
|
||||||
unit_bbox,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate ground layer for this unit
|
|
||||||
generate_ground_for_unit(&mut editor, unit, shared, args);
|
|
||||||
|
|
||||||
editor
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process a single unit and return the WorldEditor with blocks placed.
|
|
||||||
/// The caller is responsible for saving and dropping the editor to free memory.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn process_unit<'a>(
|
|
||||||
unit: &ProcessingUnit,
|
|
||||||
elements: &[ProcessedElement],
|
|
||||||
shared: &SharedProcessingData,
|
|
||||||
unit_bbox: &'a XZBBox,
|
|
||||||
args: &Args,
|
|
||||||
) -> WorldEditor<'a> {
|
|
||||||
// Create a WorldEditor for just this unit's bounds
|
|
||||||
let mut editor = WorldEditor::new_with_format_and_name(
|
|
||||||
shared.world_dir.clone(),
|
|
||||||
unit_bbox,
|
|
||||||
shared.llbbox,
|
|
||||||
shared.format,
|
|
||||||
shared.level_name.clone(),
|
|
||||||
None, // Spawn point not set per-unit
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set ground reference for elevation-aware block placement
|
|
||||||
editor.set_ground(Arc::clone(&shared.ground));
|
|
||||||
|
|
||||||
// Process all elements for this unit
|
|
||||||
for element in elements {
|
|
||||||
process_element(
|
|
||||||
&mut editor,
|
|
||||||
element,
|
|
||||||
&shared.highway_connectivity,
|
|
||||||
&shared.floodfill_cache,
|
|
||||||
&shared.building_footprints,
|
|
||||||
unit_bbox,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate ground layer for this unit
|
|
||||||
generate_ground_for_unit(&mut editor, unit, shared, args);
|
|
||||||
|
|
||||||
editor
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process a single element, dispatching to the appropriate generator
|
|
||||||
fn process_element(
|
|
||||||
editor: &mut WorldEditor,
|
|
||||||
element: &ProcessedElement,
|
|
||||||
highway_connectivity: &HighwayConnectivityMap,
|
|
||||||
flood_fill_cache: &FloodFillCache,
|
|
||||||
building_footprints: &BuildingFootprintBitmap,
|
|
||||||
xzbbox: &XZBBox,
|
|
||||||
args: &Args,
|
|
||||||
) {
|
|
||||||
match element {
|
|
||||||
ProcessedElement::Way(way) => {
|
|
||||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
|
||||||
buildings::generate_buildings(editor, way, args, None, flood_fill_cache);
|
|
||||||
} else if way.tags.contains_key("highway") {
|
|
||||||
highways::generate_highways(
|
|
||||||
editor,
|
|
||||||
element,
|
|
||||||
args,
|
|
||||||
highway_connectivity,
|
|
||||||
flood_fill_cache,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("landuse") {
|
|
||||||
landuse::generate_landuse(
|
|
||||||
editor,
|
|
||||||
way,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("natural") {
|
|
||||||
natural::generate_natural(
|
|
||||||
editor,
|
|
||||||
element,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("amenity") {
|
|
||||||
amenities::generate_amenities(editor, element, args, flood_fill_cache);
|
|
||||||
} else if way.tags.contains_key("leisure") {
|
|
||||||
leisure::generate_leisure(
|
|
||||||
editor,
|
|
||||||
way,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
} else if way.tags.contains_key("barrier") {
|
|
||||||
barriers::generate_barriers(editor, element);
|
|
||||||
} else if let Some(val) = way.tags.get("waterway") {
|
|
||||||
if val == "dock" {
|
|
||||||
water_areas::generate_water_area_from_way(editor, way, xzbbox);
|
|
||||||
} else {
|
|
||||||
waterways::generate_waterways(editor, way);
|
|
||||||
}
|
|
||||||
} else if way.tags.contains_key("bridge") {
|
|
||||||
// bridges::generate_bridges(editor, way, ground_level); // TODO FIX
|
|
||||||
} else if way.tags.contains_key("railway") {
|
|
||||||
railways::generate_railways(editor, way);
|
|
||||||
} else if way.tags.contains_key("roller_coaster") {
|
|
||||||
railways::generate_roller_coaster(editor, way);
|
|
||||||
} else if way.tags.contains_key("aeroway") || way.tags.contains_key("area:aeroway") {
|
|
||||||
highways::generate_aeroway(editor, way, args);
|
|
||||||
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
|
||||||
highways::generate_siding(editor, way);
|
|
||||||
} else if way.tags.contains_key("man_made") {
|
|
||||||
man_made::generate_man_made(editor, element, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProcessedElement::Node(node) => {
|
|
||||||
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
|
|
||||||
doors::generate_doors(editor, node);
|
|
||||||
} else if node.tags.contains_key("natural")
|
|
||||||
&& node.tags.get("natural") == Some(&"tree".to_string())
|
|
||||||
{
|
|
||||||
natural::generate_natural(
|
|
||||||
editor,
|
|
||||||
element,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
} else if node.tags.contains_key("amenity") {
|
|
||||||
amenities::generate_amenities(editor, element, args, flood_fill_cache);
|
|
||||||
} else if node.tags.contains_key("barrier") {
|
|
||||||
barriers::generate_barrier_nodes(editor, node);
|
|
||||||
} else if node.tags.contains_key("highway") {
|
|
||||||
highways::generate_highways(
|
|
||||||
editor,
|
|
||||||
element,
|
|
||||||
args,
|
|
||||||
highway_connectivity,
|
|
||||||
flood_fill_cache,
|
|
||||||
);
|
|
||||||
} else if node.tags.contains_key("tourism") {
|
|
||||||
tourisms::generate_tourisms(editor, node);
|
|
||||||
} else if node.tags.contains_key("man_made") {
|
|
||||||
man_made::generate_man_made_nodes(editor, node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProcessedElement::Relation(rel) => {
|
|
||||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
|
||||||
buildings::generate_building_from_relation(editor, rel, args, flood_fill_cache);
|
|
||||||
} else if rel.tags.contains_key("water")
|
|
||||||
|| rel
|
|
||||||
.tags
|
|
||||||
.get("natural")
|
|
||||||
.map(|val| val == "water" || val == "bay")
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
water_areas::generate_water_areas_from_relation(editor, rel, xzbbox);
|
|
||||||
} else if rel.tags.contains_key("natural") {
|
|
||||||
natural::generate_natural_from_relation(
|
|
||||||
editor,
|
|
||||||
rel,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
} else if rel.tags.contains_key("landuse") {
|
|
||||||
landuse::generate_landuse_from_relation(
|
|
||||||
editor,
|
|
||||||
rel,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
|
||||||
leisure::generate_leisure_from_relation(
|
|
||||||
editor,
|
|
||||||
rel,
|
|
||||||
args,
|
|
||||||
flood_fill_cache,
|
|
||||||
building_footprints,
|
|
||||||
);
|
|
||||||
} else if rel.tags.contains_key("man_made") {
|
|
||||||
man_made::generate_man_made(editor, element, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate ground layer (grass, dirt, bedrock) for a unit
|
|
||||||
fn generate_ground_for_unit(
|
|
||||||
editor: &mut WorldEditor,
|
|
||||||
unit: &ProcessingUnit,
|
|
||||||
shared: &SharedProcessingData,
|
|
||||||
_args: &Args,
|
|
||||||
) {
|
|
||||||
let terrain_enabled = shared.terrain_enabled;
|
|
||||||
let ground_level = shared.ground_level;
|
|
||||||
|
|
||||||
// Process chunk by chunk within this unit for cache locality
|
|
||||||
let min_chunk_x = unit.min_x >> 4;
|
|
||||||
let max_chunk_x = unit.max_x >> 4;
|
|
||||||
let min_chunk_z = unit.min_z >> 4;
|
|
||||||
let max_chunk_z = unit.max_z >> 4;
|
|
||||||
|
|
||||||
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 unit bounds
|
|
||||||
let chunk_min_x = (chunk_x << 4).max(unit.min_x);
|
|
||||||
let chunk_max_x = ((chunk_x << 4) + 15).min(unit.max_x);
|
|
||||||
let chunk_min_z = (chunk_z << 4).max(unit.min_z);
|
|
||||||
let chunk_max_z = ((chunk_z << 4) + 15).min(unit.max_z);
|
|
||||||
|
|
||||||
for x in chunk_min_x..=chunk_max_x {
|
|
||||||
for z in chunk_min_z..=chunk_max_z {
|
|
||||||
let ground_y = if terrain_enabled {
|
|
||||||
editor.get_ground_level(x, z)
|
|
||||||
} else {
|
|
||||||
ground_level
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add default dirt and grass layer if there isn't a stone layer already
|
|
||||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
|
||||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
|
||||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
|
||||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill underground with stone if enabled
|
|
||||||
if shared.fill_ground {
|
|
||||||
editor.fill_blocks_absolute(
|
|
||||||
STONE,
|
|
||||||
x,
|
|
||||||
MIN_Y + 1,
|
|
||||||
z,
|
|
||||||
x,
|
|
||||||
ground_y - 3,
|
|
||||||
z,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate bedrock at MIN_Y
|
|
||||||
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1030
src/world_editor.rs
Normal file
1030
src/world_editor.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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, Clone)]
|
|
||||||
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, Clone)]
|
|
||||||
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, Clone)]
|
|
||||||
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: format!("{}:{}", block.namespace(), block.name()),
|
|
||||||
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(§ion_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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
//! Java Edition Anvil format world saving.
|
|
||||||
//!
|
|
||||||
//! This module handles saving worlds in the Java Edition Anvil (.mca) format.
|
|
||||||
|
|
||||||
use super::common::{Chunk, ChunkToModify, Section};
|
|
||||||
use super::WorldEditor;
|
|
||||||
use crate::block_definitions::GRASS_BLOCK;
|
|
||||||
use crate::progress::emit_gui_progress_update;
|
|
||||||
use colored::Colorize;
|
|
||||||
use fastanvil::Region;
|
|
||||||
use fastnbt::Value;
|
|
||||||
use fnv::FnvHashMap;
|
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
|
||||||
use rayon::prelude::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::sync::{atomic::{AtomicU64, Ordering}, OnceLock};
|
|
||||||
|
|
||||||
/// Cached base chunk sections (grass at Y=-62)
|
|
||||||
/// Only computed once and reused for all empty chunks
|
|
||||||
static BASE_CHUNK_SECTIONS: OnceLock<Vec<Section>> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Get or create the cached base chunk sections
|
|
||||||
fn get_base_chunk_sections() -> &'static [Section] {
|
|
||||||
BASE_CHUNK_SECTIONS.get_or_init(|| {
|
|
||||||
let mut chunk = ChunkToModify::default();
|
|
||||||
for x in 0..16 {
|
|
||||||
for z in 0..16 {
|
|
||||||
chunk.set_block(x, -62, z, GRASS_BLOCK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chunk.sections().collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
use crate::telemetry::{send_log, LogLevel};
|
|
||||||
|
|
||||||
impl<'a> WorldEditor<'a> {
|
|
||||||
/// Creates a region file for the given region coordinates.
|
|
||||||
pub(super) fn create_region(&self, region_x: i32, region_z: i32) -> Region<File> {
|
|
||||||
let region_dir = self.world_dir.join("region");
|
|
||||||
let out_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
|
||||||
|
|
||||||
// Ensure region directory exists before creating region files
|
|
||||||
std::fs::create_dir_all(®ion_dir).expect("Failed to create region directory");
|
|
||||||
|
|
||||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
|
|
||||||
|
|
||||||
let mut region_file: File = File::options()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(&out_path)
|
|
||||||
.expect("Failed to open region file");
|
|
||||||
|
|
||||||
region_file
|
|
||||||
.write_all(REGION_TEMPLATE)
|
|
||||||
.expect("Could not write region template");
|
|
||||||
|
|
||||||
Region::from_stream(region_file).expect("Failed to load region")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to create a base chunk with grass blocks at Y -62
|
|
||||||
/// Uses cached sections for efficiency - only serialization happens per chunk
|
|
||||||
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
|
|
||||||
// Use cached sections (computed once)
|
|
||||||
let sections = get_base_chunk_sections();
|
|
||||||
|
|
||||||
let chunk_data = Chunk {
|
|
||||||
sections: sections.to_vec(),
|
|
||||||
x_pos: abs_chunk_x,
|
|
||||||
z_pos: abs_chunk_z,
|
|
||||||
is_light_on: 0,
|
|
||||||
other: FnvHashMap::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let level_data = create_level_wrapper(&chunk_data);
|
|
||||||
let mut ser_buffer = Vec::with_capacity(8192);
|
|
||||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
|
||||||
|
|
||||||
(ser_buffer, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves the world in Java Edition Anvil format.
|
|
||||||
///
|
|
||||||
/// Uses parallel processing with rayon for fast region saving.
|
|
||||||
pub(super) fn save_java(&mut self) {
|
|
||||||
println!("{} Saving world...", "[7/7]".bold());
|
|
||||||
emit_gui_progress_update(90.0, "Saving world...");
|
|
||||||
|
|
||||||
// Save metadata with error handling
|
|
||||||
if let Err(e) = self.save_metadata() {
|
|
||||||
eprintln!("Failed to save world metadata: {}", e);
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
send_log(LogLevel::Warning, "Failed to save world metadata.");
|
|
||||||
// Continue with world saving even if metadata fails
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_regions = self.world.regions.len() as u64;
|
|
||||||
let save_pb = ProgressBar::new(total_regions);
|
|
||||||
save_pb.set_style(
|
|
||||||
ProgressStyle::default_bar()
|
|
||||||
.template(
|
|
||||||
"{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} regions ({eta})",
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.progress_chars("█▓░"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let regions_processed = AtomicU64::new(0);
|
|
||||||
|
|
||||||
self.world
|
|
||||||
.regions
|
|
||||||
.par_iter()
|
|
||||||
.for_each(|((region_x, region_z), region_to_modify)| {
|
|
||||||
self.save_single_region(*region_x, *region_z, region_to_modify);
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
let regions_done = regions_processed.fetch_add(1, Ordering::SeqCst) + 1;
|
|
||||||
|
|
||||||
// Update progress at regular intervals (every ~10% or at least every 10 regions)
|
|
||||||
let update_interval = (total_regions / 10).max(1);
|
|
||||||
if regions_done.is_multiple_of(update_interval) || regions_done == total_regions {
|
|
||||||
let progress = 90.0 + (regions_done as f64 / total_regions as f64) * 9.0;
|
|
||||||
emit_gui_progress_update(progress, "Saving world...");
|
|
||||||
}
|
|
||||||
|
|
||||||
save_pb.inc(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
save_pb.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves the world silently without progress messages (for parallel unit processing).
|
|
||||||
pub(super) fn save_java_silent(&mut self) {
|
|
||||||
// Save all regions without progress output
|
|
||||||
// With batch_size=2, each unit should have at most 4 regions (2x2)
|
|
||||||
let region_count = self.world.regions.len();
|
|
||||||
if region_count > 4 {
|
|
||||||
eprintln!("BUG: Unit has {} regions (max expected: 4 for batch_size=2)", region_count);
|
|
||||||
}
|
|
||||||
for ((region_x, region_z), region_to_modify) in &self.world.regions {
|
|
||||||
self.save_single_region(*region_x, *region_z, region_to_modify);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves a single region to disk.
|
|
||||||
///
|
|
||||||
/// This is extracted to allow streaming mode to save and release regions one at a time.
|
|
||||||
fn save_single_region(
|
|
||||||
&self,
|
|
||||||
region_x: i32,
|
|
||||||
region_z: i32,
|
|
||||||
region_to_modify: &super::common::RegionToModify,
|
|
||||||
) {
|
|
||||||
let mut region = self.create_region(region_x, region_z);
|
|
||||||
let mut ser_buffer = Vec::with_capacity(8192);
|
|
||||||
|
|
||||||
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
|
||||||
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
|
||||||
// Create new chunk directly - we're writing to a fresh region file
|
|
||||||
// so there's no existing data to preserve
|
|
||||||
let chunk = Chunk {
|
|
||||||
sections: chunk_to_modify.sections().collect(),
|
|
||||||
x_pos: chunk_x + (region_x * 32),
|
|
||||||
z_pos: chunk_z + (region_z * 32),
|
|
||||||
is_light_on: 0,
|
|
||||||
other: chunk_to_modify.other.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 within world bounds exist
|
|
||||||
// Only write base chunks for chunks that are within the actual world bounding box
|
|
||||||
let world_min_chunk_x = self.xzbbox.min_x() >> 4;
|
|
||||||
let world_max_chunk_x = self.xzbbox.max_x() >> 4;
|
|
||||||
let world_min_chunk_z = self.xzbbox.min_z() >> 4;
|
|
||||||
let world_max_chunk_z = self.xzbbox.max_z() >> 4;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Skip chunks outside the world bounding box
|
|
||||||
if abs_chunk_x < world_min_chunk_x || abs_chunk_x > world_max_chunk_x ||
|
|
||||||
abs_chunk_z < world_min_chunk_z || abs_chunk_z > world_max_chunk_z {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 but is within bounds, create it with base layer
|
|
||||||
if !chunk_exists {
|
|
||||||
let (ser_buffer, _) = Self::create_base_chunk(abs_chunk_x, abs_chunk_z);
|
|
||||||
region
|
|
||||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to get entity coordinates
|
|
||||||
#[inline]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
|
|
||||||
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
|
|
||||||
*x
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let y = if let Value::Int(y) = entity.get("y").unwrap_or(&Value::Int(0)) {
|
|
||||||
*y
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
|
|
||||||
*z
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
(x, y, z)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a Level wrapper for chunk data (Java Edition format)
|
|
||||||
#[inline]
|
|
||||||
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
|
||||||
HashMap::from([(
|
|
||||||
"Level".to_string(),
|
|
||||||
Value::Compound(HashMap::from([
|
|
||||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
|
||||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
|
||||||
(
|
|
||||||
"isLightOn".to_string(),
|
|
||||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"sections".to_string(),
|
|
||||||
Value::List(
|
|
||||||
chunk
|
|
||||||
.sections
|
|
||||||
.iter()
|
|
||||||
.map(|section| {
|
|
||||||
let mut block_states = HashMap::from([(
|
|
||||||
"palette".to_string(),
|
|
||||||
Value::List(
|
|
||||||
section
|
|
||||||
.block_states
|
|
||||||
.palette
|
|
||||||
.iter()
|
|
||||||
.map(|item| {
|
|
||||||
let mut palette_item = HashMap::from([(
|
|
||||||
"Name".to_string(),
|
|
||||||
Value::String(item.name.clone()),
|
|
||||||
)]);
|
|
||||||
if let Some(props) = &item.properties {
|
|
||||||
palette_item.insert(
|
|
||||||
"Properties".to_string(),
|
|
||||||
props.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Value::Compound(palette_item)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
)]);
|
|
||||||
|
|
||||||
// Only add the `data` attribute if it's non-empty
|
|
||||||
// to maintain compatibility with third-party tools like Dynmap
|
|
||||||
if let Some(data) = §ion.block_states.data {
|
|
||||||
if !data.is_empty() {
|
|
||||||
block_states.insert(
|
|
||||||
"data".to_string(),
|
|
||||||
Value::LongArray(data.to_owned()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Value::Compound(HashMap::from([
|
|
||||||
("Y".to_string(), Value::Byte(section.y)),
|
|
||||||
("block_states".to_string(), Value::Compound(block_states)),
|
|
||||||
]))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
])),
|
|
||||||
)])
|
|
||||||
}
|
|
||||||
@@ -1,613 +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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the ground level at a specific world coordinate (without any offset)
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn get_ground_level(&self, x: i32, z: i32) -> i32 {
|
|
||||||
if let Some(ground) = &self.ground {
|
|
||||||
ground.level(XZPoint::new(
|
|
||||||
x - self.xzbbox.min_x(),
|
|
||||||
z - self.xzbbox.min_z(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
0 // Default ground level if no terrain data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the minimum world coordinates
|
|
||||||
pub fn get_min_coords(&self) -> (i32, i32) {
|
|
||||||
(self.xzbbox.min_x(), self.xzbbox.min_z())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves regions silently without progress messages (for parallel unit processing).
|
|
||||||
/// Does not save metadata - call save_metadata() separately if needed.
|
|
||||||
pub fn save_silent(&mut self) {
|
|
||||||
match self.format {
|
|
||||||
WorldFormat::JavaAnvil => self.save_java_silent(),
|
|
||||||
WorldFormat::BedrockMcWorld => {
|
|
||||||
// For Bedrock, use normal save since it's not called per-unit
|
|
||||||
self.save_bedrock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
fn save_bedrock(&mut self) {
|
|
||||||
println!("{} Saving Bedrock world...", "[7/7]".bold());
|
|
||||||
emit_gui_progress_update(90.0, "Saving Bedrock world...");
|
|
||||||
|
|
||||||
#[cfg(feature = "bedrock")]
|
|
||||||
{
|
|
||||||
if let Err(error) = self.save_bedrock_internal() {
|
|
||||||
eprintln!("Failed to save Bedrock world: {error}");
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
send_log(
|
|
||||||
LogLevel::Error,
|
|
||||||
&format!("Failed to save Bedrock world: {error}"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "bedrock"))]
|
|
||||||
{
|
|
||||||
eprintln!(
|
|
||||||
"Bedrock output requested but the 'bedrock' feature is not enabled at build time."
|
|
||||||
);
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
send_log(
|
|
||||||
LogLevel::Error,
|
|
||||||
"Bedrock output requested but the 'bedrock' feature is not enabled at build time.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "bedrock")]
|
|
||||||
fn save_bedrock_internal(&mut self) -> Result<(), BedrockSaveError> {
|
|
||||||
// Use the stored level name if available, otherwise extract from path
|
|
||||||
let level_name = self.bedrock_level_name.clone().unwrap_or_else(|| {
|
|
||||||
self.world_dir
|
|
||||||
.file_stem()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("Arnis World")
|
|
||||||
.to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
BedrockWriter::new(
|
|
||||||
self.world_dir.clone(),
|
|
||||||
level_name,
|
|
||||||
self.bedrock_spawn_point,
|
|
||||||
self.ground.clone(),
|
|
||||||
)
|
|
||||||
.write_world(&self.world, self.xzbbox, &self.llbbox)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves world metadata to a JSON file
|
|
||||||
pub(crate) fn save_metadata(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let metadata_path = self.world_dir.join("metadata.json");
|
|
||||||
|
|
||||||
let mut file = File::create(&metadata_path).map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"Failed to create metadata file at {}: {}",
|
|
||||||
metadata_path.display(),
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let metadata = WorldMetadata {
|
|
||||||
min_mc_x: self.xzbbox.min_x(),
|
|
||||||
max_mc_x: self.xzbbox.max_x(),
|
|
||||||
min_mc_z: self.xzbbox.min_z(),
|
|
||||||
max_mc_z: self.xzbbox.max_z(),
|
|
||||||
|
|
||||||
min_geo_lat: self.llbbox.min().lat(),
|
|
||||||
max_geo_lat: self.llbbox.max().lat(),
|
|
||||||
min_geo_lon: self.llbbox.min().lng(),
|
|
||||||
max_geo_lon: self.llbbox.max().lng(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let contents = serde_json::to_string(&metadata)
|
|
||||||
.map_err(|e| format!("Failed to serialize metadata to JSON: {}", e))?;
|
|
||||||
|
|
||||||
write!(&mut file, "{}", contents)
|
|
||||||
.map_err(|e| format!("Failed to write metadata to file: {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Arnis",
|
"productName": "Arnis",
|
||||||
"version": "2.4.1",
|
"version": "2.3.1",
|
||||||
"identifier": "com.louisdev.arnis",
|
"identifier": "com.louisdev.arnis",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "src/gui"
|
"frontendDist": "src/gui"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"minWidth": 1000,
|
"minWidth": 1000,
|
||||||
"minHeight": 650,
|
"minHeight": 650,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"transparent": false,
|
"transparent": true,
|
||||||
"center": true,
|
"center": true,
|
||||||
"theme": "Dark",
|
"theme": "Dark",
|
||||||
"additionalBrowserArgs": "--disable-features=VizDisplayCompositor"
|
"additionalBrowserArgs": "--disable-features=VizDisplayCompositor"
|
||||||
|
|||||||
Reference in New Issue
Block a user