Compare commits
300 Commits
v2.3.0
...
parallel-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56fe1b9515 | ||
|
|
f79b610c0d | ||
|
|
c62600e972 | ||
|
|
225cb79381 | ||
|
|
9fd1868d41 | ||
|
|
ceb0c80fba | ||
|
|
6444a4498a | ||
|
|
6ef8169d45 | ||
|
|
568a6063f7 | ||
|
|
6cdebbed78 | ||
|
|
5291f72215 | ||
|
|
c24e22b790 | ||
|
|
d4f324fd96 | ||
|
|
e7e65d0e6f | ||
|
|
927aaec22d | ||
|
|
5ec942dbd1 | ||
|
|
19bba3cc26 | ||
|
|
17d6d323fc | ||
|
|
236072dc42 | ||
|
|
7a8226923a | ||
|
|
107ab70602 | ||
|
|
1364d96291 | ||
|
|
b74b5c5ccb | ||
|
|
dd8004b159 | ||
|
|
b0845ce1df | ||
|
|
fc540db4cd | ||
|
|
1ecdffc039 | ||
|
|
9ea34b9911 | ||
|
|
48248aad05 | ||
|
|
169545d937 | ||
|
|
fba331232b | ||
|
|
b02a2783c1 | ||
|
|
dbc4741b78 | ||
|
|
b52485badc | ||
|
|
447416f6ce | ||
|
|
d26b23937e | ||
|
|
5e01abc5b6 | ||
|
|
7c808ec352 | ||
|
|
b757c5acf4 | ||
|
|
ced5fc274e | ||
|
|
295ca415d7 | ||
|
|
e2b4ca8bdb | ||
|
|
07105f0208 | ||
|
|
ad57fdbc3a | ||
|
|
550870d9e0 | ||
|
|
bd693ea007 | ||
|
|
ce8f343414 | ||
|
|
f882145780 | ||
|
|
b52d750935 | ||
|
|
4d30899909 | ||
|
|
311610a717 | ||
|
|
b4902ebc9e | ||
|
|
e5bbb3e4a0 | ||
|
|
0238cfe2d0 | ||
|
|
2d9892fe7f | ||
|
|
b858ce4691 | ||
|
|
e031e53492 | ||
|
|
6fb9b8943d | ||
|
|
18266dd459 | ||
|
|
b1940fa412 | ||
|
|
d57a732055 | ||
|
|
4e52b38f5a | ||
|
|
feb4317086 | ||
|
|
d02cbed997 | ||
|
|
99d1f8e117 | ||
|
|
6fa76bc381 | ||
|
|
0fef27e6af | ||
|
|
fa3384cf86 | ||
|
|
ffbc5e5788 | ||
|
|
4215e7644c | ||
|
|
118335bad4 | ||
|
|
7bbee28279 | ||
|
|
9cb35a3b13 | ||
|
|
4fecf98c54 | ||
|
|
47a7b81f99 | ||
|
|
7ec90b4fef | ||
|
|
f1f3fb287a | ||
|
|
b23658d5ef | ||
|
|
cc89576828 | ||
|
|
809fa23941 | ||
|
|
51ad1fef3f | ||
|
|
8e8d8e0567 | ||
|
|
da6f23c0a2 | ||
|
|
d4a872989c | ||
|
|
2a5a5230c5 | ||
|
|
9018584b1d | ||
|
|
9eda39846c | ||
|
|
5e9d6795df | ||
|
|
54a7a4f2a9 | ||
|
|
d0d65643f5 | ||
|
|
946fd43a5e | ||
|
|
05e5ffdd2a | ||
|
|
0b7e27df7f | ||
|
|
613a410c93 | ||
|
|
faefd29e30 | ||
|
|
9ad6c75440 | ||
|
|
e51f28f067 | ||
|
|
47ddb9b211 | ||
|
|
46415bb002 | ||
|
|
0683dd3343 | ||
|
|
4d304dc978 | ||
|
|
5d97391820 | ||
|
|
bef3cfb090 | ||
|
|
5a898944f7 | ||
|
|
9fdd960009 | ||
|
|
0a51b302ee | ||
|
|
93dc9f446c | ||
|
|
e6430f2a04 | ||
|
|
58e4a337d9 | ||
|
|
236a7e5af9 | ||
|
|
5962decf44 | ||
|
|
9173e5b4de | ||
|
|
1fd02d8005 | ||
|
|
438b2beceb | ||
|
|
a62e181c16 | ||
|
|
12abba3bc8 | ||
|
|
a8e31700d8 | ||
|
|
7a109cce0b | ||
|
|
86543714af | ||
|
|
b84a565210 | ||
|
|
93becaae7f | ||
|
|
06e377ce29 | ||
|
|
e22380bdd3 | ||
|
|
35cac44209 | ||
|
|
61af45d2f4 | ||
|
|
393f1f9bd8 | ||
|
|
e6f8466177 | ||
|
|
02d3a32a03 | ||
|
|
f00304ff3a | ||
|
|
a93b908104 | ||
|
|
7cbc4fa263 | ||
|
|
7e7f7ed476 | ||
|
|
3c0ba60657 | ||
|
|
fb438c4a0f | ||
|
|
5015c8b9b4 | ||
|
|
af0ace422f | ||
|
|
0bb39b7d9e | ||
|
|
5b5e93b89a | ||
|
|
958dc2107e | ||
|
|
562a3bca66 | ||
|
|
f1b37fbbb6 | ||
|
|
b34cbf4307 | ||
|
|
a03318bb98 | ||
|
|
8bb779d6cc | ||
|
|
6d164102ad | ||
|
|
127a0e5e68 | ||
|
|
4a326c3dad | ||
|
|
d4fd9b9cd3 | ||
|
|
ee0521f232 | ||
|
|
8b3a41b131 | ||
|
|
02594b1cae | ||
|
|
06ba4db97e | ||
|
|
59d31cfbb8 | ||
|
|
94388e4164 | ||
|
|
f8c9fd8f4c | ||
|
|
2ee2d48f6a | ||
|
|
56c2f2e5cd | ||
|
|
9d34bc8e92 | ||
|
|
c95b78fdcd | ||
|
|
6e52e08b8a | ||
|
|
57a4a801cf | ||
|
|
0c47e365bc | ||
|
|
dad3ab3b34 | ||
|
|
b8b63a2bc5 | ||
|
|
cab20b5e50 | ||
|
|
0e879837fa | ||
|
|
92be2ccf00 | ||
|
|
3b76d707d9 | ||
|
|
be8559dee7 | ||
|
|
94eda2fad3 | ||
|
|
7d86854e3c | ||
|
|
cddaa89d35 | ||
|
|
453845977d | ||
|
|
4e196e51bd | ||
|
|
ea4dc5dc08 | ||
|
|
c56ff83094 | ||
|
|
2b40a520ff | ||
|
|
a192be981a | ||
|
|
eb77bca10d | ||
|
|
4a891c3603 | ||
|
|
84adfdd931 | ||
|
|
823b6ba052 | ||
|
|
2ba8157ec9 | ||
|
|
7235ba0be9 | ||
|
|
dee580c564 | ||
|
|
41fc5662e0 | ||
|
|
ac884b8c2a | ||
|
|
7a9b792bee | ||
|
|
83e9a634e5 | ||
|
|
56ddea57d0 | ||
|
|
430a4970f5 | ||
|
|
74fbdabaee | ||
|
|
2643155e9a | ||
|
|
d45c360074 | ||
|
|
6277a14d22 | ||
|
|
c355f243e3 | ||
|
|
2c31d2659c | ||
|
|
996e06ab2c | ||
|
|
e11231ad0f | ||
|
|
9adf31121e | ||
|
|
69da18fbfb | ||
|
|
5976cc2868 | ||
|
|
a85eaed835 | ||
|
|
37c3d85672 | ||
|
|
15b698a1eb | ||
|
|
834ce7b51d | ||
|
|
93b6f5ac99 | ||
|
|
77df683deb | ||
|
|
9d791f4299 | ||
|
|
a2dff0b84b | ||
|
|
24630351b9 | ||
|
|
819b03e3b1 | ||
|
|
f315245637 | ||
|
|
16c9f3c3bf | ||
|
|
3043ca6d24 | ||
|
|
3657878f01 | ||
|
|
44cc57c3dd | ||
|
|
408caa9176 | ||
|
|
3191a3676d | ||
|
|
8fff2d2fb5 | ||
|
|
8c702a36ff | ||
|
|
9bedca071a | ||
|
|
d611837746 | ||
|
|
e06fcaf7a2 | ||
|
|
1f2772f052 | ||
|
|
82f3460043 | ||
|
|
65d6bb6c99 | ||
|
|
7ebee82982 | ||
|
|
5056e83dff | ||
|
|
3cb0b994e1 | ||
|
|
5ded3c961e | ||
|
|
893c14bff8 | ||
|
|
4f8e0020e3 | ||
|
|
456018abf7 | ||
|
|
175bdc4582 | ||
|
|
ed294a3973 | ||
|
|
6c966fc9cc | ||
|
|
ecfc6634fc | ||
|
|
a336ccd1aa | ||
|
|
f702a41af1 | ||
|
|
29c4dc6d7c | ||
|
|
f60b341eca | ||
|
|
92c5e52f46 | ||
|
|
e4d7dd15c2 | ||
|
|
cc6748115b | ||
|
|
ce9496aea5 | ||
|
|
4c87eb8141 | ||
|
|
ada475f73e | ||
|
|
e5a82ba526 | ||
|
|
309ac19b09 | ||
|
|
90df7688df | ||
|
|
5b3889a7bb | ||
|
|
67deb739e6 | ||
|
|
7c08d21f36 | ||
|
|
280acc7a8a | ||
|
|
ee59da5d9b | ||
|
|
b772cb6ab9 | ||
|
|
2646947ed0 | ||
|
|
b9976fd562 | ||
|
|
a621703da4 | ||
|
|
87efd02c74 | ||
|
|
6a6b58fd8f | ||
|
|
855c6fe846 | ||
|
|
1592951fe3 | ||
|
|
e267e04350 | ||
|
|
c2e8d5959f | ||
|
|
ffe8f865d2 | ||
|
|
4f9f4f127a | ||
|
|
014208426b | ||
|
|
6d848ef7cd | ||
|
|
1e25dfea37 | ||
|
|
903efec459 | ||
|
|
22b3969c72 | ||
|
|
0f62c4283b | ||
|
|
4299e64cba | ||
|
|
0cd386b399 | ||
|
|
2fdecbe206 | ||
|
|
75d1ab14e7 | ||
|
|
f9c18ae5f6 | ||
|
|
963fe2327b | ||
|
|
2a8ff6f641 | ||
|
|
ff3ea1093a | ||
|
|
fd401ab23f | ||
|
|
3f67e060eb | ||
|
|
858c2f4c93 | ||
|
|
e8fad0e197 | ||
|
|
7ebc66db7e | ||
|
|
28b75a22cf | ||
|
|
9ee9f0de64 | ||
|
|
5b874a5c29 | ||
|
|
eeecdc8f99 | ||
|
|
8b33e152ef | ||
|
|
ca8e50fbf1 | ||
|
|
7f4ef9130f | ||
|
|
8d183543be | ||
|
|
dd07fba15c | ||
|
|
083fbed040 | ||
|
|
4191fa4902 | ||
|
|
6c82265ee3 | ||
|
|
7d978047b4 |
2
.gitattributes
vendored
@@ -1 +1 @@
|
|||||||
gui-src/** linguist-vendored
|
src/gui/** linguist-vendored
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,13 +11,13 @@ assignees: ''
|
|||||||
A clear and concise description of what the bug is and what you expected to happen.
|
A clear and concise description of what the bug is and what you expected to happen.
|
||||||
|
|
||||||
**Used bbox area**
|
**Used bbox area**
|
||||||
Please provide your input parameters so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
|
Please provide your input parameters (BBOX) so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
|
||||||
|
|
||||||
**Arnis and Minecraft version**
|
**Arnis and Minecraft version**
|
||||||
Please tell us what version of Arnis and Minecraft you used.
|
Please tell us what version of Arnis and Minecraft you used, as well as if you are on Windows, Linux or MacOS.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here. If you used any more custom settings, please provide them here too. If you experienced any issue with the application itself like a crash, please provide the log file which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs
|
Add any other context about the problem here. If you used any more custom settings, please provide them here too. Please provide the log file if possible as well, which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs
|
||||||
|
|||||||
6
.github/workflows/ci-build.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Rust
|
- name: Set up Rust
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
components: clippy
|
components: clippy, rustfmt
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Rust
|
- name: Set up Rust
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
|||||||
14
.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@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- 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.101470,11.517792,48.168375,11.626968" 2> benchmark_log.txt
|
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
|
||||||
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
|
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))
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
seconds=$((duration % 60))
|
seconds=$((duration % 60))
|
||||||
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
||||||
|
|
||||||
baseline_time=135
|
baseline_time=30
|
||||||
diff=$((duration - baseline_time))
|
diff=$((duration - baseline_time))
|
||||||
abs_diff=${diff#-}
|
abs_diff=${diff#-}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
verdict="🚨 This PR **drastically worsens generation time**."
|
verdict="🚨 This PR **drastically worsens generation time**."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
baseline_mem=5865
|
baseline_mem=935
|
||||||
mem_annotation=""
|
mem_annotation=""
|
||||||
if [ "$peak_mem" -gt 2000 ]; then
|
if [ "$peak_mem" -gt 2000 ]; then
|
||||||
mem_diff=$((peak_mem - baseline_mem))
|
mem_diff=$((peak_mem - baseline_mem))
|
||||||
@@ -87,6 +87,8 @@ jobs:
|
|||||||
mem_annotation=" (↗ ${mem_percent}% more)"
|
mem_annotation=" (↗ ${mem_percent}% more)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "summary<<EOF"
|
echo "summary<<EOF"
|
||||||
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
|
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
|
||||||
@@ -98,6 +100,8 @@ jobs:
|
|||||||
echo ""
|
echo ""
|
||||||
echo "${verdict}"
|
echo "${verdict}"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "📅 **Last benchmark:** ${benchmark_time}"
|
||||||
|
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"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
@@ -108,4 +112,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 }}
|
||||||
24
.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-13 # Intel runner for x86_64 builds
|
- os: macos-15-intel # 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
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Rust
|
- name: Set up Rust
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
shell: powershell
|
shell: powershell
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
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@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: macos-13-x86_64-apple-darwin-build
|
name: macos-15-intel-x86_64-apple-darwin-build
|
||||||
path: ./intel
|
path: ./intel
|
||||||
|
|
||||||
- name: Download macOS ARM64 build
|
- name: Download macOS ARM64 build
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
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@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: macos-universal-build
|
name: macos-universal-build
|
||||||
path: arnis-mac-universal
|
path: arnis-mac-universal
|
||||||
@@ -124,22 +124,22 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download Windows build artifact
|
- name: Download Windows build artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
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@v4
|
uses: actions/download-artifact@v7
|
||||||
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@v4
|
uses: actions/download-artifact@v7
|
||||||
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 }}
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -1,8 +1,8 @@
|
|||||||
/wiki
|
/wiki
|
||||||
|
*.mcworld
|
||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
.envrc
|
|
||||||
/.direnv
|
/.direnv
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
|
|||||||
608
Cargo.lock
generated
27
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "arnis"
|
name = "arnis"
|
||||||
version = "2.3.0"
|
version = "2.4.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,40 +14,49 @@ overflow-checks = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gui"]
|
default = ["gui"]
|
||||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build"]
|
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
||||||
|
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = {version = "2", optional = true}
|
tauri-build = {version = "2", optional = true}
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
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 }
|
||||||
fastanvil = "0.31.0"
|
fastanvil = "0.32.0"
|
||||||
fastnbt = "2.5.0"
|
fastnbt = "2.6.0"
|
||||||
flate2 = "1.1"
|
flate2 = "1.1"
|
||||||
fnv = "1.0.7"
|
fnv = "1.0.7"
|
||||||
fs2 = "0.4"
|
fs2 = "0.4"
|
||||||
geo = "0.30.0"
|
geo = "0.31.0"
|
||||||
image = "0.25"
|
image = "0.25"
|
||||||
indicatif = "0.17.11"
|
indicatif = "0.17.11"
|
||||||
itertools = "0.14.0"
|
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.15.4", optional = true }
|
rfd = { version = "0.16.0", optional = true }
|
||||||
semver = "1.0.23"
|
semver = "1.0.27"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "2", optional = true }
|
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.47.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"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.23.0"
|
||||||
|
|||||||
20
README.md
@@ -1,17 +1,17 @@
|
|||||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/banner.png?raw=true" width="100%" alt="Banner">
|
<img src="assets/git/banner.png" width="100%" alt="Banner">
|
||||||
|
|
||||||
# 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 worlds that reflect real-world geography, topography, and architecture.
|
Arnis creates complex and accurate Minecraft Java Edition (1.17+) and Bedrock Edition worlds that reflect real-world geography, topography, and architecture.
|
||||||
|
|
||||||
This free and open source project is designed to handle large-scale geographic data from the real world and generate detailed Minecraft worlds. The algorithm processes geospatial data from OpenStreetMap as well as elevation data to create an accurate Minecraft representation of terrain and architecture.
|
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!
|
||||||
|
|
||||||

|

|
||||||
<i>This Github page is the official project website. Do not download Arnis from any other website.</i>
|
<i>This Github page and [arnismc.com](https://arnismc.com) are the only official project websites. Do not download Arnis from any other website.</i>
|
||||||
|
|
||||||
## :keyboard: Usage
|
## :keyboard: Usage
|
||||||
<img width="60%" src="https://github.com/louis-e/arnis/blob/main/gitassets/gui.png?raw=true"><br>
|
<img width="60%" src="assets/git/gui.png"><br>
|
||||||
Download the [latest release](https://github.com/louis-e/arnis/releases/) or [compile](#trophy-open-source) the project on your own.
|
Download the [latest release](https://github.com/louis-e/arnis/releases/) or [compile](#trophy-open-source) the project on your own.
|
||||||
|
|
||||||
Choose your area on the map using the rectangle tool and select your Minecraft world - then simply click on <i>Start Generation</i>!
|
Choose your area on the map using the rectangle tool and select your Minecraft world - then simply click on <i>Start Generation</i>!
|
||||||
@@ -19,7 +19,7 @@ Additionally, you can customize various generation settings, such as world scale
|
|||||||
|
|
||||||
## 📚 Documentation
|
## 📚 Documentation
|
||||||
|
|
||||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/documentation.png?raw=true" width="100%" alt="Banner">
|
<img src="assets/git/documentation.png" width="100%" alt="Banner">
|
||||||
|
|
||||||
Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/arnis/wiki/), covering topics such as technical explanations, FAQs, contribution guidelines and roadmaps.
|
Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/arnis/wiki/), covering topics such as technical explanations, FAQs, contribution guidelines and roadmaps.
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/
|
|||||||
#### How to contribute
|
#### How to contribute
|
||||||
This project is open source and welcomes contributions from everyone! Whether you're interested in fixing bugs, improving performance, adding new features, or enhancing documentation, your input is valuable. Simply fork the repository, make your changes, and submit a pull request. Please respect the above mentioned key objectives. Contributions of all levels are appreciated, and your efforts help improve this tool for everyone.
|
This project is open source and welcomes contributions from everyone! Whether you're interested in fixing bugs, improving performance, adding new features, or enhancing documentation, your input is valuable. Simply fork the repository, make your changes, and submit a pull request. Please respect the above mentioned key objectives. Contributions of all levels are appreciated, and your efforts help improve this tool for everyone.
|
||||||
|
|
||||||
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```<br>
|
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lat,min_lng,max_lat,max_lng"```<br>
|
||||||
GUI Build: ```cargo run```<br>
|
GUI Build: ```cargo run```<br>
|
||||||
|
|
||||||
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
|
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
|
||||||
@@ -51,7 +51,7 @@ After your pull request was merged, I will take care of regularly creating updat
|
|||||||
|
|
||||||
## :newspaper: Academic & Press Recognition
|
## :newspaper: Academic & Press Recognition
|
||||||
|
|
||||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/recognition.png?raw=true" width="100%" alt="Banner">
|
<img src="assets/git/recognition.png" width="100%" alt="Banner">
|
||||||
|
|
||||||
Arnis has been recognized in various academic and press publications after gaining a lot of attention in December 2024.
|
Arnis has been recognized in various academic and press publications after gaining a lot of attention in December 2024.
|
||||||
|
|
||||||
@@ -63,6 +63,8 @@ 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)
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.[^3]
|
limitations under the License.[^3]
|
||||||
|
|
||||||
Download Arnis only from the official source (https://github.com/louis-e/arnis/). Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.
|
Download Arnis only from the official source https://arnismc.com or https://github.com/louis-e/arnis/. Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.
|
||||||
|
|
||||||
The logo was made by @nxfx21.
|
The logo was made by @nxfx21.
|
||||||
|
|
||||||
|
|||||||
183
analyze_performance.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/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
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
"(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
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
"(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"
|
||||||
|
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 790 KiB After Width: | Height: | Size: 790 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/minecraft/world_icon.jpeg
Normal file
|
After Width: | Height: | Size: 54 KiB |
892
docs/PARALLEL_REGION_PROCESSING.md
Normal file
@@ -0,0 +1,892 @@
|
|||||||
|
# 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.
|
||||||
60
flake.lock
generated
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1755615617,
|
||||||
|
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
36
flake.nix
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
flake-utils,
|
||||||
|
nixpkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
stdenv = if pkgs.stdenv.isLinux then pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv else pkgs.stdenv;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = pkgs.mkShell.override { inherit stdenv; } {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
openssl.dev
|
||||||
|
pkg-config
|
||||||
|
wayland
|
||||||
|
glib
|
||||||
|
gdk-pixbuf
|
||||||
|
pango
|
||||||
|
gtk3
|
||||||
|
libsoup_3.dev
|
||||||
|
webkitgtk_4_1.dev
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
99
flake/flake.lock
generated
@@ -1,99 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"fenix": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"rust-analyzer-src": "rust-analyzer-src"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1724653830,
|
|
||||||
"narHash": "sha256-88f0KK8h6tGIP4Na5RJDKs0S+7WsGGaCGNkLj/bPV3g=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "fenix",
|
|
||||||
"rev": "9ecf5e7d800ace001320da8acadd4a3deb872a83",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "fenix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1710146030,
|
|
||||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1724479785,
|
|
||||||
"narHash": "sha256-pP3Azj5d6M5nmG68Fu4JqZmdGt4S4vqI5f8te+E/FTw=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "d0e1602ddde669d5beb01aec49d71a51937ed7be",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "nixpkgs",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"fenix": "fenix",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-analyzer-src": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1724586512,
|
|
||||||
"narHash": "sha256-mrfwk6nO8N2WtCq3sB2zhd2QN1HMKzeSESzOA6lSsQg=",
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"rev": "7106cd3be50b2a43c1d9f2787bf22d4369c2b25b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"ref": "nightly",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
inputs = {
|
|
||||||
fenix = {
|
|
||||||
url = "github:nix-community/fenix";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, fenix, flake-utils, nixpkgs }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
fenixPkgs = (fenix.packages.${system}.stable);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShell = pkgs.mkShell
|
|
||||||
{
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
openssl.dev
|
|
||||||
pkg-config
|
|
||||||
fenixPkgs.toolchain
|
|
||||||
wayland
|
|
||||||
glib
|
|
||||||
gdk-pixbuf
|
|
||||||
pango
|
|
||||||
gtk3
|
|
||||||
libsoup_3.dev
|
|
||||||
webkitgtk_4_1.dev
|
|
||||||
];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,221 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="stylesheet" href="./css/styles.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Arnis</title>
|
|
||||||
<script type="module" src="./js/main.js" defer></script>
|
|
||||||
<script type="module" src="./js/license.js" defer></script>
|
|
||||||
<script type="module" src="./js/language-selector.js" defer></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="container">
|
|
||||||
<div class="row">
|
|
||||||
<a href="https://github.com/louis-e/arnis" target="_blank">
|
|
||||||
<img src="./images/logo.png" id="arnis-logo" class="logo arnis" alt="Arnis Logo" style="width: 35%; height: auto;">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-container">
|
|
||||||
<!-- Left Box: Map and BBox Input -->
|
|
||||||
<section class="section map-box" style="margin-bottom: 0; padding-bottom: 0;">
|
|
||||||
<h2 data-localize="select_location">Select Location</h2>
|
|
||||||
<span id="bbox-text" style="font-size: 1.0em; display: block; margin-top: -8px; margin-bottom: 3px;" data-localize="zoom_in_and_choose">
|
|
||||||
Zoom in and choose your area using the rectangle tool
|
|
||||||
</span>
|
|
||||||
<iframe src="maps.html" width="100%" height="300" class="map-container" title="Map Picker"></iframe>
|
|
||||||
|
|
||||||
<span id="bbox-info"
|
|
||||||
style="font-size: 0.75em; color: #7bd864; display: block; margin-bottom: 4px; font-weight: bold; min-height: 2em;"></span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Right Box: Directory Selection, Start Button, and Progress Bar -->
|
|
||||||
<section class="section controls-box">
|
|
||||||
<div class="controls-content">
|
|
||||||
<h2 data-localize="select_world">Select World</h2>
|
|
||||||
|
|
||||||
<!-- Updated Tooltip Structure -->
|
|
||||||
<div class="tooltip" style="width: 100%;">
|
|
||||||
<button type="button" onclick="openWorldPicker()" style="padding: 10px; line-height: 1.2; width: 100%;">
|
|
||||||
<span id="choose_world">Choose World</span>
|
|
||||||
<br>
|
|
||||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
|
||||||
No world selected
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</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">
|
|
||||||
<h2 data-localize="progress">Progress</h2>
|
|
||||||
<div class="progress-bar-container">
|
|
||||||
<div class="progress-bar" id="progress-bar"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-status">
|
|
||||||
<span id="progress-message"></span>
|
|
||||||
<span id="progress-detail">0%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- World Picker Modal -->
|
|
||||||
<div id="world-modal" class="modal" style="display: none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-button" onclick="closeWorldPicker()">×</span>
|
|
||||||
<h2 data-localize="choose_world_modal_title">Choose World</h2>
|
|
||||||
|
|
||||||
<button type="button" id="select-world-button" class="select-world-button" onclick="selectWorld(false)" data-localize="select_existing_world">Select existing world</button>
|
|
||||||
<button type="button" id="generate-world-button" class="generate-world-button" onclick="selectWorld(true)" data-localize="generate_new_world">Generate new world</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
|
||||||
<div id="settings-modal" class="modal" style="display: none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-button" onclick="closeSettings()">×</span>
|
|
||||||
<h2 data-localize="customization_settings">Customization Settings</h2>
|
|
||||||
|
|
||||||
<!-- Terrain Toggle Button -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="terrain-toggle" data-localize="terrain">Terrain</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="checkbox" id="terrain-toggle" name="terrain-toggle" checked>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Interior Toggle Button -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="interior-toggle" data-localize="interior">Interior Generation</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="checkbox" id="interior-toggle" name="interior-toggle" checked>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Roof Toggle Button -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="roof-toggle" data-localize="roof">Roof Generation</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="checkbox" id="roof-toggle" name="roof-toggle" checked>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fill ground Toggle Button -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="fillground-toggle" data-localize="fillground">Fill Ground</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="checkbox" id="fillground-toggle" name="fillground-toggle">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- World Scale Slider -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="scale-value-slider" data-localize="world_scale">World Scale</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.30" max="2.5" step="0.1" value="1">
|
|
||||||
<span id="slider-value">1.00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bounding Box Input -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="bbox-coords" data-localize="custom_bounding_box">Custom Bounding Box</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" placeholder="Format: lat,lng,lat,lng">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Floodfill Timeout Input -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="floodfill-timeout" data-localize="floodfill_timeout">Floodfill Timeout (sec)</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<input type="number" id="floodfill-timeout" name="floodfill-timeout" min="0" step="1" value="20" placeholder="Seconds">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Map Theme Selector -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="tile-theme-select" data-localize="map_theme">Map Theme</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<select id="tile-theme-select" name="tile-theme-select" class="theme-dropdown">
|
|
||||||
<option value="osm">Standard</option>
|
|
||||||
<option value="esri-imagery">Satellite</option>
|
|
||||||
<option value="opentopomap">Topographic</option>
|
|
||||||
<option value="stadia-bright">Smooth Bright</option>
|
|
||||||
<option value="stadia-dark">Smooth Dark</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Language Selector -->
|
|
||||||
<div class="settings-row">
|
|
||||||
<label for="language-select" data-localize="language">Language</label>
|
|
||||||
<div class="settings-control">
|
|
||||||
<select id="language-select" name="language-select" class="language-dropdown">
|
|
||||||
<option value="en">English</option>
|
|
||||||
<option value="de">Deutsch</option>
|
|
||||||
<option value="es">Español</option>
|
|
||||||
<option value="fr-FR">Français</option>
|
|
||||||
<option value="ru">Русский</option>
|
|
||||||
<option value="zh-CN">中文 (简体)</option>
|
|
||||||
<option value="ko">한국어</option>
|
|
||||||
<option value="pl">Polski</option>
|
|
||||||
<option value="sv">Svenska</option>
|
|
||||||
<option value="ar">العربية</option>
|
|
||||||
<option value="fi">Suomi</option>
|
|
||||||
<option value="hu">Magyar</option>
|
|
||||||
<option value="lt">Lietuvių</option>
|
|
||||||
<option value="ua">Українська</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- License and Credits Button -->
|
|
||||||
<div class="settings-row license-button-row">
|
|
||||||
<button type="button" id="license-button" class="license-button" onclick="openLicense()" data-localize="license_and_credits">License and Credits</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Buy Me a Coffee and Discord -->
|
|
||||||
<div class="settings-row buymeacoffee-row">
|
|
||||||
<a href="https://buymeacoffee.com/louisdev" target="_blank" class="buymeacoffee-link">
|
|
||||||
<img src="./images/buymeacoffee.png" alt="Buy Me a Coffee" class="buymeacoffee-logo">
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/mA2g69Fhxq" target="_blank" class="discord-link">
|
|
||||||
<img src="./images/discord.png" alt="Discord" class="discord-logo">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- License Modal -->
|
|
||||||
<div id="license-modal" class="modal" style="display: none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-button" onclick="closeLicense()">×</span>
|
|
||||||
<h2 data-localize="license_and_credits">License and Credits</h2>
|
|
||||||
<div id="license-content" style="overflow-y: auto; max-height: 300px; font-size: 0.85em; line-height: 1.3; padding: 10px; border: 1px solid #ccc; border-radius: 4px;">
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="footer">
|
|
||||||
<a href="https://github.com/louis-e/arnis" target="_blank" class="footer-link" data-localize="footer_text">
|
|
||||||
© {year} Arnis v{version} by louis-e
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
29
src/args.rs
@@ -1,6 +1,6 @@
|
|||||||
use crate::coordinate_system::geographic::LLBBox;
|
use crate::coordinate_system::geographic::LLBBox;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::path::Path;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Command-line arguments parser
|
/// Command-line arguments parser
|
||||||
@@ -21,7 +21,7 @@ pub struct Args {
|
|||||||
|
|
||||||
/// Path to the Minecraft world (required)
|
/// Path to the Minecraft world (required)
|
||||||
#[arg(long, value_parser = validate_minecraft_world_path)]
|
#[arg(long, value_parser = validate_minecraft_world_path)]
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
|
|
||||||
/// Downloader method (requests/curl/wget) (optional)
|
/// Downloader method (requests/curl/wget) (optional)
|
||||||
#[arg(long, default_value = "requests")]
|
#[arg(long, default_value = "requests")]
|
||||||
@@ -59,13 +59,26 @@ pub struct Args {
|
|||||||
#[arg(long, value_parser = parse_duration)]
|
#[arg(long, value_parser = parse_duration)]
|
||||||
pub timeout: Option<Duration>,
|
pub timeout: Option<Duration>,
|
||||||
|
|
||||||
/// Spawn point coordinates (lat, lng)
|
/// Number of parallel threads (0 = auto, uses available cores - 1)
|
||||||
#[arg(skip)]
|
#[arg(long, default_value_t = 0)]
|
||||||
pub spawn_point: Option<(f64, f64)>,
|
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)]
|
||||||
|
pub force_parallel: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_minecraft_world_path(path: &str) -> Result<String, String> {
|
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||||
let mc_world_path = Path::new(path);
|
let mc_world_path = PathBuf::from(path);
|
||||||
if !mc_world_path.exists() {
|
if !mc_world_path.exists() {
|
||||||
return Err(format!("Path does not exist: {path}"));
|
return Err(format!("Path does not exist: {path}"));
|
||||||
}
|
}
|
||||||
@@ -76,7 +89,7 @@ fn validate_minecraft_world_path(path: &str) -> Result<String, String> {
|
|||||||
if !region.is_dir() {
|
if !region.is_dir() {
|
||||||
return Err(format!("No Minecraft world found at {region:?}"));
|
return Err(format!("No Minecraft world found at {region:?}"));
|
||||||
}
|
}
|
||||||
Ok(path.to_string())
|
Ok(mc_world_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||||
|
|||||||
849
src/bedrock_block_map.rs
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
//! Bedrock Block Mapping
|
||||||
|
//!
|
||||||
|
//! This module provides translation between the internal Block representation
|
||||||
|
//! and Bedrock Edition block format. Bedrock uses string identifiers with
|
||||||
|
//! state properties that differ slightly from Java Edition.
|
||||||
|
|
||||||
|
use crate::block_definitions::Block;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Represents a Bedrock block with its identifier and state properties.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BedrockBlock {
|
||||||
|
/// The Bedrock block identifier (e.g., "minecraft:stone")
|
||||||
|
pub name: String,
|
||||||
|
/// Block state properties as key-value pairs
|
||||||
|
pub states: HashMap<String, BedrockBlockStateValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bedrock block state values can be strings, booleans, or integers.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum BedrockBlockStateValue {
|
||||||
|
String(String),
|
||||||
|
Bool(bool),
|
||||||
|
Int(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BedrockBlock {
|
||||||
|
/// Creates a simple block with no state properties.
|
||||||
|
pub fn simple(name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: format!("minecraft:{name}"),
|
||||||
|
states: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a block with state properties.
|
||||||
|
pub fn with_states(name: &str, states: Vec<(&str, BedrockBlockStateValue)>) -> Self {
|
||||||
|
let mut state_map = HashMap::new();
|
||||||
|
for (key, value) in states {
|
||||||
|
state_map.insert(key.to_string(), value);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
name: format!("minecraft:{name}"),
|
||||||
|
states: state_map,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an internal Block to a BedrockBlock representation.
|
||||||
|
///
|
||||||
|
/// This function handles the mapping between Java Edition block names/properties
|
||||||
|
/// and their Bedrock Edition equivalents. Many blocks are identical, but some
|
||||||
|
/// require translation of property names or values.
|
||||||
|
pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||||
|
let java_name = block.name();
|
||||||
|
|
||||||
|
// Most blocks have the same name in both editions
|
||||||
|
// Handle special cases first, then fall back to direct mapping
|
||||||
|
match java_name {
|
||||||
|
// Grass block is just "grass_block" in both editions
|
||||||
|
"grass_block" => BedrockBlock::simple("grass_block"),
|
||||||
|
|
||||||
|
// Short grass is just "short_grass" in Java but "tallgrass" in Bedrock
|
||||||
|
"short_grass" => BedrockBlock::with_states(
|
||||||
|
"tallgrass",
|
||||||
|
vec![(
|
||||||
|
"tall_grass_type",
|
||||||
|
BedrockBlockStateValue::String("tall".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tall grass needs height state
|
||||||
|
"tall_grass" => BedrockBlock::with_states(
|
||||||
|
"double_plant",
|
||||||
|
vec![(
|
||||||
|
"double_plant_type",
|
||||||
|
BedrockBlockStateValue::String("grass".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Oak leaves with persistence
|
||||||
|
"oak_leaves" => BedrockBlock::with_states(
|
||||||
|
"leaves",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"old_leaf_type",
|
||||||
|
BedrockBlockStateValue::String("oak".to_string()),
|
||||||
|
),
|
||||||
|
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Birch leaves with persistence
|
||||||
|
"birch_leaves" => BedrockBlock::with_states(
|
||||||
|
"leaves",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"old_leaf_type",
|
||||||
|
BedrockBlockStateValue::String("birch".to_string()),
|
||||||
|
),
|
||||||
|
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Oak log with axis (default up_down)
|
||||||
|
"oak_log" => BedrockBlock::with_states(
|
||||||
|
"oak_log",
|
||||||
|
vec![(
|
||||||
|
"pillar_axis",
|
||||||
|
BedrockBlockStateValue::String("y".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Birch log with axis
|
||||||
|
"birch_log" => BedrockBlock::with_states(
|
||||||
|
"birch_log",
|
||||||
|
vec![(
|
||||||
|
"pillar_axis",
|
||||||
|
BedrockBlockStateValue::String("y".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Spruce log with axis
|
||||||
|
"spruce_log" => BedrockBlock::with_states(
|
||||||
|
"spruce_log",
|
||||||
|
vec![(
|
||||||
|
"pillar_axis",
|
||||||
|
BedrockBlockStateValue::String("y".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Stone slab (bottom half by default)
|
||||||
|
"stone_slab" => BedrockBlock::with_states(
|
||||||
|
"stone_block_slab",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"stone_slab_type",
|
||||||
|
BedrockBlockStateValue::String("smooth_stone".to_string()),
|
||||||
|
),
|
||||||
|
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Stone brick slab
|
||||||
|
"stone_brick_slab" => BedrockBlock::with_states(
|
||||||
|
"stone_block_slab",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"stone_slab_type",
|
||||||
|
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||||
|
),
|
||||||
|
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Oak slab
|
||||||
|
"oak_slab" => BedrockBlock::with_states(
|
||||||
|
"wooden_slab",
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"wood_type",
|
||||||
|
BedrockBlockStateValue::String("oak".to_string()),
|
||||||
|
),
|
||||||
|
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Water (flowing by default)
|
||||||
|
"water" => BedrockBlock::with_states(
|
||||||
|
"water",
|
||||||
|
vec![("liquid_depth", BedrockBlockStateValue::Int(0))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Rail with shape state
|
||||||
|
"rail" => BedrockBlock::with_states(
|
||||||
|
"rail",
|
||||||
|
vec![("rail_direction", BedrockBlockStateValue::Int(0))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Farmland with moisture
|
||||||
|
"farmland" => BedrockBlock::with_states(
|
||||||
|
"farmland",
|
||||||
|
vec![("moisturized_amount", BedrockBlockStateValue::Int(7))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Snow layer
|
||||||
|
"snow" => BedrockBlock::with_states(
|
||||||
|
"snow_layer",
|
||||||
|
vec![("height", BedrockBlockStateValue::Int(0))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Cobblestone wall
|
||||||
|
"cobblestone_wall" => BedrockBlock::with_states(
|
||||||
|
"cobblestone_wall",
|
||||||
|
vec![(
|
||||||
|
"wall_block_type",
|
||||||
|
BedrockBlockStateValue::String("cobblestone".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Andesite wall
|
||||||
|
"andesite_wall" => BedrockBlock::with_states(
|
||||||
|
"cobblestone_wall",
|
||||||
|
vec![(
|
||||||
|
"wall_block_type",
|
||||||
|
BedrockBlockStateValue::String("andesite".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Stone brick wall
|
||||||
|
"stone_brick_wall" => BedrockBlock::with_states(
|
||||||
|
"cobblestone_wall",
|
||||||
|
vec![(
|
||||||
|
"wall_block_type",
|
||||||
|
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Flowers - poppy is just "red_flower" in Bedrock
|
||||||
|
"poppy" => BedrockBlock::with_states(
|
||||||
|
"red_flower",
|
||||||
|
vec![(
|
||||||
|
"flower_type",
|
||||||
|
BedrockBlockStateValue::String("poppy".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Dandelion is "yellow_flower" in Bedrock
|
||||||
|
"dandelion" => BedrockBlock::simple("yellow_flower"),
|
||||||
|
|
||||||
|
// Blue orchid
|
||||||
|
"blue_orchid" => BedrockBlock::with_states(
|
||||||
|
"red_flower",
|
||||||
|
vec![(
|
||||||
|
"flower_type",
|
||||||
|
BedrockBlockStateValue::String("orchid".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Azure bluet
|
||||||
|
"azure_bluet" => BedrockBlock::with_states(
|
||||||
|
"red_flower",
|
||||||
|
vec![(
|
||||||
|
"flower_type",
|
||||||
|
BedrockBlockStateValue::String("houstonia".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Concrete colors (Bedrock uses a single block with color state)
|
||||||
|
"white_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||||
|
),
|
||||||
|
"black_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||||
|
),
|
||||||
|
"gray_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||||
|
),
|
||||||
|
"light_gray_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("silver".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"light_blue_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"cyan_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||||
|
),
|
||||||
|
"blue_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||||
|
),
|
||||||
|
"purple_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("purple".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"magenta_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("magenta".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"red_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||||
|
),
|
||||||
|
"orange_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("orange".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"yellow_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("yellow".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"lime_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("lime".to_string()))],
|
||||||
|
),
|
||||||
|
"brown_concrete" => BedrockBlock::with_states(
|
||||||
|
"concrete",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Terracotta colors
|
||||||
|
"white_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||||
|
),
|
||||||
|
"orange_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("orange".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"yellow_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("yellow".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"light_blue_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"blue_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||||
|
),
|
||||||
|
"gray_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||||
|
),
|
||||||
|
"green_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||||
|
),
|
||||||
|
"red_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||||
|
),
|
||||||
|
"brown_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||||
|
),
|
||||||
|
"black_terracotta" => BedrockBlock::with_states(
|
||||||
|
"stained_hardened_clay",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||||
|
),
|
||||||
|
// Plain terracotta
|
||||||
|
"terracotta" => BedrockBlock::simple("hardened_clay"),
|
||||||
|
|
||||||
|
// Wool colors
|
||||||
|
"white_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||||
|
),
|
||||||
|
"red_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||||
|
),
|
||||||
|
"green_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||||
|
),
|
||||||
|
"brown_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||||
|
),
|
||||||
|
"cyan_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||||
|
),
|
||||||
|
"yellow_wool" => BedrockBlock::with_states(
|
||||||
|
"wool",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("yellow".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Carpets
|
||||||
|
"white_carpet" => BedrockBlock::with_states(
|
||||||
|
"carpet",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||||
|
),
|
||||||
|
"red_carpet" => BedrockBlock::with_states(
|
||||||
|
"carpet",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Stained glass
|
||||||
|
"white_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||||
|
),
|
||||||
|
"gray_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||||
|
),
|
||||||
|
"light_gray_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![(
|
||||||
|
"color",
|
||||||
|
BedrockBlockStateValue::String("silver".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"brown_stained_glass" => BedrockBlock::with_states(
|
||||||
|
"stained_glass",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Planks - Bedrock uses single "planks" block with wood_type state
|
||||||
|
"oak_planks" => BedrockBlock::with_states(
|
||||||
|
"planks",
|
||||||
|
vec![(
|
||||||
|
"wood_type",
|
||||||
|
BedrockBlockStateValue::String("oak".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"spruce_planks" => BedrockBlock::with_states(
|
||||||
|
"planks",
|
||||||
|
vec![(
|
||||||
|
"wood_type",
|
||||||
|
BedrockBlockStateValue::String("spruce".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"birch_planks" => BedrockBlock::with_states(
|
||||||
|
"planks",
|
||||||
|
vec![(
|
||||||
|
"wood_type",
|
||||||
|
BedrockBlockStateValue::String("birch".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"jungle_planks" => BedrockBlock::with_states(
|
||||||
|
"planks",
|
||||||
|
vec![(
|
||||||
|
"wood_type",
|
||||||
|
BedrockBlockStateValue::String("jungle".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"acacia_planks" => BedrockBlock::with_states(
|
||||||
|
"planks",
|
||||||
|
vec![(
|
||||||
|
"wood_type",
|
||||||
|
BedrockBlockStateValue::String("acacia".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"dark_oak_planks" => BedrockBlock::with_states(
|
||||||
|
"planks",
|
||||||
|
vec![(
|
||||||
|
"wood_type",
|
||||||
|
BedrockBlockStateValue::String("dark_oak".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"crimson_planks" => BedrockBlock::simple("crimson_planks"),
|
||||||
|
"warped_planks" => BedrockBlock::simple("warped_planks"),
|
||||||
|
|
||||||
|
// Stone variants
|
||||||
|
"stone" => BedrockBlock::simple("stone"),
|
||||||
|
"granite" => BedrockBlock::with_states(
|
||||||
|
"stone",
|
||||||
|
vec![(
|
||||||
|
"stone_type",
|
||||||
|
BedrockBlockStateValue::String("granite".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"polished_granite" => BedrockBlock::with_states(
|
||||||
|
"stone",
|
||||||
|
vec![(
|
||||||
|
"stone_type",
|
||||||
|
BedrockBlockStateValue::String("granite_smooth".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"diorite" => BedrockBlock::with_states(
|
||||||
|
"stone",
|
||||||
|
vec![(
|
||||||
|
"stone_type",
|
||||||
|
BedrockBlockStateValue::String("diorite".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"polished_diorite" => BedrockBlock::with_states(
|
||||||
|
"stone",
|
||||||
|
vec![(
|
||||||
|
"stone_type",
|
||||||
|
BedrockBlockStateValue::String("diorite_smooth".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"andesite" => BedrockBlock::with_states(
|
||||||
|
"stone",
|
||||||
|
vec![(
|
||||||
|
"stone_type",
|
||||||
|
BedrockBlockStateValue::String("andesite".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
"polished_andesite" => BedrockBlock::with_states(
|
||||||
|
"stone",
|
||||||
|
vec![(
|
||||||
|
"stone_type",
|
||||||
|
BedrockBlockStateValue::String("andesite_smooth".to_string()),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Blocks with different names in Bedrock
|
||||||
|
"bricks" => BedrockBlock::simple("brick_block"),
|
||||||
|
"end_stone_bricks" => BedrockBlock::simple("end_bricks"),
|
||||||
|
"nether_bricks" => BedrockBlock::simple("nether_brick"),
|
||||||
|
"red_nether_bricks" => BedrockBlock::simple("red_nether_brick"),
|
||||||
|
"snow_block" => BedrockBlock::simple("snow"),
|
||||||
|
"dirt_path" => BedrockBlock::simple("grass_path"),
|
||||||
|
"dead_bush" => BedrockBlock::simple("deadbush"),
|
||||||
|
"note_block" => BedrockBlock::simple("noteblock"),
|
||||||
|
|
||||||
|
// Oak items mapped to dark_oak in Bedrock (or generic equivalents)
|
||||||
|
"oak_pressure_plate" => BedrockBlock::simple("wooden_pressure_plate"),
|
||||||
|
"oak_door" => BedrockBlock::simple("wooden_door"),
|
||||||
|
"oak_trapdoor" => BedrockBlock::simple("trapdoor"),
|
||||||
|
|
||||||
|
// Bed (Bedrock uses single "bed" block with color state)
|
||||||
|
"red_bed" => BedrockBlock::with_states(
|
||||||
|
"bed",
|
||||||
|
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Default: use the same name (works for many blocks)
|
||||||
|
// Log unmapped blocks to help identify missing mappings
|
||||||
|
_ => BedrockBlock::simple(java_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an internal Block with optional Java properties to a BedrockBlock.
|
||||||
|
///
|
||||||
|
/// This function extends `to_bedrock_block` by also handling block-specific properties
|
||||||
|
/// like stair facing/shape, slab type, etc. Java property names and values are converted
|
||||||
|
/// to their Bedrock equivalents.
|
||||||
|
pub fn to_bedrock_block_with_properties(
|
||||||
|
block: Block,
|
||||||
|
java_properties: Option<&fastnbt::Value>,
|
||||||
|
) -> BedrockBlock {
|
||||||
|
let java_name = block.name();
|
||||||
|
|
||||||
|
// Extract Java properties as a map if present
|
||||||
|
let props_map = java_properties.and_then(|v| {
|
||||||
|
if let fastnbt::Value::Compound(map) = v {
|
||||||
|
Some(map)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle stairs with facing/shape properties
|
||||||
|
if java_name.ends_with("_stairs") {
|
||||||
|
return convert_stairs(java_name, props_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle slabs with type property (top/bottom/double)
|
||||||
|
if java_name.ends_with("_slab") {
|
||||||
|
return convert_slab(java_name, props_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle logs with axis property
|
||||||
|
if java_name.ends_with("_log") || java_name.ends_with("_wood") {
|
||||||
|
return convert_log(java_name, props_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to basic conversion without properties
|
||||||
|
to_bedrock_block(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Java stair block to Bedrock format with proper orientation.
|
||||||
|
fn convert_stairs(
|
||||||
|
java_name: &str,
|
||||||
|
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||||
|
) -> BedrockBlock {
|
||||||
|
// Map Java stair names to Bedrock equivalents
|
||||||
|
let bedrock_name = match java_name {
|
||||||
|
"end_stone_brick_stairs" => "end_brick_stairs",
|
||||||
|
_ => java_name, // Most stairs have the same name
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
|
||||||
|
// Convert facing: Java uses "north/south/east/west", Bedrock uses "weirdo_direction" (0-3)
|
||||||
|
// Bedrock: 0=east, 1=west, 2=south, 3=north
|
||||||
|
if let Some(props) = props {
|
||||||
|
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||||
|
let direction = match facing.as_str() {
|
||||||
|
"east" => 0,
|
||||||
|
"west" => 1,
|
||||||
|
"south" => 2,
|
||||||
|
"north" => 3,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
states.insert(
|
||||||
|
"weirdo_direction".to_string(),
|
||||||
|
BedrockBlockStateValue::Int(direction),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert half: Java uses "top/bottom", Bedrock uses "upside_down_bit"
|
||||||
|
if let Some(fastnbt::Value::String(half)) = props.get("half") {
|
||||||
|
let upside_down = half == "top";
|
||||||
|
states.insert(
|
||||||
|
"upside_down_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(upside_down),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no properties were set, use defaults
|
||||||
|
if states.is_empty() {
|
||||||
|
states.insert(
|
||||||
|
"weirdo_direction".to_string(),
|
||||||
|
BedrockBlockStateValue::Int(0),
|
||||||
|
);
|
||||||
|
states.insert(
|
||||||
|
"upside_down_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BedrockBlock {
|
||||||
|
name: format!("minecraft:{bedrock_name}"),
|
||||||
|
states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Java slab block to Bedrock format with proper type.
|
||||||
|
fn convert_slab(
|
||||||
|
java_name: &str,
|
||||||
|
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||||
|
) -> BedrockBlock {
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
|
||||||
|
// Convert type: Java uses "top/bottom/double", Bedrock uses "top_slot_bit"
|
||||||
|
if let Some(props) = props {
|
||||||
|
if let Some(fastnbt::Value::String(slab_type)) = props.get("type") {
|
||||||
|
let top_slot = slab_type == "top";
|
||||||
|
states.insert(
|
||||||
|
"top_slot_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(top_slot),
|
||||||
|
);
|
||||||
|
// Note: "double" slabs in Java become full blocks in Bedrock (different block ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to bottom if not specified
|
||||||
|
if !states.contains_key("top_slot_bit") {
|
||||||
|
states.insert(
|
||||||
|
"top_slot_bit".to_string(),
|
||||||
|
BedrockBlockStateValue::Bool(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special slab name mappings (same as in to_bedrock_block)
|
||||||
|
let bedrock_name = match java_name {
|
||||||
|
"stone_slab" => "stone_block_slab",
|
||||||
|
"stone_brick_slab" => "stone_block_slab",
|
||||||
|
"oak_slab" => "wooden_slab",
|
||||||
|
"spruce_slab" => "wooden_slab",
|
||||||
|
"birch_slab" => "wooden_slab",
|
||||||
|
"jungle_slab" => "wooden_slab",
|
||||||
|
"acacia_slab" => "wooden_slab",
|
||||||
|
"dark_oak_slab" => "wooden_slab",
|
||||||
|
_ => java_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add wood_type for wooden slabs
|
||||||
|
if bedrock_name == "wooden_slab" {
|
||||||
|
let wood_type = java_name.trim_end_matches("_slab");
|
||||||
|
states.insert(
|
||||||
|
"wood_type".to_string(),
|
||||||
|
BedrockBlockStateValue::String(wood_type.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stone_slab_type for stone slabs
|
||||||
|
if bedrock_name == "stone_block_slab" {
|
||||||
|
let slab_type = if java_name == "stone_brick_slab" {
|
||||||
|
"stone_brick"
|
||||||
|
} else {
|
||||||
|
"stone"
|
||||||
|
};
|
||||||
|
states.insert(
|
||||||
|
"stone_slab_type".to_string(),
|
||||||
|
BedrockBlockStateValue::String(slab_type.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BedrockBlock {
|
||||||
|
name: format!("minecraft:{bedrock_name}"),
|
||||||
|
states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Java log/wood block to Bedrock format with proper axis.
|
||||||
|
fn convert_log(
|
||||||
|
java_name: &str,
|
||||||
|
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||||
|
) -> BedrockBlock {
|
||||||
|
let bedrock_name = java_name;
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
|
||||||
|
// Convert axis: Java uses "x/y/z", Bedrock uses "pillar_axis"
|
||||||
|
if let Some(props) = props {
|
||||||
|
if let Some(fastnbt::Value::String(axis)) = props.get("axis") {
|
||||||
|
states.insert(
|
||||||
|
"pillar_axis".to_string(),
|
||||||
|
BedrockBlockStateValue::String(axis.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to y-axis if not specified
|
||||||
|
if states.is_empty() {
|
||||||
|
states.insert(
|
||||||
|
"pillar_axis".to_string(),
|
||||||
|
BedrockBlockStateValue::String("y".to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BedrockBlock {
|
||||||
|
name: format!("minecraft:{bedrock_name}"),
|
||||||
|
states,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::block_definitions::{AIR, GRASS_BLOCK, STONE};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_blocks() {
|
||||||
|
let bedrock = to_bedrock_block(STONE);
|
||||||
|
assert_eq!(bedrock.name, "minecraft:stone");
|
||||||
|
assert!(bedrock.states.is_empty());
|
||||||
|
|
||||||
|
let bedrock = to_bedrock_block(AIR);
|
||||||
|
assert_eq!(bedrock.name, "minecraft:air");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_grass_block() {
|
||||||
|
let bedrock = to_bedrock_block(GRASS_BLOCK);
|
||||||
|
assert_eq!(bedrock.name, "minecraft:grass_block");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_colored_blocks() {
|
||||||
|
use crate::block_definitions::WHITE_CONCRETE;
|
||||||
|
let bedrock = to_bedrock_block(WHITE_CONCRETE);
|
||||||
|
assert_eq!(bedrock.name, "minecraft:concrete");
|
||||||
|
assert!(matches!(
|
||||||
|
bedrock.states.get("color"),
|
||||||
|
Some(BedrockBlockStateValue::String(s)) if s == "white"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stairs_with_properties() {
|
||||||
|
use crate::block_definitions::OAK_STAIRS;
|
||||||
|
use std::collections::HashMap as StdHashMap;
|
||||||
|
|
||||||
|
// Create Java properties for a south-facing stair
|
||||||
|
let mut props = StdHashMap::new();
|
||||||
|
props.insert(
|
||||||
|
"facing".to_string(),
|
||||||
|
fastnbt::Value::String("south".to_string()),
|
||||||
|
);
|
||||||
|
props.insert(
|
||||||
|
"half".to_string(),
|
||||||
|
fastnbt::Value::String("bottom".to_string()),
|
||||||
|
);
|
||||||
|
let java_props = fastnbt::Value::Compound(props);
|
||||||
|
|
||||||
|
let bedrock = to_bedrock_block_with_properties(OAK_STAIRS, Some(&java_props));
|
||||||
|
assert_eq!(bedrock.name, "minecraft:oak_stairs");
|
||||||
|
|
||||||
|
// Check weirdo_direction is set correctly (south = 2)
|
||||||
|
assert!(matches!(
|
||||||
|
bedrock.states.get("weirdo_direction"),
|
||||||
|
Some(BedrockBlockStateValue::Int(2))
|
||||||
|
));
|
||||||
|
|
||||||
|
// Check upside_down_bit is false for bottom half
|
||||||
|
assert!(matches!(
|
||||||
|
bedrock.states.get("upside_down_bit"),
|
||||||
|
Some(BedrockBlockStateValue::Bool(false))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stairs_upside_down() {
|
||||||
|
use crate::block_definitions::STONE_BRICK_STAIRS;
|
||||||
|
use std::collections::HashMap as StdHashMap;
|
||||||
|
|
||||||
|
// Create Java properties for an upside-down north-facing stair
|
||||||
|
let mut props = StdHashMap::new();
|
||||||
|
props.insert(
|
||||||
|
"facing".to_string(),
|
||||||
|
fastnbt::Value::String("north".to_string()),
|
||||||
|
);
|
||||||
|
props.insert(
|
||||||
|
"half".to_string(),
|
||||||
|
fastnbt::Value::String("top".to_string()),
|
||||||
|
);
|
||||||
|
let java_props = fastnbt::Value::Compound(props);
|
||||||
|
|
||||||
|
let bedrock = to_bedrock_block_with_properties(STONE_BRICK_STAIRS, Some(&java_props));
|
||||||
|
|
||||||
|
// Check weirdo_direction is set correctly (north = 3)
|
||||||
|
assert!(matches!(
|
||||||
|
bedrock.states.get("weirdo_direction"),
|
||||||
|
Some(BedrockBlockStateValue::Int(3))
|
||||||
|
));
|
||||||
|
|
||||||
|
// Check upside_down_bit is true for top half
|
||||||
|
assert!(matches!(
|
||||||
|
bedrock.states.get("upside_down_bit"),
|
||||||
|
Some(BedrockBlockStateValue::Bool(true))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,7 +236,7 @@ impl Block {
|
|||||||
155 => "chest",
|
155 => "chest",
|
||||||
156 => "red_carpet",
|
156 => "red_carpet",
|
||||||
157 => "anvil",
|
157 => "anvil",
|
||||||
158 => "jukebox",
|
158 => "note_block",
|
||||||
159 => "oak_door",
|
159 => "oak_door",
|
||||||
160 => "brewing_stand",
|
160 => "brewing_stand",
|
||||||
161 => "red_bed", // North head
|
161 => "red_bed", // North head
|
||||||
@@ -266,6 +266,7 @@ 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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,7 +668,7 @@ pub const OAK_STAIRS: Block = Block::new(144);
|
|||||||
pub const CHEST: Block = Block::new(155);
|
pub const CHEST: Block = Block::new(155);
|
||||||
pub const RED_CARPET: Block = Block::new(156);
|
pub const RED_CARPET: Block = Block::new(156);
|
||||||
pub const ANVIL: Block = Block::new(157);
|
pub const ANVIL: Block = Block::new(157);
|
||||||
pub const JUKEBOX: Block = Block::new(158);
|
pub const NOTE_BLOCK: Block = Block::new(158);
|
||||||
pub const OAK_DOOR: Block = Block::new(159);
|
pub const OAK_DOOR: Block = Block::new(159);
|
||||||
pub const BREWING_STAND: Block = Block::new(160);
|
pub const BREWING_STAND: Block = Block::new(160);
|
||||||
pub const RED_BED_NORTH_HEAD: Block = Block::new(161);
|
pub const RED_BED_NORTH_HEAD: Block = Block::new(161);
|
||||||
@@ -697,6 +698,7 @@ 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]
|
||||||
|
|||||||
706
src/clipping.rs
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
// Sutherland-Hodgman polygon clipping and related geometry utilities.
|
||||||
|
//
|
||||||
|
// Provides bbox clipping for polygons, polylines, and water rings with
|
||||||
|
// proper corner insertion for closed shapes.
|
||||||
|
|
||||||
|
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||||
|
use crate::osm_parser::ProcessedNode;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Clips a way to the bounding box using Sutherland-Hodgman for polygons or
|
||||||
|
/// simple line clipping for polylines. Preserves endpoint IDs for ring assembly.
|
||||||
|
pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
|
||||||
|
if nodes.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_closed = is_closed_polygon(nodes);
|
||||||
|
|
||||||
|
if !is_closed {
|
||||||
|
return clip_polyline_to_bbox(nodes, xzbbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all nodes are inside the bbox, return unchanged
|
||||||
|
let has_nodes_outside = nodes
|
||||||
|
.iter()
|
||||||
|
.any(|node| !xzbbox.contains(&XZPoint::new(node.x, node.z)));
|
||||||
|
|
||||||
|
if !has_nodes_outside {
|
||||||
|
return nodes.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_x = xzbbox.min_x() as f64;
|
||||||
|
let min_z = xzbbox.min_z() as f64;
|
||||||
|
let max_x = xzbbox.max_x() as f64;
|
||||||
|
let max_z = xzbbox.max_z() as f64;
|
||||||
|
|
||||||
|
let mut polygon: Vec<(f64, f64)> = nodes.iter().map(|n| (n.x as f64, n.z as f64)).collect();
|
||||||
|
|
||||||
|
polygon = clip_polygon_sutherland_hodgman(polygon, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
|
if polygon.len() < 3 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final clamping for floating-point errors
|
||||||
|
for p in &mut polygon {
|
||||||
|
p.0 = p.0.clamp(min_x, max_x);
|
||||||
|
p.1 = p.1.clamp(min_z, max_z);
|
||||||
|
}
|
||||||
|
|
||||||
|
let polygon = remove_consecutive_duplicates(polygon);
|
||||||
|
if polygon.len() < 3 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||||
|
let polygon = remove_consecutive_duplicates(polygon);
|
||||||
|
if polygon.len() < 3 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
||||||
|
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clips a water polygon ring to bbox using Sutherland-Hodgman (post-ring-merge).
|
||||||
|
pub fn clip_water_ring_to_bbox(
|
||||||
|
ring: &[ProcessedNode],
|
||||||
|
xzbbox: &XZBBox,
|
||||||
|
) -> Option<Vec<ProcessedNode>> {
|
||||||
|
if ring.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_x = xzbbox.min_x() as f64;
|
||||||
|
let min_z = xzbbox.min_z() as f64;
|
||||||
|
let max_x = xzbbox.max_x() as f64;
|
||||||
|
let max_z = xzbbox.max_z() as f64;
|
||||||
|
|
||||||
|
// Check if entire ring is inside bbox
|
||||||
|
let all_inside = ring.iter().all(|n| {
|
||||||
|
n.x as f64 >= min_x && n.x as f64 <= max_x && n.z as f64 >= min_z && n.z as f64 <= max_z
|
||||||
|
});
|
||||||
|
|
||||||
|
if all_inside {
|
||||||
|
return Some(ring.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if entire ring is outside bbox
|
||||||
|
if is_ring_outside_bbox(ring, min_x, min_z, max_x, max_z) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to f64 coordinates and ensure closed
|
||||||
|
let mut polygon: Vec<(f64, f64)> = ring.iter().map(|n| (n.x as f64, n.z as f64)).collect();
|
||||||
|
if !polygon.is_empty() && polygon.first() != polygon.last() {
|
||||||
|
polygon.push(polygon[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip with full-range clamping (water uses simpler approach)
|
||||||
|
polygon = clip_polygon_sutherland_hodgman_simple(polygon, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
|
if polygon.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all points are within bbox
|
||||||
|
let all_points_inside = polygon
|
||||||
|
.iter()
|
||||||
|
.all(|&(x, z)| x >= min_x && x <= max_x && z >= min_z && z <= max_z);
|
||||||
|
|
||||||
|
if !all_points_inside {
|
||||||
|
eprintln!("ERROR: clip_water_ring_to_bbox produced points outside bbox!");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||||
|
if polygon.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to ProcessedNode with synthetic IDs
|
||||||
|
let mut result: Vec<ProcessedNode> = polygon
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, &(x, z))| ProcessedNode {
|
||||||
|
id: 1_000_000_000 + i as u64,
|
||||||
|
tags: HashMap::new(),
|
||||||
|
x: x.clamp(min_x, max_x).round() as i32,
|
||||||
|
z: z.clamp(min_z, max_z).round() as i32,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Close the loop by matching first and last ID
|
||||||
|
if !result.is_empty() {
|
||||||
|
let first_id = result[0].id;
|
||||||
|
result.last_mut().unwrap().id = first_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Internal helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Checks if a way forms a closed polygon.
|
||||||
|
fn is_closed_polygon(nodes: &[ProcessedNode]) -> bool {
|
||||||
|
if nodes.len() < 3 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let first = nodes.first().unwrap();
|
||||||
|
let last = nodes.last().unwrap();
|
||||||
|
first.id == last.id || (first.x == last.x && first.z == last.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an entire ring is outside the bbox.
|
||||||
|
fn is_ring_outside_bbox(
|
||||||
|
ring: &[ProcessedNode],
|
||||||
|
min_x: f64,
|
||||||
|
min_z: f64,
|
||||||
|
max_x: f64,
|
||||||
|
max_z: f64,
|
||||||
|
) -> bool {
|
||||||
|
let all_left = ring.iter().all(|n| (n.x as f64) < min_x);
|
||||||
|
let all_right = ring.iter().all(|n| (n.x as f64) > max_x);
|
||||||
|
let all_top = ring.iter().all(|n| (n.z as f64) < min_z);
|
||||||
|
let all_bottom = ring.iter().all(|n| (n.z as f64) > max_z);
|
||||||
|
all_left || all_right || all_top || all_bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clips a polyline (open path) to the bounding box.
|
||||||
|
fn clip_polyline_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
|
||||||
|
if nodes.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_x = xzbbox.min_x() as f64;
|
||||||
|
let min_z = xzbbox.min_z() as f64;
|
||||||
|
let max_x = xzbbox.max_x() as f64;
|
||||||
|
let max_z = xzbbox.max_z() as f64;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..nodes.len() {
|
||||||
|
let current = &nodes[i];
|
||||||
|
let current_point = (current.x as f64, current.z as f64);
|
||||||
|
let current_inside = point_in_bbox(current_point, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
|
if current_inside {
|
||||||
|
result.push(current.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if i + 1 < nodes.len() {
|
||||||
|
let next = &nodes[i + 1];
|
||||||
|
let next_point = (next.x as f64, next.z as f64);
|
||||||
|
let next_inside = point_in_bbox(next_point, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
|
if current_inside != next_inside {
|
||||||
|
// One endpoint inside, one outside, find single intersection
|
||||||
|
let intersections =
|
||||||
|
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
|
for intersection in intersections {
|
||||||
|
let synthetic_id = nodes[0]
|
||||||
|
.id
|
||||||
|
.wrapping_mul(10000000)
|
||||||
|
.wrapping_add(result.len() as u64);
|
||||||
|
result.push(ProcessedNode {
|
||||||
|
id: synthetic_id,
|
||||||
|
x: intersection.0.round() as i32,
|
||||||
|
z: intersection.1.round() as i32,
|
||||||
|
tags: HashMap::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if !current_inside && !next_inside {
|
||||||
|
// Both endpoints outside, segment might still cross through bbox
|
||||||
|
let mut intersections =
|
||||||
|
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
|
if intersections.len() >= 2 {
|
||||||
|
// Sort intersections by distance from current point
|
||||||
|
intersections.sort_by(|a, b| {
|
||||||
|
let dist_a =
|
||||||
|
(a.0 - current_point.0).powi(2) + (a.1 - current_point.1).powi(2);
|
||||||
|
let dist_b =
|
||||||
|
(b.0 - current_point.0).powi(2) + (b.1 - current_point.1).powi(2);
|
||||||
|
dist_a
|
||||||
|
.partial_cmp(&dist_b)
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
for intersection in intersections {
|
||||||
|
let synthetic_id = nodes[0]
|
||||||
|
.id
|
||||||
|
.wrapping_mul(10000000)
|
||||||
|
.wrapping_add(result.len() as u64);
|
||||||
|
result.push(ProcessedNode {
|
||||||
|
id: synthetic_id,
|
||||||
|
x: intersection.0.round() as i32,
|
||||||
|
z: intersection.1.round() as i32,
|
||||||
|
tags: HashMap::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve endpoint IDs where possible
|
||||||
|
if result.len() >= 2 {
|
||||||
|
let tolerance = 50.0;
|
||||||
|
if let Some(first_orig) = nodes.first() {
|
||||||
|
if matches_endpoint(
|
||||||
|
(result[0].x as f64, result[0].z as f64),
|
||||||
|
first_orig,
|
||||||
|
tolerance,
|
||||||
|
) {
|
||||||
|
result[0].id = first_orig.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(last_orig) = nodes.last() {
|
||||||
|
let last_idx = result.len() - 1;
|
||||||
|
if matches_endpoint(
|
||||||
|
(result[last_idx].x as f64, result[last_idx].z as f64),
|
||||||
|
last_orig,
|
||||||
|
tolerance,
|
||||||
|
) {
|
||||||
|
result[last_idx].id = last_orig.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sutherland-Hodgman polygon clipping with edge-specific clamping.
|
||||||
|
fn clip_polygon_sutherland_hodgman(
|
||||||
|
mut polygon: Vec<(f64, f64)>,
|
||||||
|
min_x: f64,
|
||||||
|
min_z: f64,
|
||||||
|
max_x: f64,
|
||||||
|
max_z: f64,
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
// Edges: bottom, right, top, left (counter-clockwise traversal)
|
||||||
|
let bbox_edges = [
|
||||||
|
(min_x, min_z, max_x, min_z, 0), // Bottom: clamp z
|
||||||
|
(max_x, min_z, max_x, max_z, 1), // Right: clamp x
|
||||||
|
(max_x, max_z, min_x, max_z, 2), // Top: clamp z
|
||||||
|
(min_x, max_z, min_x, min_z, 3), // Left: clamp x
|
||||||
|
];
|
||||||
|
|
||||||
|
for (edge_x1, edge_z1, edge_x2, edge_z2, edge_idx) in bbox_edges {
|
||||||
|
if polygon.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut clipped = Vec::new();
|
||||||
|
let is_closed = !polygon.is_empty() && polygon.first() == polygon.last();
|
||||||
|
let edge_count = if is_closed {
|
||||||
|
polygon.len().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
polygon.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..edge_count {
|
||||||
|
let current = polygon[i];
|
||||||
|
let next = polygon.get(i + 1).copied().unwrap_or(polygon[0]);
|
||||||
|
|
||||||
|
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||||
|
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||||
|
|
||||||
|
if next_inside {
|
||||||
|
if !current_inside {
|
||||||
|
if let Some(mut intersection) = line_edge_intersection(
|
||||||
|
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||||
|
) {
|
||||||
|
// Clamp to current edge only
|
||||||
|
match edge_idx {
|
||||||
|
0 => intersection.1 = min_z,
|
||||||
|
1 => intersection.0 = max_x,
|
||||||
|
2 => intersection.1 = max_z,
|
||||||
|
3 => intersection.0 = min_x,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
clipped.push(intersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clipped.push(next);
|
||||||
|
} else if current_inside {
|
||||||
|
if let Some(mut intersection) = line_edge_intersection(
|
||||||
|
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||||
|
) {
|
||||||
|
match edge_idx {
|
||||||
|
0 => intersection.1 = min_z,
|
||||||
|
1 => intersection.0 = max_x,
|
||||||
|
2 => intersection.1 = max_z,
|
||||||
|
3 => intersection.0 = min_x,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
clipped.push(intersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
polygon = clipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
polygon
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sutherland-Hodgman with full bbox clamping (simpler, for water rings).
|
||||||
|
fn clip_polygon_sutherland_hodgman_simple(
|
||||||
|
mut polygon: Vec<(f64, f64)>,
|
||||||
|
min_x: f64,
|
||||||
|
min_z: f64,
|
||||||
|
max_x: f64,
|
||||||
|
max_z: f64,
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
let bbox_edges = [
|
||||||
|
(min_x, min_z, max_x, min_z),
|
||||||
|
(max_x, min_z, max_x, max_z),
|
||||||
|
(max_x, max_z, min_x, max_z),
|
||||||
|
(min_x, max_z, min_x, min_z),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
|
||||||
|
if polygon.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut clipped = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..(polygon.len().saturating_sub(1)) {
|
||||||
|
let current = polygon[i];
|
||||||
|
let next = polygon[i + 1];
|
||||||
|
|
||||||
|
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||||
|
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||||
|
|
||||||
|
if next_inside {
|
||||||
|
if !current_inside {
|
||||||
|
if let Some(mut intersection) = line_edge_intersection(
|
||||||
|
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||||
|
) {
|
||||||
|
intersection.0 = intersection.0.clamp(min_x, max_x);
|
||||||
|
intersection.1 = intersection.1.clamp(min_z, max_z);
|
||||||
|
clipped.push(intersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clipped.push(next);
|
||||||
|
} else if current_inside {
|
||||||
|
if let Some(mut intersection) = line_edge_intersection(
|
||||||
|
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||||
|
) {
|
||||||
|
intersection.0 = intersection.0.clamp(min_x, max_x);
|
||||||
|
intersection.1 = intersection.1.clamp(min_z, max_z);
|
||||||
|
clipped.push(intersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
polygon = clipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
polygon
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if point is inside bbox.
|
||||||
|
fn point_in_bbox(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> bool {
|
||||||
|
point.0 >= min_x && point.0 <= max_x && point.1 >= min_z && point.1 <= max_z
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if point is on the "inside" side of an edge (cross product test).
|
||||||
|
fn point_inside_edge(
|
||||||
|
point: (f64, f64),
|
||||||
|
edge_x1: f64,
|
||||||
|
edge_z1: f64,
|
||||||
|
edge_x2: f64,
|
||||||
|
edge_z2: f64,
|
||||||
|
) -> bool {
|
||||||
|
let edge_dx = edge_x2 - edge_x1;
|
||||||
|
let edge_dz = edge_z2 - edge_z1;
|
||||||
|
let point_dx = point.0 - edge_x1;
|
||||||
|
let point_dz = point.1 - edge_z1;
|
||||||
|
(edge_dx * point_dz - edge_dz * point_dx) >= 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds intersection between a line segment and an edge.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn line_edge_intersection(
|
||||||
|
line_x1: f64,
|
||||||
|
line_z1: f64,
|
||||||
|
line_x2: f64,
|
||||||
|
line_z2: f64,
|
||||||
|
edge_x1: f64,
|
||||||
|
edge_z1: f64,
|
||||||
|
edge_x2: f64,
|
||||||
|
edge_z2: f64,
|
||||||
|
) -> Option<(f64, f64)> {
|
||||||
|
let line_dx = line_x2 - line_x1;
|
||||||
|
let line_dz = line_z2 - line_z1;
|
||||||
|
let edge_dx = edge_x2 - edge_x1;
|
||||||
|
let edge_dz = edge_z2 - edge_z1;
|
||||||
|
|
||||||
|
let denom = line_dx * edge_dz - line_dz * edge_dx;
|
||||||
|
if denom.abs() < 1e-10 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dx = edge_x1 - line_x1;
|
||||||
|
let dz = edge_z1 - line_z1;
|
||||||
|
let t = (dx * edge_dz - dz * edge_dx) / denom;
|
||||||
|
|
||||||
|
if (0.0..=1.0).contains(&t) {
|
||||||
|
Some((line_x1 + t * line_dx, line_z1 + t * line_dz))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds intersections between a line segment and bbox edges.
|
||||||
|
fn find_bbox_intersections(
|
||||||
|
start: (f64, f64),
|
||||||
|
end: (f64, f64),
|
||||||
|
min_x: f64,
|
||||||
|
min_z: f64,
|
||||||
|
max_x: f64,
|
||||||
|
max_z: f64,
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
let mut intersections = Vec::new();
|
||||||
|
|
||||||
|
let bbox_edges = [
|
||||||
|
(min_x, min_z, max_x, min_z),
|
||||||
|
(max_x, min_z, max_x, max_z),
|
||||||
|
(max_x, max_z, min_x, max_z),
|
||||||
|
(min_x, max_z, min_x, min_z),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
|
||||||
|
if let Some(intersection) = line_edge_intersection(
|
||||||
|
start.0, start.1, end.0, end.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||||
|
) {
|
||||||
|
let on_edge = point_in_bbox(intersection, min_x, min_z, max_x, max_z)
|
||||||
|
&& ((intersection.0 == min_x || intersection.0 == max_x)
|
||||||
|
|| (intersection.1 == min_z || intersection.1 == max_z));
|
||||||
|
|
||||||
|
if on_edge {
|
||||||
|
intersections.push(intersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
intersections
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
|
||||||
|
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
|
||||||
|
let eps = 0.5;
|
||||||
|
|
||||||
|
let on_left = (point.0 - min_x).abs() < eps;
|
||||||
|
let on_right = (point.0 - max_x).abs() < eps;
|
||||||
|
let on_bottom = (point.1 - min_z).abs() < eps;
|
||||||
|
let on_top = (point.1 - max_z).abs() < eps;
|
||||||
|
|
||||||
|
// Handle corners (assign to edge in counter-clockwise order)
|
||||||
|
if on_bottom && on_left {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
if on_bottom && on_right {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if on_top && on_right {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if on_top && on_left {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if on_bottom {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if on_right {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if on_top {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if on_left {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns corners to insert when traversing from edge1 to edge2 via shorter path.
|
||||||
|
fn get_corners_between_edges(
|
||||||
|
edge1: i32,
|
||||||
|
edge2: i32,
|
||||||
|
min_x: f64,
|
||||||
|
min_z: f64,
|
||||||
|
max_x: f64,
|
||||||
|
max_z: f64,
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
if edge1 == edge2 || edge1 < 0 || edge2 < 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let corners = [
|
||||||
|
(max_x, min_z), // 0: bottom-right
|
||||||
|
(max_x, max_z), // 1: top-right
|
||||||
|
(min_x, max_z), // 2: top-left
|
||||||
|
(min_x, min_z), // 3: bottom-left
|
||||||
|
];
|
||||||
|
|
||||||
|
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
|
||||||
|
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
|
||||||
|
|
||||||
|
// Opposite edges: don't insert corners
|
||||||
|
if ccw_dist == 2 && cw_dist == 2 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
if ccw_dist <= cw_dist {
|
||||||
|
let mut current = edge1;
|
||||||
|
for _ in 0..ccw_dist {
|
||||||
|
result.push(corners[current as usize]);
|
||||||
|
current = (current + 1) % 4;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut current = edge1;
|
||||||
|
for _ in 0..cw_dist {
|
||||||
|
current = (current + 4 - 1) % 4;
|
||||||
|
result.push(corners[current as usize]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts bbox corners where polygon transitions between different bbox edges.
|
||||||
|
fn insert_bbox_corners(
|
||||||
|
polygon: Vec<(f64, f64)>,
|
||||||
|
min_x: f64,
|
||||||
|
min_z: f64,
|
||||||
|
max_x: f64,
|
||||||
|
max_z: f64,
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
if polygon.len() < 3 {
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Vec::with_capacity(polygon.len() + 4);
|
||||||
|
|
||||||
|
for i in 0..polygon.len() {
|
||||||
|
let current = polygon[i];
|
||||||
|
let next = polygon[(i + 1) % polygon.len()];
|
||||||
|
|
||||||
|
result.push(current);
|
||||||
|
|
||||||
|
let edge1 = get_bbox_edge(current, min_x, min_z, max_x, max_z);
|
||||||
|
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
|
||||||
|
|
||||||
|
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
|
||||||
|
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
|
||||||
|
result.push(corner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes consecutive duplicate points (within epsilon tolerance).
|
||||||
|
fn remove_consecutive_duplicates(polygon: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
|
||||||
|
if polygon.is_empty() {
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
let eps = 0.1;
|
||||||
|
let mut result: Vec<(f64, f64)> = Vec::with_capacity(polygon.len());
|
||||||
|
|
||||||
|
for p in &polygon {
|
||||||
|
if let Some(last) = result.last() {
|
||||||
|
if (p.0 - last.0).abs() < eps && (p.1 - last.1).abs() < eps {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(*p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check first/last duplicates for closed polygons
|
||||||
|
if result.len() > 1 {
|
||||||
|
let first = result.first().unwrap();
|
||||||
|
let last = result.last().unwrap();
|
||||||
|
if (first.0 - last.0).abs() < eps && (first.1 - last.1).abs() < eps {
|
||||||
|
result.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a clipped coordinate matches an original endpoint.
|
||||||
|
fn matches_endpoint(coord: (f64, f64), endpoint: &ProcessedNode, tolerance: f64) -> bool {
|
||||||
|
let dx = (coord.0 - endpoint.x as f64).abs();
|
||||||
|
let dz = (coord.1 - endpoint.z as f64).abs();
|
||||||
|
dx * dx + dz * dz < tolerance * tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assigns node IDs to clipped coordinates, preserving original endpoint IDs.
|
||||||
|
fn assign_node_ids_preserving_endpoints(
|
||||||
|
original_nodes: &[ProcessedNode],
|
||||||
|
clipped_coords: Vec<(f64, f64)>,
|
||||||
|
way_id: u64,
|
||||||
|
) -> Vec<ProcessedNode> {
|
||||||
|
if clipped_coords.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let original_first = original_nodes.first();
|
||||||
|
let original_last = original_nodes.last();
|
||||||
|
let tolerance = 50.0;
|
||||||
|
let last_index = clipped_coords.len() - 1;
|
||||||
|
|
||||||
|
clipped_coords
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, coord)| {
|
||||||
|
let is_first = i == 0;
|
||||||
|
let is_last = i == last_index;
|
||||||
|
|
||||||
|
if is_first || is_last {
|
||||||
|
if let Some(first) = original_first {
|
||||||
|
if matches_endpoint(coord, first, tolerance) {
|
||||||
|
return ProcessedNode {
|
||||||
|
id: first.id,
|
||||||
|
x: coord.0.round() as i32,
|
||||||
|
z: coord.1.round() as i32,
|
||||||
|
tags: HashMap::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(last) = original_last {
|
||||||
|
if matches_endpoint(coord, last, tolerance) {
|
||||||
|
return ProcessedNode {
|
||||||
|
id: last.id,
|
||||||
|
x: coord.0.round() as i32,
|
||||||
|
z: coord.1.round() as i32,
|
||||||
|
tags: HashMap::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessedNode {
|
||||||
|
id: way_id.wrapping_mul(10000000).wrapping_add(i as u64),
|
||||||
|
x: coord.0.round() as i32,
|
||||||
|
z: coord.1.round() as i32,
|
||||||
|
tags: HashMap::new(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
mod xzbbox;
|
pub mod xzbbox;
|
||||||
mod xzpoint;
|
mod xzpoint;
|
||||||
mod xzvector;
|
mod xzvector;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
mod rectangle;
|
pub mod rectangle;
|
||||||
mod xzbbox_enum;
|
mod xzbbox_enum;
|
||||||
|
|
||||||
pub use xzbbox_enum::XZBBox;
|
pub use xzbbox_enum::XZBBox;
|
||||||
|
|||||||
@@ -1,35 +1,454 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
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::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::osm_parser::ProcessedElement;
|
use crate::osm_parser::ProcessedElement;
|
||||||
use crate::progress::emit_gui_progress_update;
|
use crate::parallel_processing::{
|
||||||
use crate::world_editor::WorldEditor;
|
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")]
|
||||||
|
use crate::telemetry::{send_log, LogLevel};
|
||||||
|
use crate::unit_processing::{process_unit_refs, SharedProcessingData};
|
||||||
|
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,
|
||||||
|
llbbox: LLBBox,
|
||||||
ground: Ground,
|
ground: Ground,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let region_dir: String = format!("{}/region", args.path);
|
// Default to Java format when called from CLI
|
||||||
let mut editor: WorldEditor = WorldEditor::new(®ion_dir, &xzbbox);
|
let options = GenerationOptions {
|
||||||
|
path: args.path.clone(),
|
||||||
|
format: WorldFormat::JavaAnvil,
|
||||||
|
level_name: None,
|
||||||
|
spawn_point: None,
|
||||||
|
};
|
||||||
|
|
||||||
println!("{} Processing data...", "[4/7]".bold());
|
// Use sequential by default (parallel has correctness issues)
|
||||||
|
// 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()
|
||||||
|
};
|
||||||
|
|
||||||
// Set ground reference in the editor to enable elevation-aware block placement
|
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options, parallel_config)
|
||||||
editor.set_ground(&ground);
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate world with explicit format options (used by GUI for Bedrock support)
|
||||||
|
pub fn generate_world_with_options(
|
||||||
|
elements: Vec<ProcessedElement>,
|
||||||
|
xzbbox: XZBBox,
|
||||||
|
llbbox: LLBBox,
|
||||||
|
ground: Ground,
|
||||||
|
args: &Args,
|
||||||
|
options: GenerationOptions,
|
||||||
|
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}")
|
||||||
@@ -40,7 +459,8 @@ pub fn generate_world(
|
|||||||
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;
|
||||||
|
|
||||||
for element in &elements {
|
// Process elements by draining in insertion order
|
||||||
|
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 {
|
||||||
@@ -58,36 +478,68 @@ pub fn generate_world(
|
|||||||
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);
|
buildings::generate_buildings(&mut editor, way, args, None, &flood_fill_cache);
|
||||||
} else if way.tags.contains_key("highway") {
|
} else if way.tags.contains_key("highway") {
|
||||||
highways::generate_highways(&mut editor, element, args);
|
highways::generate_highways(
|
||||||
|
&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(&mut editor, way, args);
|
landuse::generate_landuse(
|
||||||
|
&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(&mut editor, element, args);
|
natural::generate_natural(
|
||||||
|
&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);
|
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||||
} else if way.tags.contains_key("leisure") {
|
} else if way.tags.contains_key("leisure") {
|
||||||
leisure::generate_leisure(&mut editor, way, args);
|
leisure::generate_leisure(
|
||||||
|
&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 way.tags.contains_key("waterway") {
|
} else if let Some(val) = way.tags.get("waterway") {
|
||||||
waterways::generate_waterways(&mut editor, way);
|
if val == "dock" {
|
||||||
|
// docks count as water areas
|
||||||
|
water_areas::generate_water_area_from_way(&mut editor, way, &xzbbox);
|
||||||
|
} else {
|
||||||
|
waterways::generate_waterways(&mut editor, way);
|
||||||
|
}
|
||||||
} else if way.tags.contains_key("bridge") {
|
} else if way.tags.contains_key("bridge") {
|
||||||
//bridges::generate_bridges(&mut editor, way, ground_level); // TODO FIX
|
//bridges::generate_bridges(&mut editor, way, ground_level); // TODO FIX
|
||||||
} else if way.tags.contains_key("railway") {
|
} else if way.tags.contains_key("railway") {
|
||||||
railways::generate_railways(&mut editor, way);
|
railways::generate_railways(&mut editor, way);
|
||||||
|
} else if way.tags.contains_key("roller_coaster") {
|
||||||
|
railways::generate_roller_coaster(&mut editor, way);
|
||||||
} else if way.tags.contains_key("aeroway") || way.tags.contains_key("area:aeroway")
|
} else if way.tags.contains_key("aeroway") || way.tags.contains_key("area:aeroway")
|
||||||
{
|
{
|
||||||
highways::generate_aeroway(&mut editor, way, args);
|
highways::generate_aeroway(&mut editor, way, args);
|
||||||
} 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") {
|
||||||
@@ -95,13 +547,25 @@ pub fn generate_world(
|
|||||||
} 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(&mut editor, element, args);
|
natural::generate_natural(
|
||||||
|
&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);
|
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||||
} 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(&mut editor, element, args);
|
highways::generate_highways(
|
||||||
|
&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") {
|
||||||
@@ -110,30 +574,59 @@ pub fn generate_world(
|
|||||||
}
|
}
|
||||||
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(&mut editor, rel, args);
|
buildings::generate_building_from_relation(
|
||||||
} else if rel.tags.contains_key("water")
|
|
||||||
|| rel.tags.get("natural") == Some(&"water".to_string())
|
|
||||||
{
|
|
||||||
water_areas::generate_water_areas(&mut editor, rel);
|
|
||||||
} else if rel.tags.contains_key("natural") {
|
|
||||||
natural::generate_natural_from_relation(&mut editor, rel, args);
|
|
||||||
} else if rel.tags.contains_key("landuse") {
|
|
||||||
landuse::generate_landuse_from_relation(&mut editor, rel, args);
|
|
||||||
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
|
||||||
leisure::generate_leisure_from_relation(&mut editor, rel, args);
|
|
||||||
} else if rel.tags.contains_key("man_made") {
|
|
||||||
man_made::generate_man_made(
|
|
||||||
&mut editor,
|
&mut editor,
|
||||||
&ProcessedElement::Relation(rel.clone()),
|
rel,
|
||||||
args,
|
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(&mut editor, rel, &xzbbox);
|
||||||
|
} else if rel.tags.contains_key("natural") {
|
||||||
|
natural::generate_natural_from_relation(
|
||||||
|
&mut editor,
|
||||||
|
rel,
|
||||||
|
args,
|
||||||
|
&flood_fill_cache,
|
||||||
|
&building_footprints,
|
||||||
|
);
|
||||||
|
} else if rel.tags.contains_key("landuse") {
|
||||||
|
landuse::generate_landuse_from_relation(
|
||||||
|
&mut editor,
|
||||||
|
rel,
|
||||||
|
args,
|
||||||
|
&flood_fill_cache,
|
||||||
|
&building_footprints,
|
||||||
|
);
|
||||||
|
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||||
|
leisure::generate_leisure_from_relation(
|
||||||
|
&mut editor,
|
||||||
|
rel,
|
||||||
|
args,
|
||||||
|
&flood_fill_cache,
|
||||||
|
&building_footprints,
|
||||||
|
);
|
||||||
|
} else if rel.tags.contains_key("man_made") {
|
||||||
|
man_made::generate_man_made(&mut editor, &element, 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;
|
||||||
@@ -157,44 +650,72 @@ pub fn generate_world(
|
|||||||
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;
|
||||||
|
|
||||||
let groundlayer_block = GRASS_BLOCK;
|
// Check if terrain elevation is enabled; when disabled, we can skip ground level lookups entirely
|
||||||
|
let terrain_enabled = ground.elevation_enabled;
|
||||||
|
|
||||||
for x in xzbbox.min_x()..=xzbbox.max_x() {
|
// Process ground generation chunk-by-chunk for better cache locality.
|
||||||
for z in xzbbox.min_z()..=xzbbox.max_z() {
|
// This keeps the same region/chunk HashMap entries hot in CPU cache,
|
||||||
// Add default dirt and grass layer if there isn't a stone layer already
|
// rather than jumping between regions on every Z iteration.
|
||||||
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
|
let min_chunk_x = xzbbox.min_x() >> 4;
|
||||||
editor.set_block(groundlayer_block, x, 0, z, None, None);
|
let max_chunk_x = xzbbox.max_x() >> 4;
|
||||||
editor.set_block(DIRT, x, -1, z, None, None);
|
let min_chunk_z = xzbbox.min_z() >> 4;
|
||||||
editor.set_block(DIRT, x, -2, z, None, None);
|
let max_chunk_z = xzbbox.max_z() >> 4;
|
||||||
}
|
|
||||||
|
|
||||||
// Fill underground with stone
|
for chunk_x in min_chunk_x..=max_chunk_x {
|
||||||
if args.fillground {
|
for chunk_z in min_chunk_z..=max_chunk_z {
|
||||||
// Fill from bedrock+1 to 3 blocks below ground with stone
|
// Calculate the block range for this chunk, clamped to bbox
|
||||||
editor.fill_blocks_absolute(
|
let chunk_min_x = (chunk_x << 4).max(xzbbox.min_x());
|
||||||
STONE,
|
let chunk_max_x = ((chunk_x << 4) + 15).min(xzbbox.max_x());
|
||||||
x,
|
let chunk_min_z = (chunk_z << 4).max(xzbbox.min_z());
|
||||||
MIN_Y + 1,
|
let chunk_max_z = ((chunk_z << 4) + 15).min(xzbbox.max_z());
|
||||||
z,
|
|
||||||
x,
|
|
||||||
editor.get_absolute_y(x, -3, z),
|
|
||||||
z,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Generate a bedrock level at MIN_Y
|
|
||||||
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
|
|
||||||
|
|
||||||
block_counter += 1;
|
for x in chunk_min_x..=chunk_max_x {
|
||||||
if block_counter % batch_size == 0 {
|
for z in chunk_min_z..=chunk_max_z {
|
||||||
ground_pb.inc(batch_size);
|
// Get ground level, when terrain is enabled, look it up once per block
|
||||||
}
|
// When disabled, use constant ground_level (no function call overhead)
|
||||||
|
let ground_y = if terrain_enabled {
|
||||||
|
editor.get_ground_level(x, z)
|
||||||
|
} else {
|
||||||
|
args.ground_level
|
||||||
|
};
|
||||||
|
|
||||||
gui_progress_grnd += progress_increment_grnd;
|
// Add default dirt and grass layer if there isn't a stone layer already
|
||||||
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
|
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||||
emit_gui_progress_update(gui_progress_grnd, "");
|
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||||
last_emitted_progress = gui_progress_grnd;
|
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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,30 +738,109 @@ pub fn generate_world(
|
|||||||
// 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 let Some(spawn_coords) = &args.spawn_point {
|
if world_format == WorldFormat::JavaAnvil {
|
||||||
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().lng(),
|
|
||||||
args.bbox.min().lat(),
|
args.bbox.min().lat(),
|
||||||
args.bbox.max().lng(),
|
args.bbox.min().lng(),
|
||||||
args.bbox.max().lat()
|
args.bbox.max().lat(),
|
||||||
|
args.bbox.max().lng()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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,
|
ground.as_ref(),
|
||||||
) {
|
) {
|
||||||
eprintln!("Warning: Failed to update spawn point Y coordinate: {e}");
|
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||||
|
eprintln!("Warning: {}", warning_msg);
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
send_log(LogLevel::Warning, &warning_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
// For Bedrock format, emit event to open the mcworld file
|
||||||
println!("{}", "Done! World generation completed.".green().bold());
|
if world_format == WorldFormat::BedrockMcWorld {
|
||||||
Ok(())
|
if let Some(path_str) = output_path.to_str() {
|
||||||
|
emit_open_mcworld_file(path_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information needed to generate a map preview after world generation is complete
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MapPreviewInfo {
|
||||||
|
pub world_path: PathBuf,
|
||||||
|
pub min_x: i32,
|
||||||
|
pub max_x: i32,
|
||||||
|
pub min_z: i32,
|
||||||
|
pub max_z: i32,
|
||||||
|
pub world_area: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MapPreviewInfo {
|
||||||
|
/// Create MapPreviewInfo from world bounds
|
||||||
|
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
|
||||||
|
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||||
|
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||||
|
Self {
|
||||||
|
world_path,
|
||||||
|
min_x: xzbbox.min_x(),
|
||||||
|
max_x: xzbbox.max_x(),
|
||||||
|
min_z: xzbbox.min_z(),
|
||||||
|
max_z: xzbbox.max_z(),
|
||||||
|
world_area: world_width * world_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
|
||||||
|
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||||
|
|
||||||
|
/// Start map preview generation in a background thread.
|
||||||
|
/// This should be called AFTER the world generation is complete, the session lock is released,
|
||||||
|
/// and the GUI has been notified of 100% completion.
|
||||||
|
///
|
||||||
|
/// For Java worlds only, and only if the world area is within limits.
|
||||||
|
pub fn start_map_preview_generation(info: MapPreviewInfo) {
|
||||||
|
if info.world_area > MAX_MAP_PREVIEW_AREA {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
// Use catch_unwind to prevent any panic from affecting the application
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
map_renderer::render_world_map(
|
||||||
|
&info.world_path,
|
||||||
|
info.min_x,
|
||||||
|
info.max_x,
|
||||||
|
info.min_z,
|
||||||
|
info.max_z,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Ok(_path)) => {
|
||||||
|
// Notify the GUI that the map preview is ready
|
||||||
|
emit_map_preview_ready();
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
127
src/deterministic_rng.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
//! 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,11 +2,19 @@ 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::flood_fill_area;
|
use crate::deterministic_rng::element_rng;
|
||||||
|
use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
||||||
|
use crate::floodfill_cache::FloodFillCache;
|
||||||
use crate::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(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
pub fn generate_amenities(
|
||||||
|
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 {
|
||||||
@@ -42,18 +50,14 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
|||||||
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;
|
||||||
|
|
||||||
let polygon_coords: Vec<(i32, i32)> = element
|
// Use pre-computed flood fill from cache
|
||||||
.nodes()
|
let floor_area: Vec<(i32, i32)> =
|
||||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
|
||||||
.collect();
|
|
||||||
|
|
||||||
if polygon_coords.is_empty() {
|
if floor_area.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);
|
||||||
@@ -80,8 +84,10 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
|||||||
"bench" => {
|
"bench" => {
|
||||||
// Place a bench
|
// Place a bench
|
||||||
if let Some(pt) = first_node {
|
if let Some(pt) = first_node {
|
||||||
// 50% chance to 90 degrees rotate the bench using if
|
// Use deterministic RNG for consistent bench orientation across region boundaries
|
||||||
if rand::random::<bool>() {
|
let mut rng = element_rng(element.id());
|
||||||
|
// 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);
|
||||||
@@ -92,22 +98,12 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"vending" => {
|
|
||||||
// Place vending machine blocks
|
|
||||||
if let Some(pt) = first_node {
|
|
||||||
editor.set_block(IRON_BLOCK, pt.x, 1, pt.z, None, None);
|
|
||||||
editor.set_block(IRON_BLOCK, pt.x, 2, pt.z, None, None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"shelter" => {
|
"shelter" => {
|
||||||
let roof_block: Block = STONE_BRICK_SLAB;
|
let roof_block: Block = STONE_BRICK_SLAB;
|
||||||
|
|
||||||
let polygon_coords: Vec<(i32, i32)> = element
|
// Use pre-computed flood fill from cache
|
||||||
.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_area(&polygon_coords, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute_element(element, 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,37 +3,97 @@ 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
|
// TODO FIX - This handles ways with bridge=yes tag (e.g., highway bridges)
|
||||||
#[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; // Fixed height
|
let bridge_height = 3; // Height above the ground level
|
||||||
|
|
||||||
|
// Get start and end node elevations and use MAX for level bridge deck
|
||||||
|
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
|
||||||
|
let bridge_deck_ground_y = if element.nodes.len() >= 2 {
|
||||||
|
let start_node = &element.nodes[0];
|
||||||
|
let end_node = &element.nodes[element.nodes.len() - 1];
|
||||||
|
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||||
|
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||||
|
start_y.max(end_y)
|
||||||
|
} else {
|
||||||
|
return; // Need at least 2 nodes for a bridge
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate total bridge length for ramp positioning
|
||||||
|
let total_length: f64 = element
|
||||||
|
.nodes
|
||||||
|
.windows(2)
|
||||||
|
.map(|pair| {
|
||||||
|
let dx = (pair[1].x - pair[0].x) as f64;
|
||||||
|
let dz = (pair[1].z - pair[0].z) as f64;
|
||||||
|
(dx * dx + dz * dz).sqrt()
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
if total_length == 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut accumulated_length: f64 = 0.0;
|
||||||
|
|
||||||
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 total_length = points.len();
|
let ramp_length = (total_length * 0.15).clamp(6.0, 20.0) as usize; // 15% of bridge, min 6, max 20 blocks
|
||||||
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() {
|
||||||
let height = if idx < ramp_length {
|
// Calculate progress along this segment
|
||||||
|
let segment_progress = if points.len() > 1 {
|
||||||
|
idx as f64 / (points.len() - 1) as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate overall progress along the entire bridge
|
||||||
|
let point_distance = accumulated_length + segment_progress * segment_length;
|
||||||
|
let overall_progress = (point_distance / total_length).clamp(0.0, 1.0);
|
||||||
|
let total_len_usize = total_length as usize;
|
||||||
|
let overall_idx = (overall_progress * total_len_usize as f64) as usize;
|
||||||
|
|
||||||
|
// Calculate ramp height offset
|
||||||
|
let ramp_offset = if overall_idx < ramp_length {
|
||||||
// Start ramp (rising)
|
// Start ramp (rising)
|
||||||
(idx * bridge_height) / ramp_length
|
(overall_idx as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
||||||
} else if idx >= total_length - ramp_length {
|
} else if overall_idx >= total_len_usize.saturating_sub(ramp_length) {
|
||||||
// End ramp (descending)
|
// End ramp (descending)
|
||||||
((total_length - idx) * bridge_height) / ramp_length
|
let dist_from_end = total_len_usize - overall_idx;
|
||||||
|
(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(LIGHT_GRAY_CONCRETE, *x + dx, height as i32, *z, None, None);
|
editor.set_block_absolute(
|
||||||
|
LIGHT_GRAY_CONCRETE,
|
||||||
|
*x + dx,
|
||||||
|
bridge_y,
|
||||||
|
*z,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accumulated_length += segment_length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ 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::flood_fill_area;
|
use crate::floodfill_cache::FloodFillCache;
|
||||||
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;
|
||||||
@@ -28,6 +29,7 @@ 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") {
|
||||||
@@ -43,10 +45,9 @@ 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);
|
||||||
|
|
||||||
// Cache floodfill result: compute once and reuse throughout
|
// Use pre-computed flood fill from cache
|
||||||
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_area(&polygon_coords, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute(element, 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
|
||||||
@@ -121,7 +122,8 @@ 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;
|
||||||
let mut rng = rand::thread_rng();
|
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -386,7 +388,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, args.timeout.as_ref());
|
generate_bridge(editor, element, flood_fill_cache, args.timeout.as_ref());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1484,6 +1486,7 @@ 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
|
||||||
@@ -1495,7 +1498,13 @@ 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(editor, &member.way, args, Some(relation_levels));
|
generate_buildings(
|
||||||
|
editor,
|
||||||
|
&member.way,
|
||||||
|
args,
|
||||||
|
Some(relation_levels),
|
||||||
|
flood_fill_cache,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1516,52 +1525,18 @@ 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;
|
||||||
|
|
||||||
// Process the nodes to create bridge pathways and railings
|
// Calculate bridge level offset based on the "level" tag
|
||||||
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
|
||||||
@@ -1572,8 +1547,51 @@ 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(floor_block, x, bridge_y_offset, z, None, None);
|
editor.set_block_absolute(floor_block, x, floor_y, z, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,82 @@ 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::flood_fill_area;
|
use crate::floodfill_cache::FloodFillCache;
|
||||||
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;
|
||||||
|
|
||||||
pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
/// Type alias for highway connectivity map
|
||||||
|
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||||
|
|
||||||
|
/// Minimum terrain dip (in blocks) below max endpoint elevation to classify a bridge as valley-spanning
|
||||||
|
const VALLEY_BRIDGE_THRESHOLD: i32 = 7;
|
||||||
|
|
||||||
|
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
||||||
|
pub fn generate_highways(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
element: &ProcessedElement,
|
||||||
|
args: &Args,
|
||||||
|
highway_connectivity: &HighwayConnectivityMap,
|
||||||
|
flood_fill_cache: &FloodFillCache,
|
||||||
|
) {
|
||||||
|
generate_highways_internal(
|
||||||
|
editor,
|
||||||
|
element,
|
||||||
|
args,
|
||||||
|
highway_connectivity,
|
||||||
|
flood_fill_cache,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||||
|
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
|
||||||
|
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
||||||
|
|
||||||
|
for element in elements {
|
||||||
|
if let ProcessedElement::Way(way) = element {
|
||||||
|
if way.tags.contains_key("highway") {
|
||||||
|
let layer_value = way
|
||||||
|
.tags
|
||||||
|
.get("layer")
|
||||||
|
.and_then(|layer| layer.parse::<i32>().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Treat negative layers as ground level (0) for connectivity
|
||||||
|
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||||
|
|
||||||
|
// Add connectivity for start and end nodes
|
||||||
|
if !way.nodes.is_empty() {
|
||||||
|
let start_node = &way.nodes[0];
|
||||||
|
let end_node = &way.nodes[way.nodes.len() - 1];
|
||||||
|
|
||||||
|
let start_coord = (start_node.x, start_node.z);
|
||||||
|
let end_coord = (end_node.x, end_node.z);
|
||||||
|
|
||||||
|
connectivity_map
|
||||||
|
.entry(start_coord)
|
||||||
|
.or_default()
|
||||||
|
.push(layer_value);
|
||||||
|
connectivity_map
|
||||||
|
.entry(end_coord)
|
||||||
|
.or_default()
|
||||||
|
.push(layer_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectivity_map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal function that generates highways with connectivity context for elevation handling
|
||||||
|
fn generate_highways_internal(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
element: &ProcessedElement,
|
||||||
|
args: &Args,
|
||||||
|
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>, // Maps node coordinates to list of layers that connect to this node
|
||||||
|
flood_fill_cache: &FloodFillCache,
|
||||||
|
) {
|
||||||
if let Some(highway_type) = element.tags().get("highway") {
|
if let Some(highway_type) = element.tags().get("highway") {
|
||||||
if highway_type == "street_lamp" {
|
if highway_type == "street_lamp" {
|
||||||
// Handle street lamps
|
// Handle street lamps
|
||||||
@@ -77,14 +148,9 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the area using flood fill or by iterating through the nodes
|
// Fill the area using flood fill cache
|
||||||
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_area(&polygon_coords, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute(way, 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);
|
||||||
@@ -97,13 +163,22 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
let mut add_outline = false;
|
let mut add_outline = false;
|
||||||
let scale_factor = args.scale;
|
let scale_factor = args.scale;
|
||||||
|
|
||||||
// Skip if 'layer' or 'level' is negative in the tags
|
// Check if this is a bridge - bridges need special elevation handling
|
||||||
if let Some(layer) = element.tags().get("layer") {
|
// to span across valleys instead of following terrain
|
||||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
|
||||||
return;
|
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Parse the layer value for elevation calculation
|
||||||
|
let layer_value = element
|
||||||
|
.tags()
|
||||||
|
.get("layer")
|
||||||
|
.and_then(|layer| layer.parse::<i32>().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Treat negative layers as ground level (0)
|
||||||
|
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||||
|
|
||||||
|
// Skip if 'level' is negative in the tags (indoor mapping)
|
||||||
if let Some(level) = element.tags().get("level") {
|
if let Some(level) = element.tags().get("level") {
|
||||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||||
return;
|
return;
|
||||||
@@ -120,10 +195,14 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
block_type = DIRT_PATH;
|
block_type = DIRT_PATH;
|
||||||
block_range = 1;
|
block_range = 1;
|
||||||
}
|
}
|
||||||
"motorway" | "primary" => {
|
"motorway" | "primary" | "trunk" => {
|
||||||
block_range = 5;
|
block_range = 5;
|
||||||
add_stripe = true;
|
add_stripe = true;
|
||||||
}
|
}
|
||||||
|
"secondary" => {
|
||||||
|
block_range = 4;
|
||||||
|
add_stripe = true;
|
||||||
|
}
|
||||||
"tertiary" => {
|
"tertiary" => {
|
||||||
add_stripe = true;
|
add_stripe = true;
|
||||||
}
|
}
|
||||||
@@ -173,7 +252,98 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
block_range = ((block_range as f64) * scale_factor).floor() as i32;
|
block_range = ((block_range as f64) * scale_factor).floor() as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate elevation based on layer
|
||||||
|
const LAYER_HEIGHT_STEP: i32 = 6; // Each layer is 6 blocks higher/lower
|
||||||
|
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
||||||
|
|
||||||
|
// Check if we need slopes at start and end
|
||||||
|
// This is used for overpasses that need ramps to ground-level roads
|
||||||
|
let needs_start_slope =
|
||||||
|
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
||||||
|
let needs_end_slope = should_add_slope_at_node(
|
||||||
|
&way.nodes[way.nodes.len() - 1],
|
||||||
|
layer_value,
|
||||||
|
highway_connectivity,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate total way length for slope distribution (needed before valley bridge check)
|
||||||
|
let total_way_length = calculate_way_length(way);
|
||||||
|
|
||||||
|
// For bridges: detect if this spans a valley by checking terrain profile
|
||||||
|
// A valley bridge has terrain that dips significantly below the endpoints
|
||||||
|
// Skip valley detection entirely if terrain is disabled (no valleys in flat terrain)
|
||||||
|
// Skip very short bridges (< 25 blocks) as they're unlikely to span significant valleys
|
||||||
|
let terrain_enabled = editor
|
||||||
|
.get_ground()
|
||||||
|
.map(|g| g.elevation_enabled)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let (is_valley_bridge, bridge_deck_y) =
|
||||||
|
if is_bridge && terrain_enabled && way.nodes.len() >= 2 && total_way_length >= 25 {
|
||||||
|
let start_node = &way.nodes[0];
|
||||||
|
let end_node = &way.nodes[way.nodes.len() - 1];
|
||||||
|
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||||
|
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||||
|
let max_endpoint_y = start_y.max(end_y);
|
||||||
|
|
||||||
|
// Sample terrain at middle nodes only (excluding endpoints we already have)
|
||||||
|
// This avoids redundant get_ground_level() calls
|
||||||
|
let middle_nodes = &way.nodes[1..way.nodes.len().saturating_sub(1)];
|
||||||
|
let sampled_min = if middle_nodes.is_empty() {
|
||||||
|
// No middle nodes, just use endpoints
|
||||||
|
start_y.min(end_y)
|
||||||
|
} else {
|
||||||
|
// Sample up to 3 middle points (5 total with endpoints) for performance
|
||||||
|
// Valleys are wide terrain features, so sparse sampling is sufficient
|
||||||
|
let sample_count = middle_nodes.len().min(3);
|
||||||
|
let step = if sample_count > 1 {
|
||||||
|
(middle_nodes.len() - 1) / (sample_count - 1)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
|
||||||
|
middle_nodes
|
||||||
|
.iter()
|
||||||
|
.step_by(step.max(1))
|
||||||
|
.map(|node| editor.get_ground_level(node.x, node.z))
|
||||||
|
.min()
|
||||||
|
.unwrap_or(max_endpoint_y)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include endpoint elevations in the minimum calculation
|
||||||
|
let min_terrain_y = sampled_min.min(start_y).min(end_y);
|
||||||
|
|
||||||
|
// If ANY sampled point along the bridge is significantly lower than the max endpoint,
|
||||||
|
// treat as valley bridge
|
||||||
|
let is_valley = min_terrain_y < max_endpoint_y - VALLEY_BRIDGE_THRESHOLD;
|
||||||
|
|
||||||
|
if is_valley {
|
||||||
|
(true, max_endpoint_y)
|
||||||
|
} else {
|
||||||
|
(false, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(false, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if this is a short isolated elevated segment (layer > 0), if so, treat as ground level
|
||||||
|
let is_short_isolated_elevated =
|
||||||
|
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
||||||
|
|
||||||
|
// Override elevation and slopes for short isolated segments
|
||||||
|
let (effective_elevation, effective_start_slope, effective_end_slope) =
|
||||||
|
if is_short_isolated_elevated {
|
||||||
|
(0, false, false) // Treat as ground level
|
||||||
|
} else {
|
||||||
|
(base_elevation, needs_start_slope, needs_end_slope)
|
||||||
|
};
|
||||||
|
|
||||||
|
let slope_length = (total_way_length as f32 * 0.35).clamp(15.0, 50.0) as usize; // 35% of way length, max 50 blocks, min 15 blocks
|
||||||
|
|
||||||
// Iterate over nodes to create the highway
|
// Iterate over nodes to create the highway
|
||||||
|
let mut segment_index = 0;
|
||||||
|
let total_segments = way.nodes.len() - 1;
|
||||||
|
|
||||||
for node in &way.nodes {
|
for node in &way.nodes {
|
||||||
if let Some(prev) = previous_node {
|
if let Some(prev) = previous_node {
|
||||||
let (x1, z1) = prev;
|
let (x1, z1) = prev;
|
||||||
@@ -181,17 +351,41 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
let z2: i32 = node.z;
|
let z2: i32 = node.z;
|
||||||
|
|
||||||
// Generate the line of coordinates between the two nodes
|
// Generate the line of coordinates between the two nodes
|
||||||
// we don't care about the y because it's going to get overwritten
|
|
||||||
// I'm not sure if we'll keep it this way
|
|
||||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||||
bresenham_line(x1, 0, z1, x2, 0, z2);
|
bresenham_line(x1, 0, z1, x2, 0, z2);
|
||||||
|
|
||||||
|
// Calculate elevation for this segment
|
||||||
|
let segment_length = bresenham_points.len();
|
||||||
|
|
||||||
// Variables to manage dashed line pattern
|
// Variables to manage dashed line pattern
|
||||||
let mut stripe_length: i32 = 0;
|
let mut stripe_length: i32 = 0;
|
||||||
let dash_length: i32 = (5.0 * scale_factor).ceil() as i32; // Length of the solid part of the stripe
|
let dash_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32; // Length of the gap part of the stripe
|
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||||
|
|
||||||
|
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
||||||
|
// Calculate Y elevation for this point
|
||||||
|
// For valley bridges: use fixed deck height (max of endpoints) to stay level
|
||||||
|
// For overpasses and regular roads: use terrain-relative elevation with slopes
|
||||||
|
let (current_y, use_absolute_y) = if is_valley_bridge {
|
||||||
|
// Valley bridge deck is level at the maximum endpoint elevation
|
||||||
|
// Don't add base_elevation - the layer tag indicates it's above water/road,
|
||||||
|
// not that it should be higher than the terrain endpoints
|
||||||
|
(bridge_deck_y, true)
|
||||||
|
} else {
|
||||||
|
// Regular road or overpass: use terrain-relative calculation with ramps
|
||||||
|
let y = calculate_point_elevation(
|
||||||
|
segment_index,
|
||||||
|
point_index,
|
||||||
|
segment_length,
|
||||||
|
total_segments,
|
||||||
|
effective_elevation,
|
||||||
|
effective_start_slope,
|
||||||
|
effective_end_slope,
|
||||||
|
slope_length,
|
||||||
|
);
|
||||||
|
(y, false)
|
||||||
|
};
|
||||||
|
|
||||||
for (x, _, z) in bresenham_points {
|
|
||||||
// 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 {
|
||||||
for dz in -block_range..=block_range {
|
for dz in -block_range..=block_range {
|
||||||
@@ -206,52 +400,150 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
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 {
|
||||||
editor.set_block(
|
if use_absolute_y {
|
||||||
WHITE_CONCRETE,
|
editor.set_block_absolute(
|
||||||
|
WHITE_CONCRETE,
|
||||||
|
set_x,
|
||||||
|
current_y,
|
||||||
|
set_z,
|
||||||
|
Some(&[BLACK_CONCRETE]),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editor.set_block(
|
||||||
|
WHITE_CONCRETE,
|
||||||
|
set_x,
|
||||||
|
current_y,
|
||||||
|
set_z,
|
||||||
|
Some(&[BLACK_CONCRETE]),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if use_absolute_y {
|
||||||
|
editor.set_block_absolute(
|
||||||
|
BLACK_CONCRETE,
|
||||||
set_x,
|
set_x,
|
||||||
0,
|
current_y,
|
||||||
set_z,
|
set_z,
|
||||||
Some(&[BLACK_CONCRETE]),
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
editor.set_block(
|
editor.set_block(
|
||||||
BLACK_CONCRETE,
|
BLACK_CONCRETE,
|
||||||
set_x,
|
set_x,
|
||||||
0,
|
current_y,
|
||||||
set_z,
|
set_z,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if set_z % 2 < 1 {
|
} else if set_z % 2 < 1 {
|
||||||
editor.set_block(
|
if use_absolute_y {
|
||||||
WHITE_CONCRETE,
|
editor.set_block_absolute(
|
||||||
|
WHITE_CONCRETE,
|
||||||
|
set_x,
|
||||||
|
current_y,
|
||||||
|
set_z,
|
||||||
|
Some(&[BLACK_CONCRETE]),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editor.set_block(
|
||||||
|
WHITE_CONCRETE,
|
||||||
|
set_x,
|
||||||
|
current_y,
|
||||||
|
set_z,
|
||||||
|
Some(&[BLACK_CONCRETE]),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if use_absolute_y {
|
||||||
|
editor.set_block_absolute(
|
||||||
|
BLACK_CONCRETE,
|
||||||
set_x,
|
set_x,
|
||||||
0,
|
current_y,
|
||||||
set_z,
|
set_z,
|
||||||
Some(&[BLACK_CONCRETE]),
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
editor.set_block(
|
editor.set_block(
|
||||||
BLACK_CONCRETE,
|
BLACK_CONCRETE,
|
||||||
set_x,
|
set_x,
|
||||||
0,
|
current_y,
|
||||||
set_z,
|
set_z,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else if use_absolute_y {
|
||||||
editor.set_block(
|
editor.set_block_absolute(
|
||||||
block_type,
|
block_type,
|
||||||
set_x,
|
set_x,
|
||||||
0,
|
current_y,
|
||||||
set_z,
|
set_z,
|
||||||
None,
|
None,
|
||||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
editor.set_block(
|
||||||
|
block_type,
|
||||||
|
set_x,
|
||||||
|
current_y,
|
||||||
|
set_z,
|
||||||
|
None,
|
||||||
|
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stone brick foundation underneath elevated highways/bridges for thickness
|
||||||
|
if (effective_elevation > 0 || use_absolute_y) && current_y > 0 {
|
||||||
|
// Add 1 layer of stone bricks underneath the highway surface
|
||||||
|
if use_absolute_y {
|
||||||
|
editor.set_block_absolute(
|
||||||
|
STONE_BRICKS,
|
||||||
|
set_x,
|
||||||
|
current_y - 1,
|
||||||
|
set_z,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editor.set_block(
|
||||||
|
STONE_BRICKS,
|
||||||
|
set_x,
|
||||||
|
current_y - 1,
|
||||||
|
set_z,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add support pillars for elevated highways/bridges
|
||||||
|
if (effective_elevation != 0 || use_absolute_y) && current_y > 0 {
|
||||||
|
if use_absolute_y {
|
||||||
|
add_highway_support_pillar_absolute(
|
||||||
|
editor,
|
||||||
|
set_x,
|
||||||
|
current_y,
|
||||||
|
set_z,
|
||||||
|
dx,
|
||||||
|
dz,
|
||||||
|
block_range,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
add_highway_support_pillar(
|
||||||
|
editor,
|
||||||
|
set_x,
|
||||||
|
current_y,
|
||||||
|
set_z,
|
||||||
|
dx,
|
||||||
|
dz,
|
||||||
|
block_range,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,43 +554,76 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
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;
|
||||||
editor.set_block(
|
if use_absolute_y {
|
||||||
LIGHT_GRAY_CONCRETE,
|
editor.set_block_absolute(
|
||||||
outline_x,
|
LIGHT_GRAY_CONCRETE,
|
||||||
0,
|
outline_x,
|
||||||
outline_z,
|
current_y,
|
||||||
None,
|
outline_z,
|
||||||
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;
|
||||||
editor.set_block(
|
if use_absolute_y {
|
||||||
LIGHT_GRAY_CONCRETE,
|
editor.set_block_absolute(
|
||||||
outline_x,
|
LIGHT_GRAY_CONCRETE,
|
||||||
0,
|
outline_x,
|
||||||
outline_z,
|
current_y,
|
||||||
None,
|
outline_z,
|
||||||
None,
|
None,
|
||||||
);
|
None,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editor.set_block(
|
||||||
|
LIGHT_GRAY_CONCRETE,
|
||||||
|
outline_x,
|
||||||
|
current_y,
|
||||||
|
outline_z,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a dashed white line in the middle for larger roads
|
// Add a dashed white line in the middle for larger roads
|
||||||
if add_stripe {
|
if add_stripe {
|
||||||
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;
|
||||||
editor.set_block(
|
if use_absolute_y {
|
||||||
WHITE_CONCRETE,
|
editor.set_block_absolute(
|
||||||
stripe_x,
|
WHITE_CONCRETE,
|
||||||
0,
|
stripe_x,
|
||||||
stripe_z,
|
current_y,
|
||||||
Some(&[BLACK_CONCRETE]),
|
stripe_z,
|
||||||
None,
|
Some(&[BLACK_CONCRETE]),
|
||||||
);
|
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
|
||||||
@@ -308,6 +633,8 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
segment_index += 1;
|
||||||
}
|
}
|
||||||
previous_node = Some((node.x, node.z));
|
previous_node = Some((node.x, node.z));
|
||||||
}
|
}
|
||||||
@@ -315,6 +642,171 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to determine if a slope should be added at a specific node
|
||||||
|
fn should_add_slope_at_node(
|
||||||
|
node: &crate::osm_parser::ProcessedNode,
|
||||||
|
current_layer: i32,
|
||||||
|
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>,
|
||||||
|
) -> bool {
|
||||||
|
let node_coord = (node.x, node.z);
|
||||||
|
|
||||||
|
// If we don't have connectivity information, always add slopes for non-zero layers
|
||||||
|
if highway_connectivity.is_empty() {
|
||||||
|
return current_layer != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are other highways at different layers connected to this node
|
||||||
|
if let Some(connected_layers) = highway_connectivity.get(&node_coord) {
|
||||||
|
// Count how many ways are at the same layer as current way
|
||||||
|
let same_layer_count = connected_layers
|
||||||
|
.iter()
|
||||||
|
.filter(|&&layer| layer == current_layer)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// If this is the only way at this layer connecting to this node, we need a slope
|
||||||
|
// (unless we're at ground level and connecting to ground level ways)
|
||||||
|
if same_layer_count <= 1 {
|
||||||
|
return current_layer != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are multiple ways at the same layer, don't add slope
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// No other highways connected, add slope if not at ground level
|
||||||
|
current_layer != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to calculate the total length of a way in blocks
|
||||||
|
fn calculate_way_length(way: &ProcessedWay) -> usize {
|
||||||
|
let mut total_length = 0;
|
||||||
|
let mut previous_node: Option<&crate::osm_parser::ProcessedNode> = None;
|
||||||
|
|
||||||
|
for node in &way.nodes {
|
||||||
|
if let Some(prev) = previous_node {
|
||||||
|
let dx = (node.x - prev.x).abs();
|
||||||
|
let dz = (node.z - prev.z).abs();
|
||||||
|
total_length += ((dx * dx + dz * dz) as f32).sqrt() as usize;
|
||||||
|
}
|
||||||
|
previous_node = Some(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
total_length
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the Y elevation for a specific point along the highway
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn calculate_point_elevation(
|
||||||
|
segment_index: usize,
|
||||||
|
point_index: usize,
|
||||||
|
segment_length: usize,
|
||||||
|
total_segments: usize,
|
||||||
|
base_elevation: i32,
|
||||||
|
needs_start_slope: bool,
|
||||||
|
needs_end_slope: bool,
|
||||||
|
slope_length: usize,
|
||||||
|
) -> i32 {
|
||||||
|
// If no slopes needed, return base elevation
|
||||||
|
if !needs_start_slope && !needs_end_slope {
|
||||||
|
return base_elevation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total distance from start
|
||||||
|
let total_distance_from_start = segment_index * segment_length + point_index;
|
||||||
|
let total_way_length = total_segments * segment_length;
|
||||||
|
|
||||||
|
// Ensure we have reasonable values
|
||||||
|
if total_way_length == 0 || slope_length == 0 {
|
||||||
|
return base_elevation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start slope calculation - gradual rise from ground level
|
||||||
|
if needs_start_slope && total_distance_from_start <= slope_length {
|
||||||
|
let slope_progress = total_distance_from_start as f32 / slope_length as f32;
|
||||||
|
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
||||||
|
return elevation_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End slope calculation - gradual descent to ground level
|
||||||
|
if needs_end_slope
|
||||||
|
&& total_distance_from_start >= (total_way_length.saturating_sub(slope_length))
|
||||||
|
{
|
||||||
|
let distance_from_end = total_way_length - total_distance_from_start;
|
||||||
|
let slope_progress = distance_from_end as f32 / slope_length as f32;
|
||||||
|
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
||||||
|
return elevation_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middle section at full elevation
|
||||||
|
base_elevation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add support pillars for elevated highways
|
||||||
|
fn add_highway_support_pillar(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
x: i32,
|
||||||
|
highway_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 {
|
||||||
|
// Add pillar from ground to highway level
|
||||||
|
for y in 1..highway_y {
|
||||||
|
editor.set_block(STONE_BRICKS, x, y, z, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pillar base
|
||||||
|
for base_dx in -1..=1 {
|
||||||
|
for base_dz in -1..=1 {
|
||||||
|
editor.set_block(STONE_BRICKS, x + base_dx, 0, z + base_dz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,16 +1,26 @@
|
|||||||
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::flood_fill_area;
|
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||||
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(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
|
pub fn generate_landuse(
|
||||||
|
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,
|
||||||
@@ -22,13 +32,13 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
if residential_tag == "rural" {
|
if residential_tag == "rural" {
|
||||||
GRASS_BLOCK
|
GRASS_BLOCK
|
||||||
} else {
|
} else {
|
||||||
STONE_BRICKS
|
STONE_BRICKS // Placeholder, will be randomized per-block
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"commercial" => SMOOTH_STONE,
|
"commercial" => SMOOTH_STONE, // Placeholder, will be randomized per-block
|
||||||
"education" => POLISHED_ANDESITE,
|
"education" => POLISHED_ANDESITE,
|
||||||
"religious" => POLISHED_ANDESITE,
|
"religious" => POLISHED_ANDESITE,
|
||||||
"industrial" => COBBLESTONE,
|
"industrial" => STONE, // Placeholder, will be randomized per-block
|
||||||
"military" => GRAY_CONCRETE,
|
"military" => GRAY_CONCRETE,
|
||||||
"railway" => GRAVEL,
|
"railway" => GRAVEL,
|
||||||
"landfill" => {
|
"landfill" => {
|
||||||
@@ -44,19 +54,56 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
_ => GRASS_BLOCK,
|
_ => GRASS_BLOCK,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the area of the landuse element
|
// Get the area of the landuse element using cache
|
||||||
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
|
let floor_area: Vec<(i32, i32)> =
|
||||||
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute(element, 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 {
|
||||||
if landuse_tag == "traffic_island" {
|
// Apply per-block randomness for certain landuse types
|
||||||
editor.set_block(block_type, x, 1, z, None, None);
|
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||||
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
|
||||||
editor.set_block(block_type, x, 0, z, None, Some(&[SPONGE]));
|
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 {
|
} else {
|
||||||
editor.set_block(block_type, x, 0, z, None, None);
|
block_type
|
||||||
|
};
|
||||||
|
|
||||||
|
if landuse_tag == "traffic_island" {
|
||||||
|
editor.set_block(actual_block, x, 1, z, None, None);
|
||||||
|
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
||||||
|
editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));
|
||||||
|
} else {
|
||||||
|
editor.set_block(actual_block, x, 0, z, None, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add specific features for different landuse types
|
// Add specific features for different landuse types
|
||||||
@@ -84,7 +131,7 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
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));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
@@ -94,7 +141,7 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
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));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} 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,
|
||||||
@@ -105,7 +152,11 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
};
|
};
|
||||||
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 {
|
||||||
editor.set_block(GRASS, x, 1, z, None, None);
|
if rng.gen_range(0..100) < 12 {
|
||||||
|
editor.set_block(FERN, x, 1, z, None, None);
|
||||||
|
} else {
|
||||||
|
editor.set_block(GRASS, x, 1, z, None, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +258,8 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
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..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
1..=8 => editor.set_block(FERN, x, 1, z, None, None),
|
||||||
|
9..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +268,8 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
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..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||||
|
3..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,11 +278,13 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
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));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
@@ -237,11 +292,12 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
}
|
}
|
||||||
"orchard" => {
|
"orchard" => {
|
||||||
if x % 18 == 0 && z % 10 == 0 {
|
if x % 18 == 0 && z % 10 == 0 {
|
||||||
Tree::create(editor, (x, 1, z));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} 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..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||||
|
3..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,12 +331,20 @@ 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(editor, &member.way.clone(), args);
|
generate_landuse(
|
||||||
|
editor,
|
||||||
|
&member.way.clone(),
|
||||||
|
args,
|
||||||
|
flood_fill_cache,
|
||||||
|
building_footprints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +366,13 @@ pub fn generate_landuse_from_relation(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate landuse area from combined way
|
// Generate landuse area from combined way
|
||||||
generate_landuse(editor, &combined_way, args);
|
generate_landuse(
|
||||||
|
editor,
|
||||||
|
&combined_way,
|
||||||
|
args,
|
||||||
|
flood_fill_cache,
|
||||||
|
building_footprints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +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::flood_fill_area;
|
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||||
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(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
|
pub fn generate_leisure(
|
||||||
|
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);
|
||||||
@@ -18,6 +25,7 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
"park" | "nature_reserve" | "garden" | "disc_golf_course" | "golf_course" => {
|
"park" | "nature_reserve" | "garden" | "disc_golf_course" | "golf_course" => {
|
||||||
GRASS_BLOCK
|
GRASS_BLOCK
|
||||||
}
|
}
|
||||||
|
"schoolyard" => BLACK_CONCRETE,
|
||||||
"playground" | "recreation_ground" | "pitch" | "beach_resort" | "dog_park" => {
|
"playground" | "recreation_ground" | "pitch" | "beach_resort" | "dog_park" => {
|
||||||
if let Some(surface) = element.tags.get("surface") {
|
if let Some(surface) = element.tags.get("surface") {
|
||||||
match surface.as_str() {
|
match surface.as_str() {
|
||||||
@@ -73,15 +81,13 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
previous_node = Some((node.x, node.z));
|
previous_node = Some((node.x, node.z));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flood-fill the interior of the leisure area
|
// Flood-fill the interior of the leisure area using cache
|
||||||
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_area(&polygon_coords, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||||
|
|
||||||
|
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||||
|
let mut rng = element_rng(element.id);
|
||||||
|
|
||||||
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);
|
||||||
@@ -90,7 +96,6 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
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 {
|
||||||
@@ -114,7 +119,7 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
}
|
}
|
||||||
105..120 => {
|
105..120 => {
|
||||||
// Tree
|
// Tree
|
||||||
Tree::create(editor, (x, 1, z));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -122,7 +127,6 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
|||||||
|
|
||||||
// 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 {
|
||||||
@@ -175,12 +179,20 @@ 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(editor, &member.way, args);
|
generate_leisure(
|
||||||
|
editor,
|
||||||
|
&member.way,
|
||||||
|
args,
|
||||||
|
flood_fill_cache,
|
||||||
|
building_footprints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +212,12 @@ pub fn generate_leisure_from_relation(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate leisure area from combined way
|
// Generate leisure area from combined way
|
||||||
generate_leisure(editor, &combined_way, args);
|
generate_leisure(
|
||||||
|
editor,
|
||||||
|
&combined_way,
|
||||||
|
args,
|
||||||
|
flood_fill_cache,
|
||||||
|
building_footprints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub fn generate_man_made(editor: &mut WorldEditor, element: &ProcessedElement, _
|
|||||||
"chimney" => generate_chimney(editor, element),
|
"chimney" => generate_chimney(editor, element),
|
||||||
"water_well" => generate_water_well(editor, element),
|
"water_well" => generate_water_well(editor, element),
|
||||||
"water_tower" => generate_water_tower(editor, element),
|
"water_tower" => generate_water_tower(editor, element),
|
||||||
|
"mast" => generate_antenna(editor, element),
|
||||||
_ => {} // Unknown man_made type, ignore
|
_ => {} // Unknown man_made type, ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,6 @@ fn generate_antenna(editor: &mut WorldEditor, element: &ProcessedElement) {
|
|||||||
Some(h) => h.parse::<i32>().unwrap_or(20).min(40), // Max 40 blocks
|
Some(h) => h.parse::<i32>().unwrap_or(20).min(40), // Max 40 blocks
|
||||||
None => match element.tags().get("tower:type").map(|s| s.as_str()) {
|
None => match element.tags().get("tower:type").map(|s| s.as_str()) {
|
||||||
Some("communication") => 20,
|
Some("communication") => 20,
|
||||||
Some("transmission") => 25,
|
|
||||||
Some("cellular") => 15,
|
Some("cellular") => 15,
|
||||||
_ => 20,
|
_ => 20,
|
||||||
},
|
},
|
||||||
@@ -249,6 +249,7 @@ pub fn generate_man_made_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
|
|||||||
"chimney" => generate_chimney(editor, &element),
|
"chimney" => generate_chimney(editor, &element),
|
||||||
"water_well" => generate_water_well(editor, &element),
|
"water_well" => generate_water_well(editor, &element),
|
||||||
"water_tower" => generate_water_tower(editor, &element),
|
"water_tower" => generate_water_tower(editor, &element),
|
||||||
|
"mast" => generate_antenna(editor, &element),
|
||||||
_ => {} // Unknown man_made type, ignore
|
_ => {} // Unknown man_made type, ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
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::flood_fill_area;
|
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||||
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(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
pub fn generate_natural(
|
||||||
|
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));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut previous_node: Option<(i32, i32)> = None;
|
let mut previous_node: Option<(i32, i32)> = None;
|
||||||
@@ -69,17 +76,13 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
|||||||
previous_node = Some((x, z));
|
previous_node = Some((x, z));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are natural nodes, flood-fill the area
|
// If there are natural nodes, flood-fill the area using cache
|
||||||
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_area(&polygon_coords, args.timeout.as_ref());
|
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
||||||
|
|
||||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||||
|
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);
|
||||||
@@ -132,7 +135,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
|||||||
}
|
}
|
||||||
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));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} 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,
|
||||||
@@ -161,7 +164,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
|||||||
}
|
}
|
||||||
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));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} 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,
|
||||||
@@ -220,7 +223,11 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
|||||||
// 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(editor, (x, 1, z));
|
Tree::create(
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -304,6 +311,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
|||||||
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
|
||||||
@@ -416,7 +424,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
|||||||
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));
|
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||||
} 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) {
|
||||||
@@ -448,12 +456,20 @@ 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(editor, &ProcessedElement::Way(member.way.clone()), args);
|
generate_natural(
|
||||||
|
editor,
|
||||||
|
&ProcessedElement::Way((*member.way).clone()),
|
||||||
|
args,
|
||||||
|
flood_fill_cache,
|
||||||
|
building_footprints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +491,13 @@ pub fn generate_natural_from_relation(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate natural area from combined way
|
// Generate natural area from combined way
|
||||||
generate_natural(editor, &ProcessedElement::Way(combined_way), args);
|
generate_natural(
|
||||||
|
editor,
|
||||||
|
&ProcessedElement::Way(combined_way),
|
||||||
|
args,
|
||||||
|
flood_fill_cache,
|
||||||
|
building_footprints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,3 +174,71 @@ fn determine_rail_direction(
|
|||||||
(None, None) => RAIL_NORTH_SOUTH,
|
(None, None) => RAIL_NORTH_SOUTH,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_roller_coaster(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||||
|
if let Some(roller_coaster) = element.tags.get("roller_coaster") {
|
||||||
|
if roller_coaster == "track" {
|
||||||
|
// Check if it's indoor (skip if yes)
|
||||||
|
if let Some(indoor) = element.tags.get("indoor") {
|
||||||
|
if indoor == "yes" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if layer is negative (skip if yes)
|
||||||
|
if let Some(layer) = element.tags.get("layer") {
|
||||||
|
if let Ok(layer_value) = layer.parse::<i32>() {
|
||||||
|
if layer_value < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let elevation_height = 4; // 4 blocks in the air
|
||||||
|
let pillar_interval = 6; // Support pillars every 6 blocks
|
||||||
|
|
||||||
|
for i in 1..element.nodes.len() {
|
||||||
|
let prev_node = element.nodes[i - 1].xz();
|
||||||
|
let cur_node = element.nodes[i].xz();
|
||||||
|
|
||||||
|
let points = bresenham_line(prev_node.x, 0, prev_node.z, cur_node.x, 0, cur_node.z);
|
||||||
|
let smoothed_points = smooth_diagonal_rails(&points);
|
||||||
|
|
||||||
|
for j in 0..smoothed_points.len() {
|
||||||
|
let (bx, _, bz) = smoothed_points[j];
|
||||||
|
|
||||||
|
// Place track foundation at elevation height
|
||||||
|
editor.set_block(IRON_BLOCK, bx, elevation_height, bz, None, None);
|
||||||
|
|
||||||
|
let prev = if j > 0 {
|
||||||
|
Some(smoothed_points[j - 1])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let next = if j < smoothed_points.len() - 1 {
|
||||||
|
Some(smoothed_points[j + 1])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let rail_block = determine_rail_direction(
|
||||||
|
(bx, bz),
|
||||||
|
prev.map(|(x, _, z)| (x, z)),
|
||||||
|
next.map(|(x, _, z)| (x, z)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Place rail on top of the foundation
|
||||||
|
editor.set_block(rail_block, bx, elevation_height + 1, bz, None, None);
|
||||||
|
|
||||||
|
// Place support pillars every pillar_interval blocks
|
||||||
|
if bx % pillar_interval == 0 && bz % pillar_interval == 0 {
|
||||||
|
// Create a pillar from ground level up to the track
|
||||||
|
for y in 1..elevation_height {
|
||||||
|
editor.set_block(IRON_BLOCK, bx, y, bz, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
|||||||
'6' => Some(RED_BED_SOUTH_FOOT), // Bed South Foot
|
'6' => Some(RED_BED_SOUTH_FOOT), // Bed South Foot
|
||||||
'7' => Some(RED_BED_WEST_HEAD), // Bed West Head
|
'7' => Some(RED_BED_WEST_HEAD), // Bed West Head
|
||||||
'8' => Some(RED_BED_WEST_FOOT), // Bed West Foot
|
'8' => Some(RED_BED_WEST_FOOT), // Bed West Foot
|
||||||
'H' => Some(CHEST), // Chest
|
// 'H' => Some(CHEST), // Chest
|
||||||
'L' => Some(CAULDRON), // Cauldron
|
'L' => Some(CAULDRON), // Cauldron
|
||||||
'A' => Some(ANVIL), // Anvil
|
'A' => Some(ANVIL), // Anvil
|
||||||
'P' => Some(OAK_PRESSURE_PLATE), // Pressure Plate
|
'P' => Some(OAK_PRESSURE_PLATE), // Pressure Plate
|
||||||
@@ -145,7 +145,7 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
|||||||
Some(DARK_OAK_DOOR_LOWER)
|
Some(DARK_OAK_DOOR_LOWER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'J' => Some(JUKEBOX), // Jukebox
|
'J' => Some(NOTE_BLOCK), // Note block
|
||||||
'G' => Some(GLOWSTONE), // Glowstone
|
'G' => Some(GLOWSTONE), // Glowstone
|
||||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||||
'T' => Some(WHITE_CARPET), // White Carpet
|
'T' => Some(WHITE_CARPET), // White Carpet
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -107,7 +109,25 @@ pub struct Tree<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tree<'_> {
|
impl Tree<'_> {
|
||||||
pub fn create(editor: &mut WorldEditor, (x, y, z): Coord) {
|
/// Creates a tree at the specified coordinates.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `editor` - The world editor to place blocks
|
||||||
|
/// * `(x, y, z)` - The base coordinates for the tree
|
||||||
|
/// * `building_footprints` - Optional bitmap of (x, z) coordinates that are inside buildings.
|
||||||
|
/// If provided, trees will not be placed at coordinates within this bitmap.
|
||||||
|
pub fn create(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
(x, y, z): Coord,
|
||||||
|
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||||
|
) {
|
||||||
|
// 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());
|
||||||
@@ -115,7 +135,9 @@ impl Tree<'_> {
|
|||||||
blacklist.extend(Self::get_functional_blocks());
|
blacklist.extend(Self::get_functional_blocks());
|
||||||
blacklist.push(WATER);
|
blacklist.push(WATER);
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||||
|
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||||
|
let mut rng = coord_rng(x, z, 0);
|
||||||
|
|
||||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
||||||
1 => TreeType::Oak,
|
1 => TreeType::Oak,
|
||||||
@@ -307,7 +329,7 @@ impl Tree<'_> {
|
|||||||
FURNACE,
|
FURNACE,
|
||||||
ANVIL,
|
ANVIL,
|
||||||
BREWING_STAND,
|
BREWING_STAND,
|
||||||
JUKEBOX,
|
NOTE_BLOCK,
|
||||||
BOOKSHELF,
|
BOOKSHELF,
|
||||||
CAULDRON,
|
CAULDRON,
|
||||||
// Beds
|
// Beds
|
||||||
|
|||||||
@@ -1,19 +1,40 @@
|
|||||||
|
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::{
|
use crate::{
|
||||||
block_definitions::WATER,
|
block_definitions::WATER,
|
||||||
coordinate_system::cartesian::XZPoint,
|
coordinate_system::cartesian::{XZBBox, XZPoint},
|
||||||
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation},
|
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay},
|
||||||
world_editor::WorldEditor,
|
world_editor::WorldEditor,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn generate_water_areas(editor: &mut WorldEditor, element: &ProcessedRelation) {
|
pub fn generate_water_area_from_way(
|
||||||
let start_time = Instant::now();
|
editor: &mut WorldEditor,
|
||||||
|
element: &ProcessedWay,
|
||||||
|
_xzbbox: &XZBBox,
|
||||||
|
) {
|
||||||
|
let outers = [element.nodes.clone()];
|
||||||
|
if !verify_closed_rings(&outers) {
|
||||||
|
println!("Skipping way {} due to invalid polygon", element.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_water_areas(editor, &outers, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_water_areas_from_relation(
|
||||||
|
editor: &mut WorldEditor,
|
||||||
|
element: &ProcessedRelation,
|
||||||
|
xzbbox: &XZBBox,
|
||||||
|
) {
|
||||||
// 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.tags.get("natural") == Some(&"water".to_string());
|
|| element
|
||||||
|
.tags
|
||||||
|
.get("natural")
|
||||||
|
.map(|val| val == "water" || val == "bay")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
if !is_water {
|
if !is_water {
|
||||||
return;
|
return;
|
||||||
@@ -36,79 +57,132 @@ pub fn generate_water_areas(editor: &mut WorldEditor, element: &ProcessedRelatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each outer polygon individually
|
// Preserve OSM-defined outer/inner roles without modification
|
||||||
for (i, outer_nodes) in outers.iter().enumerate() {
|
merge_way_segments(&mut outers);
|
||||||
let mut individual_outers = vec![outer_nodes.clone()];
|
|
||||||
|
|
||||||
merge_loopy_loops(&mut individual_outers);
|
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
|
||||||
if !verify_loopy_loops(&individual_outers) {
|
outers = outers
|
||||||
println!(
|
.into_iter()
|
||||||
"Skipping invalid outer polygon {} for relation {}",
|
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||||
i + 1,
|
.collect();
|
||||||
element.id
|
merge_way_segments(&mut inners);
|
||||||
);
|
inners = inners
|
||||||
continue; // Skip this outer if it's not valid
|
.into_iter()
|
||||||
|
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !verify_closed_rings(&outers) {
|
||||||
|
// For clipped multipolygons, some loops may not close perfectly
|
||||||
|
// Instead of force-closing with straight lines (which creates wedges),
|
||||||
|
// filter out unclosed loops and only render the properly closed ones
|
||||||
|
|
||||||
|
// Filter: Keep only loops that are already closed OR can be closed within 1 block
|
||||||
|
outers.retain(|loop_nodes| {
|
||||||
|
if loop_nodes.len() < 3 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let first = &loop_nodes[0];
|
||||||
|
let last = loop_nodes.last().unwrap();
|
||||||
|
let dx = (first.x - last.x).abs();
|
||||||
|
let dz = (first.z - last.z).abs();
|
||||||
|
|
||||||
|
// Keep if already closed by ID or endpoints are within 1 block
|
||||||
|
first.id == last.id || (dx <= 1 && dz <= 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now close the remaining loops that are within 1 block tolerance
|
||||||
|
for loop_nodes in outers.iter_mut() {
|
||||||
|
let first = loop_nodes[0].clone();
|
||||||
|
let last_idx = loop_nodes.len() - 1;
|
||||||
|
if loop_nodes[0].id != loop_nodes[last_idx].id {
|
||||||
|
// Endpoints are close (within tolerance), close the loop
|
||||||
|
loop_nodes.push(first);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
merge_loopy_loops(&mut inners);
|
// If no valid outer loops remain, skip the relation
|
||||||
if !verify_loopy_loops(&inners) {
|
if outers.is_empty() {
|
||||||
// If inners are invalid, process outer without inners
|
return;
|
||||||
let empty_inners: Vec<Vec<ProcessedNode>> = vec![];
|
|
||||||
let mut temp_inners = empty_inners;
|
|
||||||
merge_loopy_loops(&mut temp_inners);
|
|
||||||
|
|
||||||
let (min_x, min_z) = editor.get_min_coords();
|
|
||||||
let (max_x, max_z) = editor.get_max_coords();
|
|
||||||
let individual_outers_xz: Vec<Vec<XZPoint>> = individual_outers
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
|
||||||
.collect();
|
|
||||||
let empty_inners_xz: Vec<Vec<XZPoint>> = vec![];
|
|
||||||
|
|
||||||
inverse_floodfill(
|
|
||||||
min_x,
|
|
||||||
min_z,
|
|
||||||
max_x,
|
|
||||||
max_z,
|
|
||||||
individual_outers_xz,
|
|
||||||
empty_inners_xz,
|
|
||||||
editor,
|
|
||||||
start_time,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (min_x, min_z) = editor.get_min_coords();
|
// Verify again after filtering and closing
|
||||||
let (max_x, max_z) = editor.get_max_coords();
|
if !verify_closed_rings(&outers) {
|
||||||
let individual_outers_xz: Vec<Vec<XZPoint>> = individual_outers
|
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||||
.iter()
|
return;
|
||||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
}
|
||||||
.collect();
|
|
||||||
let inners_xz: Vec<Vec<XZPoint>> = inners
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
inverse_floodfill(
|
|
||||||
min_x,
|
|
||||||
min_z,
|
|
||||||
max_x,
|
|
||||||
max_z,
|
|
||||||
individual_outers_xz,
|
|
||||||
inners_xz,
|
|
||||||
editor,
|
|
||||||
start_time,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
merge_way_segments(&mut inners);
|
||||||
|
if !verify_closed_rings(&inners) {
|
||||||
|
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_water_areas(editor, &outers, &inners);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merges ways that share nodes into full loops
|
fn generate_water_areas(
|
||||||
fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
editor: &mut WorldEditor,
|
||||||
|
outers: &[Vec<ProcessedNode>],
|
||||||
|
inners: &[Vec<ProcessedNode>],
|
||||||
|
) {
|
||||||
|
// Calculate polygon bounding box to limit fill area
|
||||||
|
let mut poly_min_x = i32::MAX;
|
||||||
|
let mut poly_min_z = i32::MAX;
|
||||||
|
let mut poly_max_x = i32::MIN;
|
||||||
|
let mut poly_max_z = i32::MIN;
|
||||||
|
|
||||||
|
for outer in outers {
|
||||||
|
for node in outer {
|
||||||
|
poly_min_x = poly_min_x.min(node.x);
|
||||||
|
poly_min_z = poly_min_z.min(node.z);
|
||||||
|
poly_max_x = poly_max_x.max(node.x);
|
||||||
|
poly_max_z = poly_max_z.max(node.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no valid bounds, nothing to fill
|
||||||
|
if poly_min_x == i32::MAX || poly_max_x == i32::MIN {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp to world bounds just in case
|
||||||
|
let (world_min_x, world_min_z) = editor.get_min_coords();
|
||||||
|
let (world_max_x, world_max_z) = editor.get_max_coords();
|
||||||
|
let min_x = poly_min_x.max(world_min_x);
|
||||||
|
let min_z = poly_min_z.max(world_min_z);
|
||||||
|
let max_x = poly_max_x.min(world_max_x);
|
||||||
|
let max_z = poly_max_z.min(world_max_z);
|
||||||
|
|
||||||
|
let outers_xz: Vec<Vec<XZPoint>> = outers
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||||
|
.collect();
|
||||||
|
let inners_xz: Vec<Vec<XZPoint>> = inners
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges way segments that share endpoints into closed rings.
|
||||||
|
fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
||||||
let mut removed: Vec<usize> = vec![];
|
let mut removed: Vec<usize> = vec![];
|
||||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||||
|
|
||||||
for i in 0..loops.len() {
|
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
||||||
for j in 0..loops.len() {
|
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||||
|
if a.id == b.id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let dx = (a.x - b.x).abs();
|
||||||
|
let dz = (a.z - b.z).abs();
|
||||||
|
dx <= 1 && dz <= 1
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..rings.len() {
|
||||||
|
for j in 0..rings.len() {
|
||||||
if i == j {
|
if i == j {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -117,20 +191,29 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let x: &Vec<ProcessedNode> = &loops[i];
|
let x: &Vec<ProcessedNode> = &rings[i];
|
||||||
let y: &Vec<ProcessedNode> = &loops[j];
|
let y: &Vec<ProcessedNode> = &rings[j];
|
||||||
|
|
||||||
// it's looped already
|
// Skip empty rings (can happen after clipping)
|
||||||
if x[0].id == x.last().unwrap().id {
|
if x.is_empty() || y.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// it's looped already
|
let x_first = &x[0];
|
||||||
if y[0].id == y.last().unwrap().id {
|
let x_last = x.last().unwrap();
|
||||||
|
let y_first = &y[0];
|
||||||
|
let y_last = y.last().unwrap();
|
||||||
|
|
||||||
|
// Skip already-closed rings
|
||||||
|
if nodes_match(x_first, x_last) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if x[0].id == y[0].id {
|
if nodes_match(y_first, y_last) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes_match(x_first, y_first) {
|
||||||
removed.push(i);
|
removed.push(i);
|
||||||
removed.push(j);
|
removed.push(j);
|
||||||
|
|
||||||
@@ -138,7 +221,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
|||||||
x.reverse();
|
x.reverse();
|
||||||
x.extend(y.iter().skip(1).cloned());
|
x.extend(y.iter().skip(1).cloned());
|
||||||
merged.push(x);
|
merged.push(x);
|
||||||
} else if x.last().unwrap().id == y.last().unwrap().id {
|
} else if nodes_match(x_last, y_last) {
|
||||||
removed.push(i);
|
removed.push(i);
|
||||||
removed.push(j);
|
removed.push(j);
|
||||||
|
|
||||||
@@ -146,7 +229,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
|||||||
x.extend(y.iter().rev().skip(1).cloned());
|
x.extend(y.iter().rev().skip(1).cloned());
|
||||||
|
|
||||||
merged.push(x);
|
merged.push(x);
|
||||||
} else if x[0].id == y.last().unwrap().id {
|
} else if nodes_match(x_first, y_last) {
|
||||||
removed.push(i);
|
removed.push(i);
|
||||||
removed.push(j);
|
removed.push(j);
|
||||||
|
|
||||||
@@ -154,7 +237,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
|||||||
y.extend(x.iter().skip(1).cloned());
|
y.extend(x.iter().skip(1).cloned());
|
||||||
|
|
||||||
merged.push(y);
|
merged.push(y);
|
||||||
} else if x.last().unwrap().id == y[0].id {
|
} else if nodes_match(x_last, y_first) {
|
||||||
removed.push(i);
|
removed.push(i);
|
||||||
removed.push(j);
|
removed.push(j);
|
||||||
|
|
||||||
@@ -169,24 +252,35 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
|||||||
removed.sort();
|
removed.sort();
|
||||||
|
|
||||||
for r in removed.iter().rev() {
|
for r in removed.iter().rev() {
|
||||||
loops.remove(*r);
|
rings.remove(*r);
|
||||||
}
|
}
|
||||||
|
|
||||||
let merged_len: usize = merged.len();
|
let merged_len: usize = merged.len();
|
||||||
for m in merged {
|
for m in merged {
|
||||||
loops.push(m);
|
rings.push(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
if merged_len > 0 {
|
if merged_len > 0 {
|
||||||
merge_loopy_loops(loops);
|
merge_way_segments(rings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_loopy_loops(loops: &[Vec<ProcessedNode>]) -> bool {
|
/// Verifies all rings are properly closed (first node matches last).
|
||||||
let mut valid: bool = true;
|
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
||||||
for l in loops {
|
let mut valid = true;
|
||||||
if l[0].id != l.last().unwrap().id {
|
for ring in rings {
|
||||||
eprintln!("WARN: Disconnected loop");
|
let first = &ring[0];
|
||||||
|
let last = ring.last().unwrap();
|
||||||
|
|
||||||
|
// Check if ring is closed (by ID or proximity)
|
||||||
|
let is_closed = first.id == last.id || {
|
||||||
|
let dx = (first.x - last.x).abs();
|
||||||
|
let dz = (first.z - last.z).abs();
|
||||||
|
dx <= 1 && dz <= 1
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_closed {
|
||||||
|
eprintln!("WARN: Disconnected ring");
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +300,8 @@ 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
|
||||||
let inners: Vec<_> = inners
|
let inners: Vec<_> = inners
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
@@ -219,6 +313,7 @@ fn inverse_floodfill(
|
|||||||
),
|
),
|
||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
|
.orient(Direction::Default)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -233,17 +328,11 @@ fn inverse_floodfill(
|
|||||||
),
|
),
|
||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
|
.orient(Direction::Default)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
inverse_floodfill_recursive(
|
inverse_floodfill_recursive((min_x, min_z), (max_x, max_z), &outers, &inners, editor);
|
||||||
(min_x, min_z),
|
|
||||||
(max_x, max_z),
|
|
||||||
&outers,
|
|
||||||
&inners,
|
|
||||||
editor,
|
|
||||||
start_time,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse_floodfill_recursive(
|
fn inverse_floodfill_recursive(
|
||||||
@@ -252,12 +341,11 @@ 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 25 seconds
|
// Check if we've exceeded 40 seconds
|
||||||
if start_time.elapsed().as_secs() > 25 {
|
// if start_time.elapsed().as_secs() > 40 {
|
||||||
println!("Water area generation exceeded 25 seconds, continuing anyway");
|
// println!("Water area generation exceeded 40 seconds, continuing anyway");
|
||||||
}
|
// }
|
||||||
|
|
||||||
const ITERATIVE_THRES: i64 = 10_000;
|
const ITERATIVE_THRES: i64 = 10_000;
|
||||||
|
|
||||||
@@ -312,7 +400,6 @@ fn inverse_floodfill_recursive(
|
|||||||
&outers_intersects,
|
&outers_intersects,
|
||||||
&inners_intersects,
|
&inners_intersects,
|
||||||
editor,
|
editor,
|
||||||
start_time,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance};
|
#[cfg(feature = "gui")]
|
||||||
|
use crate::telemetry::{send_log, LogLevel};
|
||||||
|
use crate::{
|
||||||
|
coordinate_system::{geographic::LLBBox, transformation::geo_distance},
|
||||||
|
progress::emit_gui_progress_update,
|
||||||
|
};
|
||||||
use image::Rgb;
|
use image::Rgb;
|
||||||
use std::path::Path;
|
use rayon::prelude::*;
|
||||||
|
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";
|
||||||
@@ -15,6 +19,10 @@ 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)]
|
||||||
@@ -27,6 +35,93 @@ 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();
|
||||||
@@ -44,6 +139,131 @@ 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
|
||||||
|
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(
|
||||||
|
client: &reqwest::blocking::Client,
|
||||||
|
tile_x: u32,
|
||||||
|
tile_y: u32,
|
||||||
|
zoom: u8,
|
||||||
|
tile_path: &Path,
|
||||||
|
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||||
|
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||||
|
let url: String = AWS_TERRARIUM_URL
|
||||||
|
.replace("{z}", &zoom.to_string())
|
||||||
|
.replace("{x}", &tile_x.to_string())
|
||||||
|
.replace("{y}", &tile_y.to_string());
|
||||||
|
|
||||||
|
let mut last_error: String = String::new();
|
||||||
|
|
||||||
|
for attempt in 0..TILE_DOWNLOAD_MAX_RETRIES {
|
||||||
|
if attempt > 0 {
|
||||||
|
// Exponential backoff: 500ms, 1000ms, 2000ms...
|
||||||
|
let delay_ms = TILE_DOWNLOAD_RETRY_BASE_DELAY_MS * (1 << (attempt - 1));
|
||||||
|
eprintln!(
|
||||||
|
"Retry attempt {}/{} for tile x={},y={},z={} after {}ms delay",
|
||||||
|
attempt,
|
||||||
|
TILE_DOWNLOAD_MAX_RETRIES - 1,
|
||||||
|
tile_x,
|
||||||
|
tile_y,
|
||||||
|
zoom,
|
||||||
|
delay_ms
|
||||||
|
);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
match download_tile_once(client, &url, tile_path) {
|
||||||
|
Ok(img) => return Ok(img),
|
||||||
|
Err(e) => {
|
||||||
|
last_error = e;
|
||||||
|
if attempt < TILE_DOWNLOAD_MAX_RETRIES - 1 {
|
||||||
|
eprintln!(
|
||||||
|
"Tile download failed for x={},y={},z={}: {}",
|
||||||
|
tile_x, tile_y, zoom, last_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"Failed to download tile x={},y={},z={} after {} attempts: {}",
|
||||||
|
tile_x, tile_y, zoom, TILE_DOWNLOAD_MAX_RETRIES, last_error
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single download attempt for a tile (no retries)
|
||||||
|
fn download_tile_once(
|
||||||
|
client: &reqwest::blocking::Client,
|
||||||
|
url: &str,
|
||||||
|
tile_path: &Path,
|
||||||
|
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||||
|
let response = client.get(url).send().map_err(|e| e.to_string())?;
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
@@ -67,51 +287,69 @@ 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 client: reqwest::blocking::Client = reqwest::blocking::Client::new();
|
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
|
||||||
|
|
||||||
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)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch and process each tile
|
// Create a shared HTTP client for connection pooling
|
||||||
for (tile_x, tile_y) in &tiles {
|
let client = reqwest::blocking::Client::new();
|
||||||
// Check if tile is already cached
|
|
||||||
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
|
|
||||||
|
|
||||||
let rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
|
// Download tiles in parallel with limited concurrency to be respectful to AWS
|
||||||
println!(
|
let num_tiles = tiles.len();
|
||||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
println!(
|
||||||
tile_path.display()
|
"Downloading {num_tiles} elevation tiles (up to {MAX_CONCURRENT_DOWNLOADS} concurrent)..."
|
||||||
);
|
);
|
||||||
let img: image::DynamicImage = image::open(&tile_path)?;
|
|
||||||
img.to_rgb8()
|
|
||||||
} else {
|
|
||||||
// AWS Terrain Tiles don't require an API key
|
|
||||||
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
|
||||||
let url: String = AWS_TERRARIUM_URL
|
|
||||||
.replace("{z}", &zoom.to_string())
|
|
||||||
.replace("{x}", &tile_x.to_string())
|
|
||||||
.replace("{y}", &tile_y.to_string());
|
|
||||||
|
|
||||||
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
// Use a custom thread pool to limit concurrent downloads
|
||||||
response.error_for_status_ref()?;
|
let thread_pool = rayon::ThreadPoolBuilder::new()
|
||||||
let bytes = response.bytes()?;
|
.num_threads(MAX_CONCURRENT_DOWNLOADS)
|
||||||
std::fs::write(&tile_path, &bytes)?;
|
.build()
|
||||||
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
.map_err(|e| format!("Failed to create thread pool: {e}"))?;
|
||||||
img.to_rgb8()
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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")]
|
||||||
|
send_log(
|
||||||
|
LogLevel::Warning,
|
||||||
|
&format!("Failed to download elevation tile: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -172,58 +410,61 @@ 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
|
||||||
// Reference points for tuning:
|
// Use sqrt scaling to maintain consistent relative smoothing across different area sizes.
|
||||||
const SMALL_GRID_REF: f64 = 100.0; // Reference grid size
|
// This prevents larger generation areas from appearing noisier than smaller ones.
|
||||||
const SMALL_SIGMA_REF: f64 = 15.0; // Sigma for 100x100 grid
|
// Reference: 100x100 grid uses sigma=5 (5% relative blur)
|
||||||
const LARGE_GRID_REF: f64 = 1000.0; // Reference grid size
|
const BASE_GRID_REF: f64 = 100.0;
|
||||||
const LARGE_SIGMA_REF: f64 = 7.0; // Sigma for 1000x1000 grid
|
const BASE_SIGMA_REF: f64 = 5.0;
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
let sigma: f64 = if grid_size <= SMALL_GRID_REF {
|
// Sqrt scaling provides a good balance:
|
||||||
// Linear scaling for small grids
|
// - 100x100: sigma = 5 (5% relative)
|
||||||
SMALL_SIGMA_REF * (grid_size / SMALL_GRID_REF)
|
// - 500x500: sigma ≈ 11.2 (2.2% relative)
|
||||||
} else {
|
// - 1000x1000: sigma ≈ 15.8 (1.6% relative)
|
||||||
// Logarithmic scaling for larger grids
|
// This smooths terrain proportionally while preserving more detail.
|
||||||
let ln_small: f64 = SMALL_GRID_REF.ln();
|
let sigma: f64 = BASE_SIGMA_REF * (grid_size / BASE_GRID_REF).sqrt();
|
||||||
let ln_large: f64 = LARGE_GRID_REF.ln();
|
|
||||||
let log_grid_size: f64 = grid_size.ln();
|
|
||||||
let t: f64 = (log_grid_size - ln_small) / (ln_large - ln_small);
|
|
||||||
SMALL_SIGMA_REF + t * (LARGE_SIGMA_REF - SMALL_SIGMA_REF)
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eprintln!(
|
//let blur_percentage: f64 = (sigma / grid_size) * 100.0;
|
||||||
"Grid: {}x{}, Blur sigma: {:.2}",
|
/*eprintln!(
|
||||||
grid_width, grid_height, sigma
|
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
|
||||||
); */
|
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);
|
||||||
|
|
||||||
let mut mc_heights: Vec<Vec<i32>> = Vec::with_capacity(blurred_heights.len());
|
// Release raw height grid
|
||||||
|
drop(height_grid);
|
||||||
|
|
||||||
// Find min/max in raw data
|
// Find min/max in raw data using parallel reduction
|
||||||
let mut min_height: f64 = f64::MAX;
|
let (min_height, max_height, extreme_low_count, extreme_high_count) = blurred_heights
|
||||||
let mut max_height: f64 = f64::MIN;
|
.par_iter()
|
||||||
let mut extreme_low_count = 0;
|
.map(|row| {
|
||||||
let mut extreme_high_count = 0;
|
let mut local_min = f64::MAX;
|
||||||
|
let mut local_max = f64::MIN;
|
||||||
for row in &blurred_heights {
|
let mut local_low = 0usize;
|
||||||
for &height in row {
|
let mut local_high = 0usize;
|
||||||
min_height = min_height.min(height);
|
for &height in row {
|
||||||
max_height = max_height.max(height);
|
local_min = local_min.min(height);
|
||||||
|
local_max = local_max.max(height);
|
||||||
// Count extreme values that might indicate data issues
|
if height < -1000.0 {
|
||||||
if height < -1000.0 {
|
local_low += 1;
|
||||||
extreme_low_count += 1;
|
}
|
||||||
|
if height > 10000.0 {
|
||||||
|
local_high += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if height > 10000.0 {
|
(local_min, local_max, local_low, local_high)
|
||||||
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)"
|
||||||
@@ -236,39 +477,63 @@ 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;
|
|
||||||
|
|
||||||
// Adaptive scaling: ensure we don't exceed reasonable Y range
|
// Realistic height scaling: 1 meter of real elevation = scale blocks in Minecraft
|
||||||
let available_y_range = (MAX_Y - ground_level) as f64;
|
// At scale=1.0, 1 meter = 1 block (realistic 1:1 mapping)
|
||||||
let safety_margin = 0.9; // Use 90% of available range
|
// At scale=2.0, 1 meter = 2 blocks (exaggerated for larger worlds)
|
||||||
let max_allowed_range = available_y_range * safety_margin;
|
let ideal_scaled_range: f64 = height_range * scale;
|
||||||
|
|
||||||
if scaled_range > max_allowed_range {
|
// Calculate available Y range in Minecraft (from ground_level to MAX_Y)
|
||||||
let adjustment_factor = max_allowed_range / scaled_range;
|
// Leave a buffer at the top for buildings, trees, and other structures
|
||||||
height_scale *= adjustment_factor;
|
const TERRAIN_HEIGHT_BUFFER: i32 = 15;
|
||||||
scaled_range = height_range * height_scale;
|
let available_y_range: f64 = (MAX_Y - TERRAIN_HEIGHT_BUFFER - ground_level) as f64;
|
||||||
|
|
||||||
|
// Determine final height scale:
|
||||||
|
// - Use realistic 1:1 (times scale) if terrain fits within Minecraft limits
|
||||||
|
// - Only compress if the terrain would exceed the build height
|
||||||
|
let scaled_range: f64 = if ideal_scaled_range <= available_y_range {
|
||||||
|
// Terrain fits! Use realistic scaling
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Height range too large, applying scaling adjustment factor: {adjustment_factor:.3}"
|
"Realistic elevation: {:.1}m range fits in {} available blocks",
|
||||||
|
height_range, available_y_range as i32
|
||||||
);
|
);
|
||||||
eprintln!("Adjusted scaled range: {scaled_range:.1} blocks");
|
ideal_scaled_range
|
||||||
}
|
} else {
|
||||||
|
// Terrain too tall, compress to fit within Minecraft limits
|
||||||
|
let compression_factor: f64 = available_y_range / height_range;
|
||||||
|
let compressed_range: f64 = height_range * compression_factor;
|
||||||
|
eprintln!(
|
||||||
|
"Elevation compressed: {:.1}m range -> {:.0} blocks ({:.2}:1 ratio, 1 block = {:.2}m)",
|
||||||
|
height_range,
|
||||||
|
compressed_range,
|
||||||
|
height_range / compressed_range,
|
||||||
|
compressed_range / height_range
|
||||||
|
);
|
||||||
|
compressed_range
|
||||||
|
};
|
||||||
|
|
||||||
// Convert to scaled Minecraft Y coordinates
|
// Convert to scaled Minecraft Y coordinates (parallelized across rows)
|
||||||
for row in blurred_heights {
|
// Lowest real elevation maps to ground_level, highest maps to ground_level + scaled_range
|
||||||
let mc_row: Vec<i32> = row
|
let mc_heights: Vec<Vec<i32>> = blurred_heights
|
||||||
.iter()
|
.par_iter()
|
||||||
.map(|&h| {
|
.map(|row| {
|
||||||
// Scale the height differences
|
row.iter()
|
||||||
let relative_height: f64 = (h - min_height) / height_range;
|
.map(|&h| {
|
||||||
let scaled_height: f64 = relative_height * scaled_range;
|
// Calculate relative position within the elevation range (0.0 to 1.0)
|
||||||
// With terrain enabled, ground_level is used as the MIN_Y for terrain
|
let relative_height: f64 = if height_range > 0.0 {
|
||||||
((ground_level as f64 + scaled_height).round() as i32).clamp(ground_level, MAX_Y)
|
(h - min_height) / height_range
|
||||||
})
|
} else {
|
||||||
.collect();
|
0.0
|
||||||
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;
|
||||||
@@ -278,7 +543,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,
|
||||||
@@ -305,48 +570,61 @@ 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);
|
||||||
|
|
||||||
// Apply blur
|
let height_len = heights.len();
|
||||||
let mut blurred: Vec<Vec<f64>> = heights.to_owned();
|
let width = heights[0].len();
|
||||||
|
|
||||||
// Horizontal pass
|
// Horizontal pass - parallelize across rows (each row is independent)
|
||||||
for row in blurred.iter_mut() {
|
let after_horizontal: Vec<Vec<f64>> = heights
|
||||||
let mut temp: Vec<f64> = row.clone();
|
.par_iter()
|
||||||
for (i, val) in temp.iter_mut().enumerate() {
|
.map(|row| {
|
||||||
let mut sum: f64 = 0.0;
|
let mut temp: Vec<f64> = vec![0.0; row.len()];
|
||||||
let mut weight_sum: f64 = 0.0;
|
for (i, val) in temp.iter_mut().enumerate() {
|
||||||
for (j, k) in kernel.iter().enumerate() {
|
let mut sum: f64 = 0.0;
|
||||||
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
|
let mut weight_sum: f64 = 0.0;
|
||||||
if idx >= 0 && idx < row.len() as i32 {
|
for (j, k) in kernel.iter().enumerate() {
|
||||||
sum += row[idx as usize] * k;
|
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
|
||||||
weight_sum += k;
|
if idx >= 0 && idx < row.len() as i32 {
|
||||||
|
sum += row[idx as usize] * k;
|
||||||
|
weight_sum += k;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
*val = sum / weight_sum;
|
||||||
}
|
}
|
||||||
*val = sum / weight_sum;
|
temp
|
||||||
}
|
})
|
||||||
*row = temp;
|
.collect();
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical pass
|
// Vertical pass - parallelize across columns (each column is independent)
|
||||||
let height: usize = blurred.len();
|
// Process each column in parallel and collect results as column vectors
|
||||||
let width: usize = blurred[0].len();
|
let blurred_columns: Vec<Vec<f64>> = (0..width)
|
||||||
for x in 0..width {
|
.into_par_iter()
|
||||||
let temp: Vec<_> = blurred
|
.map(|x| {
|
||||||
.iter()
|
// Extract column from after_horizontal
|
||||||
.take(height)
|
let column: Vec<f64> = after_horizontal.iter().map(|row| row[x]).collect();
|
||||||
.map(|row: &Vec<f64>| row[x])
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for (y, row) in blurred.iter_mut().enumerate().take(height) {
|
// Apply vertical blur to this column
|
||||||
let mut sum: f64 = 0.0;
|
let mut blurred_column: Vec<f64> = vec![0.0; height_len];
|
||||||
let mut weight_sum: f64 = 0.0;
|
for (y, val) in blurred_column.iter_mut().enumerate() {
|
||||||
for (j, k) in kernel.iter().enumerate() {
|
let mut sum: f64 = 0.0;
|
||||||
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
|
let mut weight_sum: f64 = 0.0;
|
||||||
if idx >= 0 && idx < height as i32 {
|
for (j, k) in kernel.iter().enumerate() {
|
||||||
sum += temp[idx as usize] * k;
|
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
|
||||||
weight_sum += k;
|
if idx >= 0 && idx < height_len as i32 {
|
||||||
|
sum += column[idx as usize] * k;
|
||||||
|
weight_sum += k;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
*val = sum / weight_sum;
|
||||||
}
|
}
|
||||||
row[x] = sum / weight_sum;
|
blurred_column
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Transpose columns back to row-major format
|
||||||
|
let mut blurred: Vec<Vec<f64>> = vec![vec![0.0; width]; height_len];
|
||||||
|
for (x, column) in blurred_columns.into_iter().enumerate() {
|
||||||
|
for (y, val) in column.into_iter().enumerate() {
|
||||||
|
blurred[y][x] = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,17 +706,24 @@ 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;
|
let p99_idx = ((len as f64 * 0.99) as usize).min(len - 1);
|
||||||
let min_reasonable = all_heights[p1_idx];
|
|
||||||
let max_reasonable = all_heights[p99_idx];
|
|
||||||
|
|
||||||
eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
// Find p1 (1st percentile) - all elements before p1_idx will be <= p1
|
||||||
|
let (_, p1_val, _) =
|
||||||
|
all_heights.select_nth_unstable_by(p1_idx, |a, b| a.partial_cmp(b).unwrap());
|
||||||
|
let min_reasonable = *p1_val;
|
||||||
|
|
||||||
|
// Find p99 (99th percentile) - need to search in remaining slice or use separate call
|
||||||
|
let (_, p99_val, _) =
|
||||||
|
all_heights.select_nth_unstable_by(p99_idx, |a, b| a.partial_cmp(b).unwrap());
|
||||||
|
let max_reasonable = *p99_val;
|
||||||
|
|
||||||
|
//eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
||||||
|
|
||||||
let mut outliers_filtered = 0;
|
let mut outliers_filtered = 0;
|
||||||
|
|
||||||
@@ -453,7 +738,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fn optimized_flood_fill_area(
|
|||||||
|
|
||||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||||
// Fast timeout check - only every few iterations
|
// Fast timeout check, only every few iterations
|
||||||
if filled_area.len() % 100 == 0 {
|
if filled_area.len() % 100 == 0 {
|
||||||
if let Some(timeout) = timeout {
|
if let Some(timeout) = timeout {
|
||||||
if start_time.elapsed() > *timeout {
|
if start_time.elapsed() > *timeout {
|
||||||
@@ -160,11 +160,10 @@ fn original_flood_fill_area(
|
|||||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||||
// Reduced timeout checking frequency for better performance
|
// Reduced timeout checking frequency for better performance
|
||||||
if global_visited.len() % 200 == 0 {
|
// Use manual % check since is_multiple_of() is unstable on stable Rust
|
||||||
if let Some(timeout) = timeout {
|
if let Some(timeout) = timeout {
|
||||||
if &start_time.elapsed() > timeout {
|
if &start_time.elapsed() > timeout {
|
||||||
return filled_area;
|
return filled_area;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
340
src/floodfill_cache.rs
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
//! 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,6 +2,8 @@ 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};
|
||||||
|
|
||||||
@@ -23,12 +25,26 @@ impl Ground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_enabled(bbox: &LLBBox, scale: f64, ground_level: i32) -> Self {
|
pub fn new_enabled(bbox: &LLBBox, scale: f64, ground_level: i32) -> Self {
|
||||||
let elevation_data = fetch_elevation_data(bbox, scale, ground_level)
|
match fetch_elevation_data(bbox, scale, ground_level) {
|
||||||
.expect("Failed to fetch elevation data");
|
Ok(elevation_data) => Self {
|
||||||
Self {
|
elevation_enabled: true,
|
||||||
elevation_enabled: true,
|
ground_level,
|
||||||
ground_level,
|
elevation_data: Some(elevation_data),
|
||||||
elevation_data: Some(elevation_data),
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to fetch elevation data: {}", e);
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
send_log(
|
||||||
|
LogLevel::Warning,
|
||||||
|
"Elevation unavailable, using flat ground",
|
||||||
|
);
|
||||||
|
// Graceful fallback: disable elevation and keep provided ground_level
|
||||||
|
Self {
|
||||||
|
elevation_enabled: false,
|
||||||
|
ground_level,
|
||||||
|
elevation_data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +147,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(15.0, "Fetching elevation...");
|
emit_gui_progress_update(14.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");
|
||||||
|
|||||||
587
src/gui.rs
@@ -1,21 +1,26 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::coordinate_system::cartesian::XZPoint;
|
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||||
use crate::data_processing;
|
use crate::coordinate_system::transformation::CoordTransformer;
|
||||||
|
use crate::data_processing::{self, GenerationOptions};
|
||||||
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::progress;
|
use crate::parallel_processing::ParallelConfig;
|
||||||
|
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::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;
|
||||||
use log::{error, LevelFilter};
|
use log::LevelFilter;
|
||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{env, fs, io::Write, panic};
|
use std::{env, fs, io::Write};
|
||||||
use tauri_plugin_log::{Builder as LogBuilder, Target, TargetKind};
|
use tauri_plugin_log::{Builder as LogBuilder, Target, TargetKind};
|
||||||
|
|
||||||
/// Manages the session.lock file for a Minecraft world directory
|
/// Manages the session.lock file for a Minecraft world directory
|
||||||
@@ -58,16 +63,36 @@ 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...");
|
||||||
|
|
||||||
// Set a custom panic hook to log panic information
|
// Install panic hook for crash reporting
|
||||||
panic::set_hook(Box::new(|panic_info| {
|
telemetry::install_panic_hook();
|
||||||
let message = format!("Application panicked: {panic_info:?}");
|
|
||||||
error!("{message}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Workaround WebKit2GTK issue with NVIDIA drivers and graphics issues
|
// Workaround WebKit2GTK issue with NVIDIA drivers and graphics issues
|
||||||
// Source: https://github.com/tauri-apps/tauri/issues/10702
|
// Source: https://github.com/tauri-apps/tauri/issues/10702
|
||||||
@@ -88,7 +113,7 @@ pub fn run_gui() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(
|
.plugin(
|
||||||
LogBuilder::default()
|
LogBuilder::default()
|
||||||
.level(LevelFilter::Warn)
|
.level(LevelFilter::Info)
|
||||||
.targets([
|
.targets([
|
||||||
Target::new(TargetKind::LogDir {
|
Target::new(TargetKind::LogDir {
|
||||||
file_name: Some("arnis".into()),
|
file_name: Some("arnis".into()),
|
||||||
@@ -102,7 +127,9 @@ pub fn run_gui() {
|
|||||||
gui_select_world,
|
gui_select_world,
|
||||||
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_show_in_folder
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_handle = app.handle();
|
let app_handle = app.handle();
|
||||||
@@ -142,16 +169,20 @@ fn gui_select_world(generate_new: bool) -> Result<String, i32> {
|
|||||||
|
|
||||||
if generate_new {
|
if generate_new {
|
||||||
// Handle new world generation
|
// Handle new world generation
|
||||||
if let Some(default_path) = &default_dir {
|
// Try Minecraft saves directory first, fall back to current directory
|
||||||
|
let target_path = if let Some(default_path) = &default_dir {
|
||||||
if default_path.exists() {
|
if default_path.exists() {
|
||||||
// Call create_new_world and return the result
|
default_path.clone()
|
||||||
create_new_world(default_path).map_err(|_| 1) // Error code 1: Minecraft directory not found
|
|
||||||
} else {
|
} else {
|
||||||
Err(1) // Error code 1: Minecraft directory not found
|
// Minecraft directory doesn't exist, use current directory
|
||||||
|
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(1) // Error code 1: Minecraft directory not found
|
// No default directory configured, use current directory
|
||||||
}
|
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||||
|
};
|
||||||
|
|
||||||
|
create_new_world(&target_path).map_err(|_| 3) // Error code 3: Failed to create new world
|
||||||
} else {
|
} else {
|
||||||
// Handle existing world selection
|
// Handle existing world selection
|
||||||
// Open the directory picker dialog
|
// Open the directory picker dialog
|
||||||
@@ -226,13 +257,13 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
|||||||
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
||||||
|
|
||||||
// Copy the region template file
|
// Copy the region template file
|
||||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../mcassets/region.template");
|
const REGION_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/region.template");
|
||||||
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
||||||
fs::write(®ion_path, REGION_TEMPLATE)
|
fs::write(®ion_path, REGION_TEMPLATE)
|
||||||
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
||||||
|
|
||||||
// Add the level.dat file
|
// Add the level.dat file
|
||||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../mcassets/level.dat");
|
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/level.dat");
|
||||||
|
|
||||||
// Decompress the gzipped level.template
|
// Decompress the gzipped level.template
|
||||||
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
||||||
@@ -299,7 +330,7 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
|||||||
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
||||||
|
|
||||||
// Add the icon.png file
|
// Add the icon.png file
|
||||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../mcassets/icon.png");
|
const ICON_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/icon.png");
|
||||||
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
||||||
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
||||||
|
|
||||||
@@ -307,52 +338,45 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds localized area name to the world name in level.dat
|
/// Adds localized area name to the world name in level.dat
|
||||||
fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||||
let world_path = PathBuf::from(world_path_str);
|
|
||||||
|
|
||||||
// Only proceed if the path exists
|
// Only proceed if the path exists
|
||||||
if !world_path.exists() {
|
if !world_path.exists() {
|
||||||
return world_path_str.to_string();
|
return world_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the level.dat file first to get the current name
|
// Check the level.dat file first to get the current name
|
||||||
let level_path = world_path.join("level.dat");
|
let level_path = world_path.join("level.dat");
|
||||||
|
|
||||||
if !level_path.exists() {
|
if !level_path.exists() {
|
||||||
return world_path_str.to_string();
|
return world_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read the current world name from level.dat
|
// Try to read the current world name from level.dat
|
||||||
let current_name = match std::fs::read(&level_path) {
|
let Ok(level_data) = std::fs::read(&level_path) else {
|
||||||
Ok(level_data) => {
|
return world_path;
|
||||||
let mut decoder = GzDecoder::new(level_data.as_slice());
|
};
|
||||||
let mut decompressed_data = Vec::new();
|
|
||||||
if decoder.read_to_end(&mut decompressed_data).is_ok() {
|
let mut decoder = GzDecoder::new(level_data.as_slice());
|
||||||
if let Ok(Value::Compound(ref root)) =
|
let mut decompressed_data = Vec::new();
|
||||||
fastnbt::from_bytes::<Value>(&decompressed_data)
|
if decoder.read_to_end(&mut decompressed_data).is_err() {
|
||||||
{
|
return world_path;
|
||||||
if let Some(Value::Compound(ref data)) = root.get("Data") {
|
}
|
||||||
if let Some(Value::String(name)) = data.get("LevelName") {
|
|
||||||
name.clone()
|
let Ok(Value::Compound(ref root)) = fastnbt::from_bytes::<Value>(&decompressed_data) else {
|
||||||
} else {
|
return world_path;
|
||||||
return world_path_str.to_string();
|
};
|
||||||
}
|
|
||||||
} else {
|
let Some(Value::Compound(ref data)) = root.get("Data") else {
|
||||||
return world_path_str.to_string();
|
return world_path;
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
return world_path_str.to_string();
|
let Some(Value::String(current_name)) = data.get("LevelName") else {
|
||||||
}
|
return world_path;
|
||||||
} else {
|
|
||||||
return world_path_str.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => return world_path_str.to_string(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only modify if it's an Arnis world and doesn't already have an area name
|
// Only modify if it's an Arnis world and doesn't already have an area name
|
||||||
if !current_name.starts_with("Arnis World ") || current_name.contains(": ") {
|
if !current_name.starts_with("Arnis World ") || current_name.contains(": ") {
|
||||||
return world_path_str.to_string();
|
return world_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate center coordinates of bbox
|
// Calculate center coordinates of bbox
|
||||||
@@ -362,7 +386,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
|||||||
// Try to fetch the area name
|
// Try to fetch the area name
|
||||||
let area_name = match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
let area_name = match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||||
Ok(Some(name)) => name,
|
Ok(Some(name)) => name,
|
||||||
_ => return world_path_str.to_string(), // Keep original name if no area name found
|
_ => return world_path, // Keep original name if no area name found
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create new name with localized area name, ensuring total length doesn't exceed 30 characters
|
// Create new name with localized area name, ensuring total length doesn't exceed 30 characters
|
||||||
@@ -378,7 +402,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
|||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
} else if max_area_name_len == 0 {
|
} else if max_area_name_len == 0 {
|
||||||
// If base name is already too long, don't add area name
|
// If base name is already too long, don't add area name
|
||||||
return world_path_str.to_string();
|
return world_path;
|
||||||
} else {
|
} else {
|
||||||
area_name
|
area_name
|
||||||
};
|
};
|
||||||
@@ -406,6 +430,11 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
|||||||
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(
|
||||||
|
LogLevel::Warning,
|
||||||
|
"Failed to update level.dat with area name",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,39 +446,23 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the original path since we didn't change the directory name
|
// Return the original path since we didn't change the directory name
|
||||||
world_path_str.to_string()
|
world_path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update player position in level.dat based on spawn point coordinates
|
/// Calculates the default spawn point at X=1, Z=1 relative to the world origin.
|
||||||
fn update_player_position(
|
/// This is used when no spawn point is explicitly selected by the user.
|
||||||
|
fn calculate_default_spawn(xzbbox: &XZBBox) -> (i32, i32) {
|
||||||
|
(xzbbox.min_x() + 1, xzbbox.min_z() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the player spawn point in level.dat using Minecraft XZ coordinates.
|
||||||
|
/// The Y coordinate is set to a temporary value (150) and will be updated
|
||||||
|
/// after terrain generation by `update_player_spawn_y_after_generation`.
|
||||||
|
fn set_player_spawn_in_level_dat(
|
||||||
world_path: &str,
|
world_path: &str,
|
||||||
spawn_point: Option<(f64, f64)>,
|
spawn_x: i32,
|
||||||
bbox_text: String,
|
spawn_z: i32,
|
||||||
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;
|
||||||
|
|
||||||
@@ -481,21 +494,24 @@ fn update_player_position(
|
|||||||
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(xzpoint.x));
|
data.insert("SpawnX".to_string(), Value::Int(spawn_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(xzpoint.z));
|
data.insert("SpawnZ".to_string(), Value::Int(spawn_z));
|
||||||
|
|
||||||
// Update player position
|
// Update player position if Player compound exists
|
||||||
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") {
|
||||||
if let Value::Double(ref mut pos_x) = pos.get_mut(0).unwrap() {
|
// Safely update position values with bounds checking
|
||||||
*pos_x = xzpoint.x as f64;
|
if pos.len() >= 3 {
|
||||||
}
|
if let Some(Value::Double(ref mut pos_x)) = pos.get_mut(0) {
|
||||||
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
*pos_x = spawn_x as f64;
|
||||||
*pos_y = y;
|
}
|
||||||
}
|
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
||||||
if let Value::Double(ref mut pos_z) = pos.get_mut(2).unwrap() {
|
*pos_y = y;
|
||||||
*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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -527,19 +543,15 @@ fn update_player_position(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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: &str,
|
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() {
|
||||||
@@ -608,7 +620,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) + 2
|
ground.level(terrain_point) + 3 // Add 3 blocks above terrain for safety
|
||||||
} else {
|
} else {
|
||||||
-61 // Default Y if no terrain
|
-61 // Default Y if no terrain
|
||||||
};
|
};
|
||||||
@@ -622,8 +634,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") {
|
||||||
// Keep existing X and Z, only update Y
|
// Safely update Y position with bounds checking
|
||||||
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
||||||
*pos_y = spawn_y as f64;
|
*pos_y = spawn_y as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -668,6 +680,114 @@ fn gui_check_for_updates() -> Result<bool, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the world map image data as base64 and geo bounds for overlay display.
|
||||||
|
/// Returns None if the map image or metadata doesn't exist.
|
||||||
|
#[tauri::command]
|
||||||
|
fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, String> {
|
||||||
|
let world_dir = PathBuf::from(&world_path);
|
||||||
|
let map_path = world_dir.join("arnis_world_map.png");
|
||||||
|
let metadata_path = world_dir.join("metadata.json");
|
||||||
|
|
||||||
|
// Check if both files exist
|
||||||
|
if !map_path.exists() || !metadata_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and encode the map image as base64
|
||||||
|
let image_data = fs::read(&map_path).map_err(|e| format!("Failed to read map image: {e}"))?;
|
||||||
|
let base64_image =
|
||||||
|
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &image_data);
|
||||||
|
|
||||||
|
// Read metadata
|
||||||
|
let metadata_content =
|
||||||
|
fs::read_to_string(&metadata_path).map_err(|e| format!("Failed to read metadata: {e}"))?;
|
||||||
|
let metadata: serde_json::Value = serde_json::from_str(&metadata_content)
|
||||||
|
.map_err(|e| format!("Failed to parse metadata: {e}"))?;
|
||||||
|
|
||||||
|
// Extract geo bounds (metadata uses camelCase from serde)
|
||||||
|
let min_lat = metadata["minGeoLat"]
|
||||||
|
.as_f64()
|
||||||
|
.ok_or("Missing minGeoLat in metadata")?;
|
||||||
|
let max_lat = metadata["maxGeoLat"]
|
||||||
|
.as_f64()
|
||||||
|
.ok_or("Missing maxGeoLat in metadata")?;
|
||||||
|
let min_lon = metadata["minGeoLon"]
|
||||||
|
.as_f64()
|
||||||
|
.ok_or("Missing minGeoLon in metadata")?;
|
||||||
|
let max_lon = metadata["maxGeoLon"]
|
||||||
|
.as_f64()
|
||||||
|
.ok_or("Missing maxGeoLon in metadata")?;
|
||||||
|
|
||||||
|
Ok(Some(WorldMapData {
|
||||||
|
image_base64: format!("data:image/png;base64,{}", base64_image),
|
||||||
|
min_lat,
|
||||||
|
max_lat,
|
||||||
|
min_lon,
|
||||||
|
max_lon,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data structure for world map overlay
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct WorldMapData {
|
||||||
|
image_base64: String,
|
||||||
|
min_lat: f64,
|
||||||
|
max_lat: f64,
|
||||||
|
min_lon: f64,
|
||||||
|
max_lon: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
||||||
|
#[tauri::command]
|
||||||
|
fn gui_show_in_folder(path: String) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// On Windows, try to open with default application (Minecraft Bedrock)
|
||||||
|
// If that fails, show in Explorer
|
||||||
|
if std::process::Command::new("cmd")
|
||||||
|
.args(["/C", "start", "", &path])
|
||||||
|
.spawn()
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
std::process::Command::new("explorer")
|
||||||
|
.args(["/select,", &path])
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to open explorer: {}", e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// On macOS, just reveal in Finder
|
||||||
|
std::process::Command::new("open")
|
||||||
|
.args(["-R", &path])
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to open Finder: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// On Linux, just show in file manager
|
||||||
|
let path_parent = std::path::Path::new(&path)
|
||||||
|
.parent()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| path.clone());
|
||||||
|
|
||||||
|
// Try nautilus with select first, then fall back to xdg-open on parent
|
||||||
|
if std::process::Command::new("nautilus")
|
||||||
|
.args(["--select", &path])
|
||||||
|
.spawn()
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
let _ = std::process::Command::new("xdg-open")
|
||||||
|
.arg(&path_parent)
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@@ -676,59 +796,118 @@ 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,
|
||||||
interior_enabled: bool,
|
interior_enabled: bool,
|
||||||
roof_enabled: bool,
|
roof_enabled: bool,
|
||||||
fillground_enabled: bool,
|
fillground_enabled: bool,
|
||||||
is_new_world: bool,
|
is_new_world: bool,
|
||||||
spawn_point: Option<(f64, f64)>,
|
spawn_point: Option<(f64, f64)>,
|
||||||
|
telemetry_consent: bool,
|
||||||
|
world_format: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
use progress::emit_gui_error;
|
use progress::emit_gui_error;
|
||||||
use LLBBox;
|
use LLBBox;
|
||||||
|
|
||||||
// If spawn point was chosen and the world is new, check and set the spawn point
|
// Store telemetry consent for crash reporting
|
||||||
if is_new_world && spawn_point.is_some() {
|
telemetry::set_telemetry_consent(telemetry_consent);
|
||||||
// Verify the spawn point is within bounds
|
|
||||||
if let Some(coords) = spawn_point {
|
|
||||||
let llbbox = match LLBBox::from_str(&bbox_text) {
|
|
||||||
Ok(bbox) => bbox,
|
|
||||||
Err(e) => {
|
|
||||||
let error_msg = format!("Failed to parse bounding box: {e}");
|
|
||||||
eprintln!("{error_msg}");
|
|
||||||
emit_gui_error(&error_msg);
|
|
||||||
return Err(error_msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Send generation click telemetry
|
||||||
|
telemetry::send_generation_click();
|
||||||
|
|
||||||
|
// For new Java worlds, set the spawn point in level.dat
|
||||||
|
// Only update player position for Java worlds - Bedrock worlds don't have a pre-existing
|
||||||
|
// level.dat to modify (the spawn point will be set when the .mcworld is created)
|
||||||
|
if is_new_world && world_format != "bedrock" {
|
||||||
|
let llbbox = match LLBBox::from_str(&bbox_text) {
|
||||||
|
Ok(bbox) => bbox,
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg = format!("Failed to parse bounding box: {e}");
|
||||||
|
eprintln!("{error_msg}");
|
||||||
|
emit_gui_error(&error_msg);
|
||||||
|
return Err(error_msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
let llpoint = LLPoint::new(coords.0, coords.1)
|
||||||
.map_err(|e| format!("Failed to parse spawn point: {e}"))?;
|
.map_err(|e| format!("Failed to parse spawn point: {e}"))?;
|
||||||
|
|
||||||
if llbbox.contains(&llpoint) {
|
if llbbox.contains(&llpoint) {
|
||||||
// Spawn point is valid, update the player position
|
let xzpoint = transformer.transform_point(llpoint);
|
||||||
update_player_position(
|
(xzpoint.x, xzpoint.z)
|
||||||
&selected_world,
|
} else {
|
||||||
spawn_point,
|
// Spawn point outside bounds, use default
|
||||||
bbox_text.clone(),
|
calculate_default_spawn(&xzbbox)
|
||||||
world_scale,
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// No user-selected spawn point - use default at X=1, Z=1 relative to world origin
|
||||||
|
calculate_default_spawn(&xzbbox)
|
||||||
|
};
|
||||||
|
|
||||||
|
set_player_spawn_in_level_dat(&selected_world, spawn_x, spawn_z)
|
||||||
|
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
if let Err(e) = tokio::task::spawn_blocking(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 world_path = PathBuf::from(&selected_world);
|
||||||
let _session_lock = match SessionLock::acquire(&world_path) {
|
|
||||||
Ok(lock) => lock,
|
// Determine world format from UI selection first (needed for session lock decision)
|
||||||
Err(e) => {
|
let world_format = if world_format == "bedrock" {
|
||||||
let error_msg = format!("Failed to acquire session lock: {e}");
|
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
|
||||||
|
// Session lock prevents Minecraft from having the world open during generation
|
||||||
|
// Bedrock worlds are generated as .mcworld files and don't need this lock
|
||||||
|
let _session_lock: Option<SessionLock> = if world_format == WorldFormat::JavaAnvil {
|
||||||
|
match SessionLock::acquire(&world_path) {
|
||||||
|
Ok(lock) => Some(lock),
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg = format!("Failed to acquire session lock: {e}");
|
||||||
|
eprintln!("{error_msg}");
|
||||||
|
emit_gui_error(&error_msg);
|
||||||
|
return Err(error_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the bounding box from the text with proper error handling
|
// Parse the bounding box from the text with proper error handling
|
||||||
@@ -742,19 +921,71 @@ fn gui_start_generation(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add localized name to the world if user generated a new world
|
// Determine output path and level name based on format
|
||||||
let updated_world_path = if is_new_world {
|
let (generation_path, level_name) = match world_format {
|
||||||
add_localized_world_name(&selected_world, &bbox)
|
WorldFormat::JavaAnvil => {
|
||||||
} else {
|
// Java: use the selected world path, add localized name if new
|
||||||
selected_world.clone()
|
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))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create an Args instance with the chosen bounding box and world directory path
|
// 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 {
|
||||||
|
// Default spawn point: X=1, Z=1 relative to world origin
|
||||||
|
if let Ok((_, xzbbox)) = CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale) {
|
||||||
|
Some(calculate_default_spawn(&xzbbox))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create generation options
|
||||||
|
let generation_options = GenerationOptions {
|
||||||
|
path: generation_path.clone(),
|
||||||
|
format: world_format,
|
||||||
|
level_name,
|
||||||
|
spawn_point: mc_spawn_point,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create an Args instance with the chosen bounding box
|
||||||
|
// Note: path is used for Java-specific features like spawn point update
|
||||||
let args: Args = Args {
|
let args: Args = Args {
|
||||||
bbox,
|
bbox,
|
||||||
file: None,
|
file: None,
|
||||||
save_json_file: None,
|
save_json_file: None,
|
||||||
path: updated_world_path,
|
path: if world_format == WorldFormat::JavaAnvil {
|
||||||
|
generation_path
|
||||||
|
} else {
|
||||||
|
world_path
|
||||||
|
},
|
||||||
downloader: "requests".to_string(),
|
downloader: "requests".to_string(),
|
||||||
scale: world_scale,
|
scale: world_scale,
|
||||||
ground_level,
|
ground_level,
|
||||||
@@ -763,11 +994,52 @@ 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(floodfill_timeout)),
|
timeout: Some(std::time::Duration::from_secs(40)),
|
||||||
spawn_point,
|
threads: 0, // Auto-detect thread count
|
||||||
|
region_batch_size: 2, // Four regions per unit (default)
|
||||||
|
no_parallel: true, // Use sequential processing (parallel has bugs)
|
||||||
|
force_parallel: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run data fetch and world generation
|
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
||||||
|
if skip_osm_objects {
|
||||||
|
// Generate ground data (terrain) for terrain-only mode
|
||||||
|
let ground = ground::generate_ground_data(&args);
|
||||||
|
|
||||||
|
// Create empty parsed_elements and xzbbox for terrain-only mode
|
||||||
|
let parsed_elements = Vec::new();
|
||||||
|
let (_coord_transformer, xzbbox) =
|
||||||
|
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
|
||||||
|
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
|
||||||
|
|
||||||
|
let _ = data_processing::generate_world_with_options(
|
||||||
|
parsed_elements,
|
||||||
|
xzbbox.clone(),
|
||||||
|
args.bbox,
|
||||||
|
ground,
|
||||||
|
&args,
|
||||||
|
generation_options.clone(),
|
||||||
|
ParallelConfig::default(), // Use parallel processing
|
||||||
|
);
|
||||||
|
// Explicitly release session lock before showing Done message
|
||||||
|
// so Minecraft can open the world immediately
|
||||||
|
drop(_session_lock);
|
||||||
|
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||||
|
println!("{}", "Done! World generation completed.".green().bold());
|
||||||
|
|
||||||
|
// Start map preview generation silently in background (Java only)
|
||||||
|
if world_format == WorldFormat::JavaAnvil {
|
||||||
|
let preview_info = data_processing::MapPreviewInfo::new(
|
||||||
|
generation_options.path.clone(),
|
||||||
|
&xzbbox,
|
||||||
|
);
|
||||||
|
data_processing::start_map_preview_generation(preview_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run data fetch and world generation (standard mode: objects + terrain, or objects only)
|
||||||
match retrieve_data::fetch_data_from_overpass(args.bbox, args.debug, "requests", None) {
|
match retrieve_data::fetch_data_from_overpass(args.bbox, args.debug, "requests", None) {
|
||||||
Ok(raw_data) => {
|
Ok(raw_data) => {
|
||||||
let (mut parsed_elements, mut xzbbox) =
|
let (mut parsed_elements, mut xzbbox) =
|
||||||
@@ -794,15 +1066,36 @@ fn gui_start_generation(
|
|||||||
&mut ground,
|
&mut ground,
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = data_processing::generate_world(parsed_elements, xzbbox, ground, &args);
|
let _ = data_processing::generate_world_with_options(
|
||||||
// Session lock will be automatically released when _session_lock goes out of scope
|
parsed_elements,
|
||||||
|
xzbbox.clone(),
|
||||||
|
args.bbox,
|
||||||
|
ground,
|
||||||
|
&args,
|
||||||
|
generation_options.clone(),
|
||||||
|
ParallelConfig::default(), // Use parallel processing
|
||||||
|
);
|
||||||
|
// Explicitly release session lock before showing Done message
|
||||||
|
// so Minecraft can open the world immediately
|
||||||
|
drop(_session_lock);
|
||||||
|
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||||
|
println!("{}", "Done! World generation completed.".green().bold());
|
||||||
|
|
||||||
|
// Start map preview generation silently in background (Java only)
|
||||||
|
if world_format == WorldFormat::JavaAnvil {
|
||||||
|
let preview_info = data_processing::MapPreviewInfo::new(
|
||||||
|
generation_options.path.clone(),
|
||||||
|
&xzbbox,
|
||||||
|
);
|
||||||
|
data_processing::start_map_preview_generation(preview_info);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = format!("Failed to fetch data: {e}");
|
emit_gui_error(&e.to_string());
|
||||||
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(error_msg)
|
Err(e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
43
gui-src/css/bbox.css → src/gui/css/bbox.css
vendored
@@ -8,13 +8,9 @@ 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 {
|
||||||
position: absolute;
|
display: none;
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
bottom: 0;
|
|
||||||
border: 0 0 7px 0;
|
|
||||||
z-index: 10000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#coord-format {
|
#coord-format {
|
||||||
@@ -344,4 +340,39 @@ body,
|
|||||||
filter: blur(1px) sepia(1) invert(1);
|
filter: blur(1px) sepia(1) invert(1);
|
||||||
transition: all 1s ease;
|
transition: all 1s ease;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* World Preview Button in Edit Toolbar */
|
||||||
|
.leaflet-draw-toolbar .leaflet-draw-edit-preview {
|
||||||
|
background-position: -31px -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled,
|
||||||
|
.leaflet-draw-toolbar .leaflet-draw-edit-preview.editing-mode {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-draw-toolbar .leaflet-draw-edit-preview.active {
|
||||||
|
background-color: #a0d0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-preview-slider-container {
|
||||||
|
padding: 6px 8px !important;
|
||||||
|
background: white !important;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-preview-slider-container a {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-preview-slider {
|
||||||
|
width: 80px;
|
||||||
|
height: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #3887BE;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 418 B After Width: | Height: | Size: 418 B |
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 205 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 207 B After Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 328 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 849 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
src/gui/css/maps/images/spritesheet-2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/gui/css/maps/images/spritesheet.png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -158,12 +158,13 @@
|
|||||||
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: -212px -2px;
|
background-position: -152px -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
|
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
|
||||||
background-position: -242px -2px;
|
background-position: -182px -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
314
gui-src/css/styles.css → src/gui/css/styles.css
vendored
@@ -32,9 +32,12 @@ p {
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 6em;
|
height: 6em;
|
||||||
padding: 1.5em;
|
padding-top: 0.4em;
|
||||||
|
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 {
|
||||||
@@ -59,10 +62,11 @@ a:hover {
|
|||||||
|
|
||||||
.flex-container {
|
.flex-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 15px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
min-height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
@@ -74,26 +78,70 @@ a:hover {
|
|||||||
|
|
||||||
.map-box,
|
.map-box,
|
||||||
.controls-box {
|
.controls-box {
|
||||||
width: 45%;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-box {
|
||||||
|
width: 63%;
|
||||||
|
min-height: 420px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
background: #575757;
|
background: #575757;
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
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: auto;
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bbox-selection-text {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #ffffff;
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
min-height: 2.5em;
|
||||||
|
line-height: 1.25em;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info-text {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #ececec;
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
min-height: 1.5em;
|
||||||
|
line-height: 1.25em;
|
||||||
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-container {
|
.map-container {
|
||||||
border: 2px solid #e0e0e0;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h2 {
|
.section h2 {
|
||||||
@@ -133,18 +181,25 @@ button:hover {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-section h2 {
|
.progress-row {
|
||||||
margin-bottom: 8px;
|
display: flex;
|
||||||
text-align: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
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 {
|
||||||
@@ -154,15 +209,6 @@ 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;
|
||||||
@@ -181,7 +227,7 @@ button:hover {
|
|||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
color: #f6f6f6;
|
color: #f6f6f6;
|
||||||
background-color: #2f2f2f;
|
background-color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@@ -213,10 +259,73 @@ 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: 1000;
|
z-index: 20001;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -229,7 +338,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: #797979;
|
background-color: #717171;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #797979;
|
border: 1px solid #797979;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -249,6 +358,33 @@ button:hover {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal actions/buttons */
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-accent);
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-accent-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#terrain-toggle {
|
#terrain-toggle {
|
||||||
accent-color: #fecc44;
|
accent-color: #fecc44;
|
||||||
}
|
}
|
||||||
@@ -281,6 +417,10 @@ button:hover {
|
|||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#telemetry-toggle {
|
||||||
|
accent-color: #fecc44;
|
||||||
|
}
|
||||||
|
|
||||||
.scale-slider-container label {
|
.scale-slider-container label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
@@ -306,7 +446,7 @@ button:hover {
|
|||||||
|
|
||||||
#bbox-coords {
|
#bbox-coords {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 5px;
|
||||||
border: 1px solid #fecc44;
|
border: 1px solid #fecc44;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -318,6 +458,20 @@ 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;
|
||||||
@@ -329,6 +483,75 @@ 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 {
|
||||||
@@ -353,7 +576,7 @@ button:hover {
|
|||||||
|
|
||||||
.license-button-row {
|
.license-button-row {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 10px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.license-button {
|
.license-button {
|
||||||
@@ -389,11 +612,39 @@ button:hover {
|
|||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Generation mode dropdown styling */
|
||||||
|
.generation-mode-dropdown {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 180px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #fecc44;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #0f0f0f;
|
||||||
|
appearance: menulist;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generation-mode-dropdown option {
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.generation-mode-dropdown {
|
||||||
|
background-color: #0f0f0f98;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #fecc44;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Language dropdown styling */
|
/* Language dropdown styling */
|
||||||
.language-dropdown {
|
.language-dropdown {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
padding: 5px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #fecc44;
|
border: 1px solid #fecc44;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
@@ -421,7 +672,7 @@ button:hover {
|
|||||||
.theme-dropdown {
|
.theme-dropdown {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
padding: 5px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #fecc44;
|
border: 1px solid #fecc44;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
@@ -476,9 +727,12 @@ button:hover {
|
|||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button .gear-icon::before {
|
.settings-button svg {
|
||||||
content: "⚙️";
|
stroke: white;
|
||||||
font-size: 18px;
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
min-width: 22px;
|
||||||
|
min-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo Animation */
|
/* Logo Animation */
|
||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 487 B |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |