Compare commits
929 Commits
dev-build-
...
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 | ||
|
|
0dcf4d2bc9 | ||
|
|
0fd8f938e7 | ||
|
|
97f891b5c9 | ||
|
|
4a92d3f249 | ||
|
|
fac0e06b1d | ||
|
|
21d9dbddbc | ||
|
|
002ae8c7cf | ||
|
|
09ab08bec2 | ||
|
|
14ecfa8aab | ||
|
|
532e3bbb89 | ||
|
|
ce7a2ad97e | ||
|
|
bc7a0d80c4 | ||
|
|
074a4f0b52 | ||
|
|
c4e2bf1b4d | ||
|
|
53e6be6f4a | ||
|
|
37f1510399 | ||
|
|
dadd6917f9 | ||
|
|
d7dd4f64ba | ||
|
|
6f47321ee7 | ||
|
|
60b5f45c39 | ||
|
|
c714541434 | ||
|
|
162a650f53 | ||
|
|
21bed97226 | ||
|
|
07d4ad94bb | ||
|
|
0c403fd09e | ||
|
|
c8cdf74f02 | ||
|
|
ef6debdff2 | ||
|
|
dba37f9f2d | ||
|
|
1f02c40cb9 | ||
|
|
9351883ae0 | ||
|
|
2e3b66fe4a | ||
|
|
d6151ab333 | ||
|
|
1a56e5f08f | ||
|
|
e3335cb6e6 | ||
|
|
3a65e8b877 | ||
|
|
fa84a8e4cc | ||
|
|
257a3bcac4 | ||
|
|
f262613871 | ||
|
|
43215ad19f | ||
|
|
00541e43ef | ||
|
|
732693b71d | ||
|
|
eaaf320c04 | ||
|
|
79507637c2 | ||
|
|
a722235e97 | ||
|
|
53f29e6e4d | ||
|
|
89363f926a | ||
|
|
c06b5df768 | ||
|
|
909e2ab97f | ||
|
|
43ed19f456 | ||
|
|
5ec087c3ed | ||
|
|
20eee2b0ec | ||
|
|
821b9b9cb5 | ||
|
|
fff9783806 | ||
|
|
fd66b51169 | ||
|
|
f8a2dc622a | ||
|
|
9aeb35ac99 | ||
|
|
84bef5fcd1 | ||
|
|
0230e7cb4c | ||
|
|
fd9af964cc | ||
|
|
954391d468 | ||
|
|
c0ab878b91 | ||
|
|
af24887399 | ||
|
|
3cb9496252 | ||
|
|
a76d0e7121 | ||
|
|
a9e883fc86 | ||
|
|
6da977cc11 | ||
|
|
acbe81db93 | ||
|
|
0249a0e933 | ||
|
|
7748ef0cd6 | ||
|
|
5cb82abc9b | ||
|
|
4fa486bc27 | ||
|
|
4fb467988c | ||
|
|
adc0bcb3ee | ||
|
|
b8b41a3541 | ||
|
|
c02ee9e988 | ||
|
|
3ccc6c0853 | ||
|
|
b1c0aea317 | ||
|
|
651aed65fb | ||
|
|
f180d84ef2 | ||
|
|
2cee6f6ab2 | ||
|
|
bbf52e8b89 | ||
|
|
8ec67f8420 | ||
|
|
a431a05f0d | ||
|
|
37b80f02db | ||
|
|
7dff3ac865 | ||
|
|
7e1981672b | ||
|
|
e0c3ab3451 | ||
|
|
9ea526f0eb | ||
|
|
3307f63864 | ||
|
|
fb70f00da8 | ||
|
|
58cff26d37 | ||
|
|
a7c5e1baee | ||
|
|
29fc3ce916 | ||
|
|
d3365d01b1 | ||
|
|
04cbb6427e | ||
|
|
3680bc8849 | ||
|
|
5cfb3faece | ||
|
|
3217eea558 | ||
|
|
c3b2624307 | ||
|
|
12a401c690 | ||
|
|
d188f6d579 | ||
|
|
d2fb65c715 | ||
|
|
d370c72425 | ||
|
|
3e9f71f81a | ||
|
|
6b2c4eb55b | ||
|
|
88a7cf2f6e | ||
|
|
d7296f69d3 | ||
|
|
c298300a27 | ||
|
|
9b752a8915 | ||
|
|
0285e63dbf | ||
|
|
33097aa302 | ||
|
|
c6504a2fa3 | ||
|
|
8da93ab628 | ||
|
|
8ba4d3ddef | ||
|
|
74ceb3afb8 | ||
|
|
c886894e14 | ||
|
|
8d6c666f75 | ||
|
|
de2bce5a0a | ||
|
|
8e9d6f0ad1 | ||
|
|
eb13dabd16 | ||
|
|
af612095fa | ||
|
|
8132881f86 | ||
|
|
994ae0d770 | ||
|
|
da10ffc655 | ||
|
|
d987ae73d0 | ||
|
|
f4accbbf8c | ||
|
|
9887efd1e3 | ||
|
|
50f5a1a844 | ||
|
|
5cd590f165 | ||
|
|
1d031a75bc | ||
|
|
a17ef09727 | ||
|
|
ed20642e8b | ||
|
|
501825d05b | ||
|
|
6ee1b55924 | ||
|
|
1a206b8e69 | ||
|
|
52fe87b90d | ||
|
|
3ea85009e7 | ||
|
|
bd5a7b9bee | ||
|
|
799c7d6007 | ||
|
|
7f28089c14 | ||
|
|
4bb69b6b16 | ||
|
|
36d084d02f | ||
|
|
9d4ce48d8d | ||
|
|
6f7efd8a1c | ||
|
|
37ec4be8fc | ||
|
|
6b43f12769 | ||
|
|
06357abe97 | ||
|
|
86c92104dd | ||
|
|
c9551d8ec0 | ||
|
|
6b676eab08 | ||
|
|
2a6a507082 | ||
|
|
9a7c00a3c4 | ||
|
|
57dcd72979 | ||
|
|
486dcdee2e | ||
|
|
7f01b596cb | ||
|
|
4a672811c0 | ||
|
|
0ec64fc13d | ||
|
|
f9796c60a0 | ||
|
|
2fd0244889 | ||
|
|
7e6ab21a9e | ||
|
|
dd7578d8d3 | ||
|
|
4b766095c8 | ||
|
|
e8d718772a | ||
|
|
0a488655d2 | ||
|
|
aed978947d | ||
|
|
f4d35eebf7 | ||
|
|
97fe4e264b | ||
|
|
d53af0a3f4 | ||
|
|
3e7e4b663e | ||
|
|
a86506b28d | ||
|
|
b9250651a2 | ||
|
|
98ed4e3f1c | ||
|
|
73776578bf | ||
|
|
aa36db7f13 | ||
|
|
38c2550f0d | ||
|
|
4c8108bdf9 | ||
|
|
0ff6cabcf5 | ||
|
|
dd22bbb943 | ||
|
|
5bbe3f1ec9 | ||
|
|
1db53c4656 | ||
|
|
43f3f2f67d | ||
|
|
dc3ea8df5e | ||
|
|
f9776c4472 | ||
|
|
27f8c70a10 | ||
|
|
6842a44e2a | ||
|
|
b28ce5cc12 | ||
|
|
d56894debd | ||
|
|
2571e43f7e | ||
|
|
0d3f1b6b5d | ||
|
|
174374e514 | ||
|
|
f3fbe6e84d | ||
|
|
34d5d4ae22 | ||
|
|
666e47ce7e | ||
|
|
5a3b92b4f3 | ||
|
|
a300f793ec | ||
|
|
cccea1d87a | ||
|
|
19bac86e5e | ||
|
|
491057255c | ||
|
|
44fe74083a | ||
|
|
10fbc7e85a | ||
|
|
cdafe0488a | ||
|
|
df3c9d2507 | ||
|
|
5cbcd68f46 | ||
|
|
fe10b7d079 | ||
|
|
b8ffb51bdc | ||
|
|
404e733f65 | ||
|
|
2fd3d47ca1 | ||
|
|
a327552fba | ||
|
|
e95a6ff738 | ||
|
|
d2f1fca921 | ||
|
|
7df43604e2 | ||
|
|
96e40cf15f | ||
|
|
e6dfa5d59e | ||
|
|
b760f0e720 | ||
|
|
fec3059157 | ||
|
|
57fee8b273 | ||
|
|
6c62ada7cc | ||
|
|
ead52305cf | ||
|
|
662c8a9a27 | ||
|
|
101b340071 | ||
|
|
39a7461f82 | ||
|
|
c49605d993 | ||
|
|
3e3948d345 | ||
|
|
d32e4edaa3 | ||
|
|
d115ee2ec7 | ||
|
|
1a41766f2e | ||
|
|
b191bc294a | ||
|
|
d146f3fd83 | ||
|
|
eea839f5ad | ||
|
|
2c0c475f57 | ||
|
|
37ed36b433 | ||
|
|
548e0f9456 | ||
|
|
eb7ec70f39 | ||
|
|
cb32be70b2 | ||
|
|
8fbf432b5b | ||
|
|
e86937ec1b | ||
|
|
71e28d1403 | ||
|
|
de79896dc5 | ||
|
|
047e3a0feb | ||
|
|
97d59d8e0c | ||
|
|
fda9a3a6b2 | ||
|
|
82145250ae | ||
|
|
4df49422df | ||
|
|
db123792d4 | ||
|
|
526b6ae5e8 | ||
|
|
55e8688031 | ||
|
|
8001014454 | ||
|
|
188bacad88 | ||
|
|
a2cd96214d | ||
|
|
103693b28d | ||
|
|
07503c1763 | ||
|
|
03c72c7a14 | ||
|
|
58157cee37 | ||
|
|
806dd11b51 | ||
|
|
6a94435d33 | ||
|
|
a188f521bb | ||
|
|
2969966fd1 | ||
|
|
b4c6f55ef4 | ||
|
|
654124be75 | ||
|
|
384f541254 | ||
|
|
5e44313730 | ||
|
|
885c3a7127 | ||
|
|
b86289c49a | ||
|
|
144ac58468 | ||
|
|
03a4997cac | ||
|
|
fd4aefaf2b | ||
|
|
4edfddb33c | ||
|
|
f46fb696ce | ||
|
|
0bc6967d83 | ||
|
|
a37612626c | ||
|
|
5e8bb3f5a0 | ||
|
|
4226438075 | ||
|
|
cd30fc74af | ||
|
|
62c3f94857 | ||
|
|
5de8f1c78d | ||
|
|
d0295bfd40 | ||
|
|
b3b3436efd | ||
|
|
dea764783b | ||
|
|
f42d15780c | ||
|
|
d19cf5aef2 | ||
|
|
bf2e3d5b94 | ||
|
|
4145e30194 | ||
|
|
6ec1827178 | ||
|
|
3a05023540 | ||
|
|
b0437cfc62 | ||
|
|
883b4b56dc | ||
|
|
00b5e5393a | ||
|
|
948bef41ae | ||
|
|
fe54a11fde | ||
|
|
400004ec51 | ||
|
|
c6377f7155 | ||
|
|
20e6621bb5 | ||
|
|
fdc5156036 | ||
|
|
23583c77de | ||
|
|
6d599e19c0 | ||
|
|
4ff0d6e2be | ||
|
|
95f1c4d9b5 | ||
|
|
b13eb1fcdf | ||
|
|
0866649444 | ||
|
|
958160918b | ||
|
|
8bd98a72aa | ||
|
|
f7c3404328 | ||
|
|
ef2f341456 | ||
|
|
4e82b0a497 | ||
|
|
d0b671625b | ||
|
|
657d50b6e0 | ||
|
|
922e752684 | ||
|
|
873f6aa995 | ||
|
|
0cd4c0e28d | ||
|
|
97eebbb977 | ||
|
|
7ae6dc8143 | ||
|
|
a112cda485 | ||
|
|
116f753e45 | ||
|
|
3c41be1ad0 | ||
|
|
7ad15c70c3 | ||
|
|
bf53f4bb2f | ||
|
|
be819c01a0 | ||
|
|
899cb2ee16 | ||
|
|
b63b3ae05a | ||
|
|
48c2e93292 | ||
|
|
4395eb6dd6 | ||
|
|
e8bbbec0f9 | ||
|
|
b16c0f84ac | ||
|
|
6efee57cbc | ||
|
|
41ae099887 | ||
|
|
781bb79ae5 | ||
|
|
3b378296a3 | ||
|
|
703b83a2fd | ||
|
|
c7e1b5bf67 | ||
|
|
b8f45ac178 | ||
|
|
e9133c136a | ||
|
|
92881cf3b3 | ||
|
|
421fe47ecf | ||
|
|
0c2c99544e | ||
|
|
491716f0fc | ||
|
|
96f9bc736e | ||
|
|
ffe694f9d8 | ||
|
|
c7b6df92ab | ||
|
|
5b67f8ee84 | ||
|
|
179cb6fa2d | ||
|
|
15c8265a21 | ||
|
|
7ec0b9aa03 | ||
|
|
c1ac1774fb | ||
|
|
b46cd17ff4 | ||
|
|
2789a40fd0 | ||
|
|
6f71bc3fdb | ||
|
|
7a102dec51 | ||
|
|
320acac80d | ||
|
|
0cad9fa307 | ||
|
|
b03202ac71 | ||
|
|
32ae0778e2 | ||
|
|
faef164abe | ||
|
|
d9930c2b44 | ||
|
|
1a0fc291b0 | ||
|
|
bb14b1da49 | ||
|
|
c0edbaa92c | ||
|
|
bf69996427 | ||
|
|
1c734cdd7c | ||
|
|
b815a3b563 | ||
|
|
32745217a7 | ||
|
|
11cec0fc15 | ||
|
|
f9cc2acf11 | ||
|
|
ed529071ae | ||
|
|
9a288e3966 | ||
|
|
e5f0717838 | ||
|
|
c258892b45 | ||
|
|
151ced7625 | ||
|
|
73e8bab4a0 | ||
|
|
3dfa2ca62e | ||
|
|
f1d3c0b980 | ||
|
|
bcc986969f | ||
|
|
2d07e9c2b9 | ||
|
|
a213bace19 | ||
|
|
7552a99a34 | ||
|
|
2a52538a1e | ||
|
|
ec3f5825b4 | ||
|
|
1294c3ef3c | ||
|
|
ca2fd0b1c6 | ||
|
|
b5596548db | ||
|
|
6167466b2f | ||
|
|
5e18402017 | ||
|
|
ec76a5f2e2 | ||
|
|
369e422b4a | ||
|
|
349f15807a | ||
|
|
6fdf28ab81 | ||
|
|
c961287290 | ||
|
|
1fefb20f71 | ||
|
|
022c44a7d4 | ||
|
|
6b8a8ad666 | ||
|
|
c0e1d0e3b4 | ||
|
|
fb64d0c360 | ||
|
|
19cac3ea81 | ||
|
|
8bad88bdca | ||
|
|
e62ba6a95a | ||
|
|
d1b18871ce | ||
|
|
4d47a22904 | ||
|
|
d4b74fc48a | ||
|
|
38752f2666 | ||
|
|
77a9c23ed3 | ||
|
|
a991f60d29 | ||
|
|
966241aa52 | ||
|
|
e9cc0ced41 | ||
|
|
987b8b3d74 | ||
|
|
f398c7e6b8 | ||
|
|
9a759c39db | ||
|
|
856cba162e | ||
|
|
55d0208a32 | ||
|
|
b1d20cbd1a | ||
|
|
cca4d1a361 | ||
|
|
ae5a838fa3 | ||
|
|
06974e2bf7 | ||
|
|
b769ff34de | ||
|
|
0f8e302aff | ||
|
|
597051303c | ||
|
|
08bdc64999 | ||
|
|
3723127132 | ||
|
|
f8a1da1967 | ||
|
|
9352ee62b7 | ||
|
|
8816af5c75 | ||
|
|
2fb09d1fba | ||
|
|
c3ce8829f3 | ||
|
|
a3f0d5e367 | ||
|
|
d93b771422 | ||
|
|
4271dc3b7d | ||
|
|
6732697f46 | ||
|
|
4d94ab98a4 | ||
|
|
2c8418bf90 | ||
|
|
a1ed2b9dd2 | ||
|
|
9b7cabc549 | ||
|
|
21b0b99d7e | ||
|
|
db47f6ab20 | ||
|
|
ef261a24e4 | ||
|
|
898457e06c | ||
|
|
0f84c8a38c | ||
|
|
f416f650c2 | ||
|
|
f39b760aa0 | ||
|
|
bfbb8b05a2 | ||
|
|
3173a8f7d8 | ||
|
|
9dac978a69 | ||
|
|
b98c043bb8 | ||
|
|
2a1233c463 | ||
|
|
e1869fbbbc | ||
|
|
bfba692aaf | ||
|
|
1382e05e25 | ||
|
|
642928057a | ||
|
|
1749f6a1f2 | ||
|
|
4c08c5fe72 | ||
|
|
8a4961a0a6 | ||
|
|
05569fb0fc | ||
|
|
f203ad1c09 | ||
|
|
4d123211ee | ||
|
|
8a191e563d | ||
|
|
9c0db6f1c7 | ||
|
|
a7638d971a | ||
|
|
fa6a023b7c | ||
|
|
2ac4cacd2d | ||
|
|
b334212667 | ||
|
|
dc7f3f973e | ||
|
|
44f58dcd22 | ||
|
|
4c1643281d | ||
|
|
6e113746d4 | ||
|
|
6a1d6d32ff | ||
|
|
8d6d10fc47 | ||
|
|
c022611d08 | ||
|
|
5a15ba3a62 | ||
|
|
c526f295c3 | ||
|
|
0e0a08cfc9 | ||
|
|
d992e5eb87 | ||
|
|
5c0e24ee73 | ||
|
|
9d85de61a3 | ||
|
|
b23749b330 | ||
|
|
77debbbc20 | ||
|
|
8970ce5ffe | ||
|
|
7a36ac9c3f | ||
|
|
4363dfd558 | ||
|
|
1aac2e3068 | ||
|
|
d16cedde9a | ||
|
|
59b71ebd84 | ||
|
|
2e01c8c710 | ||
|
|
bff58b39fd | ||
|
|
887e622428 | ||
|
|
931f03a784 | ||
|
|
c1c43d9244 | ||
|
|
8a84a08316 | ||
|
|
2c72a61324 | ||
|
|
bf263a4f8c | ||
|
|
50ba0bad6e | ||
|
|
1d80e6e047 | ||
|
|
b5e9bb885d | ||
|
|
665b8f095b | ||
|
|
75bee6ca97 | ||
|
|
41055825ef | ||
|
|
6fd3a8eda7 | ||
|
|
c578a436b4 | ||
|
|
211ac4ed72 | ||
|
|
3d0a1c3c4d | ||
|
|
0c9c8e1366 | ||
|
|
0e28d34224 | ||
|
|
3b1425cd30 | ||
|
|
ce0bce99fb | ||
|
|
cde0042b85 | ||
|
|
622edaff07 | ||
|
|
ca760563d1 | ||
|
|
f278d629eb | ||
|
|
2c319bae5d | ||
|
|
809ff2793d | ||
|
|
3ba9c58f85 | ||
|
|
66b7f47e32 | ||
|
|
a96911e112 | ||
|
|
112481f36f | ||
|
|
65cd0d453a | ||
|
|
febd1b7a3e | ||
|
|
6b527855e4 | ||
|
|
cc1e28b80a | ||
|
|
a969d65ae4 | ||
|
|
acf123488a | ||
|
|
938a2d74ad | ||
|
|
33144a149c | ||
|
|
fbb0f845cd | ||
|
|
c71cd111ca | ||
|
|
04d9d5269f | ||
|
|
f53e4d65e3 | ||
|
|
c39d6fc1ef | ||
|
|
a1c69b4cb0 | ||
|
|
b57719b0f4 | ||
|
|
893945b98a | ||
|
|
a57c5f1f1b | ||
|
|
718389e88d | ||
|
|
3f4536e04f | ||
|
|
1f79285e83 | ||
|
|
31dca8b0b7 | ||
|
|
54791388d1 | ||
|
|
7fa05d3e7b | ||
|
|
daf07bd743 | ||
|
|
163ab0407d | ||
|
|
0361e7d63d | ||
|
|
bc422460ff | ||
|
|
53f0097884 | ||
|
|
bbfb63886d | ||
|
|
928f487a29 | ||
|
|
ff7a09061e | ||
|
|
911ec00ddb | ||
|
|
54137f2117 | ||
|
|
50d38a1dd4 | ||
|
|
e5ab62eb18 | ||
|
|
1a8a3114f4 | ||
|
|
a52ba2bd39 | ||
|
|
8195f0bc6e | ||
|
|
4df4a3de32 | ||
|
|
e65a1ee37e | ||
|
|
66e47e190a | ||
|
|
b9133664ec | ||
|
|
032e084265 | ||
|
|
49202b0f33 | ||
|
|
8755fd3285 | ||
|
|
982ef56b29 | ||
|
|
28070d3c51 | ||
|
|
d9a0a4aa86 | ||
|
|
1f0f6676ed | ||
|
|
ff44fe4eda | ||
|
|
42976b0113 | ||
|
|
55b170fb7d | ||
|
|
852bbf2236 | ||
|
|
cc6080c0c3 | ||
|
|
5672ca3f1e | ||
|
|
fc29544835 | ||
|
|
bf6ac1f61a | ||
|
|
8c17a5b0bb | ||
|
|
bc6ffa3bf9 | ||
|
|
dec0317790 | ||
|
|
091acd5849 | ||
|
|
056203fb21 | ||
|
|
415c9d33fe | ||
|
|
1820112093 | ||
|
|
c573a02fd6 | ||
|
|
4c6b9833eb | ||
|
|
8cb8d76357 | ||
|
|
fc105b84e0 | ||
|
|
f873a7ab69 | ||
|
|
61a67240a6 | ||
|
|
c0753ab23e | ||
|
|
9461d1f46e | ||
|
|
3036237fdf | ||
|
|
95e71b8da5 | ||
|
|
255786bc93 | ||
|
|
3c552fe5ed | ||
|
|
49ff415316 | ||
|
|
71315c60ea | ||
|
|
18b1ea0e0b | ||
|
|
c6a8da0b40 | ||
|
|
308498550b | ||
|
|
f7e917edc7 | ||
|
|
8221da46e0 | ||
|
|
3645b7edd7 | ||
|
|
606779f681 | ||
|
|
dce5f472f0 | ||
|
|
f4bf8c1d3c | ||
|
|
7ae9ac35d1 | ||
|
|
686ac2c181 | ||
|
|
b334ed9841 | ||
|
|
96dc9ced30 | ||
|
|
03d86f4552 | ||
|
|
6e3406f117 | ||
|
|
7e929d5ef3 | ||
|
|
49a5ed8157 | ||
|
|
f60170ef59 | ||
|
|
b34bf4f646 | ||
|
|
efc14821cf | ||
|
|
ba9c7df5ff | ||
|
|
3eb29a0c21 | ||
|
|
17ead3c2fc | ||
|
|
f661f174bd | ||
|
|
7792930277 | ||
|
|
18d3e45d2c | ||
|
|
c2b9f87688 | ||
|
|
0850bca4de | ||
|
|
3e88da384a | ||
|
|
ab21b88c90 | ||
|
|
1609608ae8 | ||
|
|
ac8c5bf466 | ||
|
|
e4c6208b22 | ||
|
|
f5144d7ba4 | ||
|
|
f50e10891b | ||
|
|
66819a577b | ||
|
|
c0778c265f | ||
|
|
8e29b1422e | ||
|
|
c9c0cb2f2c | ||
|
|
18de6ec808 | ||
|
|
9f521489ab |
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.
|
||||
|
||||
**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**
|
||||
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**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. If you used any more custom settings, please provide them here too.
|
||||
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
|
||||
|
||||
56
.github/workflows/ci-build.yml
vendored
@@ -1,35 +1,32 @@
|
||||
name: CI Build
|
||||
|
||||
# Trigger on pull request creation, update, and on pushes to the main branch
|
||||
# Trigger CI on pull requests when relevant files change, and pushes to main
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'src/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.os == 'windows-latest' && 'x86_64-pc-windows-msvc' || 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }}
|
||||
components: clippy
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y software-properties-common
|
||||
@@ -39,14 +36,39 @@ jobs:
|
||||
sudo apt install -y libgtk-3-dev build-essential pkg-config libglib2.0-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- name: Check clippy lints
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Install Rust dependencies
|
||||
run: cargo fetch
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y software-properties-common
|
||||
sudo add-apt-repository universe
|
||||
echo "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc)-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
sudo apt update
|
||||
sudo apt install -y libgtk-3-dev build-essential pkg-config libglib2.0-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build (all targets, all features)
|
||||
run: cargo build --all-targets --all-features --release
|
||||
|
||||
- name: Run unit tests
|
||||
run: cargo test --all-targets --all-features
|
||||
|
||||
115
.github/workflows/pr-benchmark.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: PR Benchmark
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
if: |
|
||||
github.event_name == 'pull_request' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request != null &&
|
||||
contains(github.event.comment.body, 'retrigger-benchmark'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Create dummy Minecraft world directory
|
||||
run: |
|
||||
mkdir -p "./world/region"
|
||||
|
||||
- name: Build for release
|
||||
run: cargo build --release --no-default-features
|
||||
|
||||
- name: Start timer
|
||||
id: start_time
|
||||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run benchmark command with memory tracking
|
||||
id: benchmark
|
||||
run: |
|
||||
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
|
||||
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
|
||||
peak_kb=$(cat peak_mem_kb.txt)
|
||||
peak_mb=$((peak_kb / 1024))
|
||||
echo "peak_memory=${peak_mb}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: End timer and calculate duration
|
||||
id: end_time
|
||||
run: |
|
||||
end_time=$(date +%s)
|
||||
start_time=${{ steps.start_time.outputs.start_time }}
|
||||
duration=$((end_time - start_time))
|
||||
echo "duration=$duration" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Format duration and generate summary
|
||||
id: comment_body
|
||||
run: |
|
||||
duration=${{ steps.end_time.outputs.duration }}
|
||||
minutes=$((duration / 60))
|
||||
seconds=$((duration % 60))
|
||||
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
||||
|
||||
baseline_time=30
|
||||
diff=$((duration - baseline_time))
|
||||
abs_diff=${diff#-}
|
||||
|
||||
if [ "$diff" -lt -5 ]; then
|
||||
verdict="✅ This PR **improves generation time**."
|
||||
elif [ "$abs_diff" -le 4 ]; then
|
||||
verdict="🟢 Generation time is unchanged."
|
||||
elif [ "$diff" -le 15 ]; then
|
||||
verdict="⚠️ This PR **worsens generation time**."
|
||||
else
|
||||
verdict="🚨 This PR **drastically worsens generation time**."
|
||||
fi
|
||||
|
||||
baseline_mem=935
|
||||
mem_annotation=""
|
||||
if [ "$peak_mem" -gt 2000 ]; then
|
||||
mem_diff=$((peak_mem - baseline_mem))
|
||||
mem_percent=$((mem_diff * 100 / baseline_mem))
|
||||
mem_annotation=" (↗ ${mem_percent}% more)"
|
||||
fi
|
||||
|
||||
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
{
|
||||
echo "summary<<EOF"
|
||||
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
|
||||
echo "🧠 Peak memory usage: **${peak_mem} MB**${mem_annotation}"
|
||||
echo ""
|
||||
echo "📈 Compared against baseline: **${baseline_time}s**"
|
||||
echo "🧮 Delta: **${diff}s**"
|
||||
echo "🔢 Commit: [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA})"
|
||||
echo ""
|
||||
echo "${verdict}"
|
||||
echo ""
|
||||
echo "📅 **Last benchmark:** ${benchmark_time}"
|
||||
echo ""
|
||||
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Comment build time on PR
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: ${{ steps.comment_body.outputs.summary }}
|
||||
comment-tag: benchmark-report
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Pre-release Dev Build
|
||||
name: [DISABLED] Pre-release Dev Build
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -80,8 +80,8 @@ jobs:
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: "dev-build-${{ github.run_number }}"
|
||||
name: "Dev Build #${{ github.run_number }}"
|
||||
body: "Automated pre-release for testing purposes. This build may contain experimental features. For the latest official version, please download the latest stable release."
|
||||
name: "Experimental Development Build #${{ github.run_number }}"
|
||||
body: "Automated pre-release built from the main branch for testing purposes. This build may contain experimental features. For the latest official version, please download the latest stable release."
|
||||
draft: false
|
||||
prerelease: true
|
||||
makeLatest: false
|
||||
113
.github/workflows/release.yml
vendored
@@ -12,17 +12,25 @@ jobs:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
binary_name: arnis.exe
|
||||
asset_name: arnis-windows-x64.exe
|
||||
asset_name: arnis-windows.exe
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
binary_name: arnis
|
||||
asset_name: arnis-linux-x64
|
||||
asset_name: arnis-linux
|
||||
- os: macos-15-intel # Intel runner for x86_64 builds
|
||||
target: x86_64-apple-darwin
|
||||
binary_name: arnis
|
||||
asset_name: arnis-mac-intel
|
||||
- os: macos-latest # ARM64 runner for ARM64 builds
|
||||
target: aarch64-apple-darwin
|
||||
binary_name: arnis
|
||||
asset_name: arnis-mac-arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -45,41 +53,108 @@ jobs:
|
||||
run: cargo fetch
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename binary for release
|
||||
run: mv target/${{ matrix.target }}/release/${{ matrix.binary_name }} target/release/${{ matrix.asset_name }}
|
||||
|
||||
- name: Install Windows SDK
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
choco install windows-sdk-10.1 -y
|
||||
$env:Path += ";C:\Program Files (x86)\Windows Kits\10\bin\x64"
|
||||
shell: powershell
|
||||
|
||||
- name: Locate signtool.exe
|
||||
if: matrix.os == 'windows-latest'
|
||||
id: locate_signtool
|
||||
run: |
|
||||
$env:ProgramFilesX86 = [System.Environment]::GetFolderPath('ProgramFilesX86')
|
||||
$signtoolPath = Get-ChildItem -Path "$env:ProgramFilesX86\Windows Kits\10\bin" -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 -ExpandProperty FullName
|
||||
if (-not $signtoolPath) { throw "signtool.exe not found." }
|
||||
echo "signtool=$signtoolPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
shell: powershell
|
||||
|
||||
- name: Self-sign Windows executable
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
$password = ConvertTo-SecureString -String $env:WINDOWS_CERT_PASSWORD -Force -AsPlainText
|
||||
$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=Arnis' -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(5)
|
||||
Export-PfxCertificate -Cert $cert -FilePath arnis-cert.pfx -Password $password
|
||||
& $env:signtool sign /f arnis-cert.pfx /p $env:WINDOWS_CERT_PASSWORD /t http://timestamp.digicert.com target/release/${{ matrix.asset_name }}
|
||||
env:
|
||||
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
|
||||
shell: powershell
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-build
|
||||
path: target/release/${{ matrix.binary_name }}
|
||||
name: ${{ matrix.os }}-${{ matrix.target }}-build
|
||||
path: target/release/${{ matrix.asset_name }}
|
||||
|
||||
create-universal-macos:
|
||||
needs: build
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Download macOS Intel build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-15-intel-x86_64-apple-darwin-build
|
||||
path: ./intel
|
||||
|
||||
- name: Download macOS ARM64 build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-latest-aarch64-apple-darwin-build
|
||||
path: ./arm64
|
||||
|
||||
- name: Create universal binary
|
||||
run: |
|
||||
lipo -create -output arnis-mac-universal ./intel/arnis-mac-intel ./arm64/arnis-mac-arm64
|
||||
chmod +x arnis-mac-universal
|
||||
|
||||
- name: Upload universal binary
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: arnis-mac-universal
|
||||
|
||||
release:
|
||||
needs: build
|
||||
needs: [build, create-universal-macos]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Windows build artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-latest-build
|
||||
name: windows-latest-x86_64-pc-windows-msvc-build
|
||||
path: ./builds/windows
|
||||
|
||||
- name: Download Linux build artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ubuntu-latest-build
|
||||
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
|
||||
path: ./builds/linux
|
||||
|
||||
- name: Make Linux binary executable
|
||||
run: chmod +x ./builds/linux/arnis
|
||||
- name: Download macOS universal build artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: ./builds/macos
|
||||
|
||||
- name: Make Linux and macOS binaries executable
|
||||
run: |
|
||||
chmod +x ./builds/linux/arnis-linux
|
||||
chmod +x ./builds/macos/arnis-mac-universal
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
builds/windows/arnis.exe
|
||||
builds/linux/arnis
|
||||
builds/windows/arnis-windows.exe
|
||||
builds/linux/arnis-linux
|
||||
builds/macos/arnis-mac-universal
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
100
.github/workflows/test-macos-build.yml.disabled
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: Test macOS Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '.github/workflows/release.yml'
|
||||
- 'src/**'
|
||||
- 'Cargo.toml'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
jobs:
|
||||
test-macos-builds:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-apple-darwin
|
||||
asset_name: arnis-mac-intel
|
||||
- target: aarch64-apple-darwin
|
||||
asset_name: arnis-mac-arm64
|
||||
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: cargo fetch
|
||||
|
||||
- name: Build for ${{ matrix.target }}
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename binary
|
||||
run: mv target/${{ matrix.target }}/release/arnis target/${{ matrix.target }}/release/${{ matrix.asset_name }}
|
||||
|
||||
- name: Check binary architecture
|
||||
run: |
|
||||
file target/${{ matrix.target }}/release/${{ matrix.asset_name }}
|
||||
lipo -info target/${{ matrix.target }}/release/${{ matrix.asset_name }}
|
||||
|
||||
- name: Test binary execution (basic check)
|
||||
run: |
|
||||
chmod +x target/${{ matrix.target }}/release/${{ matrix.asset_name }}
|
||||
# Test that it at least shows help/version (don't run full generation)
|
||||
target/${{ matrix.target }}/release/${{ matrix.asset_name }} --help || echo "Help command completed"
|
||||
|
||||
- name: Upload test artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-${{ matrix.target }}-build
|
||||
path: target/${{ matrix.target }}/release/${{ matrix.asset_name }}
|
||||
|
||||
test-universal-binary:
|
||||
needs: test-macos-builds
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Download Intel build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test-x86_64-apple-darwin-build
|
||||
path: ./intel
|
||||
|
||||
- name: Download ARM64 build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test-aarch64-apple-darwin-build
|
||||
path: ./arm64
|
||||
|
||||
- name: Create and test universal binary
|
||||
run: |
|
||||
lipo -create -output arnis-mac-universal ./intel/arnis-mac-intel ./arm64/arnis-mac-arm64
|
||||
chmod +x arnis-mac-universal
|
||||
|
||||
# Verify it's actually universal
|
||||
echo "=== Universal Binary Info ==="
|
||||
file arnis-mac-universal
|
||||
lipo -info arnis-mac-universal
|
||||
|
||||
# Test execution
|
||||
echo "=== Testing Universal Binary ==="
|
||||
./arnis-mac-universal --help || echo "Universal binary help command completed"
|
||||
|
||||
# Check file size (should be sum of both architectures roughly)
|
||||
echo "=== File Sizes ==="
|
||||
ls -lah ./intel/arnis-mac-intel ./arm64/arnis-mac-arm64 arnis-mac-universal
|
||||
|
||||
- name: Upload universal binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-universal-build
|
||||
path: arnis-mac-universal
|
||||
7
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
/wiki
|
||||
*.mcworld
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.envrc
|
||||
/.direnv
|
||||
|
||||
# Build artifacts
|
||||
@@ -27,6 +29,9 @@ Thumbs.db
|
||||
# Generated files
|
||||
/export.json
|
||||
/parsed_osm_data.txt
|
||||
/elevation_debug.png
|
||||
/terrain-tile-cache
|
||||
/arnis-tile-cache
|
||||
/gen/
|
||||
/build/
|
||||
*.rmeta
|
||||
|
||||
2164
Cargo.lock
generated
60
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.1.3"
|
||||
version = "2.4.1"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -10,31 +10,53 @@ readme = "README.md"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
overflow-checks = true
|
||||
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = "2"
|
||||
tauri-build = {version = "2", optional = true}
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.1", features = ["derive"] }
|
||||
colored = "2.1.0"
|
||||
dirs = "5.0.1"
|
||||
fastanvil = "0.31.0"
|
||||
fastnbt = "2.5.0"
|
||||
flate2 = "1.0"
|
||||
base64 = "0.22.1"
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
dirs = {version = "6.0.0", optional = true }
|
||||
fastanvil = "0.32.0"
|
||||
fastnbt = "2.6.0"
|
||||
flate2 = "1.1"
|
||||
fnv = "1.0.7"
|
||||
fs2 = "0.4"
|
||||
geo = "0.29.3"
|
||||
indicatif = "0.17.8"
|
||||
geo = "0.31.0"
|
||||
image = "0.25"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
log = "0.4.22"
|
||||
once_cell = "1.19.0"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.7", features = ["blocking", "json"] }
|
||||
rfd = "0.15.1"
|
||||
semver = "1.0.23"
|
||||
rand_chacha = "0.3"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.16.0", optional = true }
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = "2"
|
||||
tauri-plugin-log = { version = "2.2.0" }
|
||||
tauri-plugin-shell = "2"
|
||||
tokio = { version = "1.42.0", features = ["full"] }
|
||||
tauri = { version = "2", optional = true }
|
||||
tauri-plugin-log = { version = "2.6.0", optional = true }
|
||||
tauri-plugin-shell = { version = "2", optional = true }
|
||||
tokio = { version = "1.48.0", features = ["full"], optional = true }
|
||||
bedrockrs_level = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_level", optional = true }
|
||||
bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_shared", optional = true }
|
||||
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
||||
vek = { version = "0.17", optional = true }
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.23.0"
|
||||
|
||||
121
README.md
@@ -1,108 +1,41 @@
|
||||
<p align="center">
|
||||
<img width="456" height="125" src="https://github.com/louis-e/arnis/blob/main/gui-src/images/logo.png?raw=true">
|
||||
</p>
|
||||
<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)
|
||||
# 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)
|
||||
|
||||
This open source project written in Rust generates any chosen location from the real world in Minecraft Java Edition with a high level of detail.
|
||||
Arnis creates complex and accurate Minecraft Java Edition (1.17+) and Bedrock Edition worlds that reflect real-world geography, topography, and architecture.
|
||||
|
||||
###### ⚠️ This Github page is the official project website. Do not download Arnis from any other website.
|
||||
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!
|
||||
|
||||
## :desktop_computer: Example
|
||||

|
||||
|
||||
By leveraging geospatial data from OpenStreetMap and utilizing the powerful capabilities of Rust, Arnis provides an efficient and robust solution for creating complex and accurate Minecraft worlds that reflect real-world geography and architecture.
|
||||
|
||||
Arnis is designed to handle large-scale data and generate rich, immersive environments that bring real-world cities, landmarks, and natural features into the Minecraft universe. Whether you're looking to replicate your hometown, explore urban environments, or simply build something unique and realistic, Arnis generates your vision.
|
||||

|
||||
<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
|
||||
<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.
|
||||
|
||||
Choose your area in Arnis using the rectangle tool and select your Minecraft world - then simply click on 'Start Generation'!
|
||||
The world will always be generated starting from the Minecraft coordinates 0 0 0 (/tp 0 0 0). This is the top left of your selected area.
|
||||
|
||||
If you choose to select an own world, make sure to generate a new flat world in advance in Minecraft.
|
||||
Choose your area on the map using the rectangle tool and select your Minecraft world - then simply click on <i>Start Generation</i>!
|
||||
Additionally, you can customize various generation settings, such as world scale, spawn point, or building interior generation.
|
||||
|
||||
Minecraft version 1.16.5 and below is currently not supported, but we are working on it! For the best results, use Minecraft version 1.21.4.
|
||||
## 📚 Documentation
|
||||
|
||||
## :floppy_disk: How it works
|
||||

|
||||
<img src="assets/git/documentation.png" width="100%" alt="Banner">
|
||||
|
||||
The raw data obtained from the API *[(see FAQ)](#question-faq)* includes each element (buildings, walls, fountains, farmlands, etc.) with its respective corner coordinates (nodes) and descriptive tags. When you run Arnis, the following steps are performed automatically to generate a Minecraft world:
|
||||
|
||||
#### Processing Pipeline
|
||||
1. **Fetching Data from the Overpass API:** The script retrieves geospatial data for the desired bounding box from the Overpass API.
|
||||
2. **Parsing Raw Data:** The raw data is parsed to extract essential information like nodes, ways, and relations. Nodes are converted into Minecraft coordinates, and relations are handled similarly to ways, ensuring all relevant elements are processed correctly. Relations and ways cluster several nodes into one specific object.
|
||||
3. **Prioritizing and Sorting Elements:** The elements (nodes, ways, relations) are sorted by priority to establish a layering system, which ensures that certain types of elements (e.g., entrances and buildings) are generated in the correct order to avoid conflicts and overlapping structures.
|
||||
4. **Generating Minecraft World:** The Minecraft world is generated using a series of element processors (generate_buildings, generate_highways, generate_landuse, etc.) that interpret the tags and nodes of each element to place the appropriate blocks in the Minecraft world. These processors handle the logic for creating 3D structures, roads, natural formations, and more, as specified by the processed data.
|
||||
5. **Generating Ground Layer:** A ground layer is generated based on the provided scale factors to provide a base for the entire Minecraft world. This step ensures all areas have an appropriate foundation (e.g., grass and dirt layers).
|
||||
6. **Saving the Minecraft World:** All the modified chunks are saved back to the Minecraft region files.
|
||||
|
||||
## :question: FAQ
|
||||
- *Wasn't this written in Python before?*<br>
|
||||
Yes! Arnis was initially developed in Python, which benefited from Python's open-source friendliness and ease of readability. This is why we strive for clear, well-documented code in the Rust port of this project to find the right balance. I decided to port the project to Rust to learn more about the language and push the algorithm's performance further. We were nearing the limits of optimization in Python, and Rust's capabilities allow for even better performance and efficiency. The old Python implementation is still available in the python-legacy branch.
|
||||
- *Where does the data come from?*<br>
|
||||
The geographic data is sourced from OpenStreetMap (OSM)[^1], a free, collaborative mapping project that serves as an open-source alternative to commercial mapping services. The data is accessed via the Overpass API, which queries OSM's database.
|
||||
- *How does the Minecraft world generation work?*<br>
|
||||
The script uses the [fastnbt](https://github.com/owengage/fastnbt) cargo package to interact with Minecraft's world format. This library allows Arnis to manipulate Minecraft region files, enabling the generation of real-world locations. The section 'Processing Pipeline' goes a bit further into the details and steps of the generation process itself.
|
||||
- *Where does the name come from?*<br>
|
||||
The project is named after the smallest city in Germany, Arnis[^2]. The city's small size made it an ideal test case for developing and debugging the algorithm efficiently.
|
||||
- *I don't have Minecraft installed but want to generate a world for my kids. How?*<br>
|
||||
When selecting a world, click on 'Select existing world' and choose a directory. The world will be generated there.
|
||||
- *Arnis instantly closes again or the window is empty!*<br>
|
||||
If you're on Windows, please install the [Evergreen Bootstrapper from Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH#download).
|
||||
- *What Minecraft version should I use?*<br>
|
||||
Please use Minecraft version 1.21.4 for the best results. Minecraft version 1.16.5 and below is currently not supported, but we are working on it!
|
||||
- *The generation did finish, but there's nothing in the world!*<br>
|
||||
Make sure to teleport to the generation starting point (/tp 0 0 0). If there is still nothing, you might need to travel a bit further into the positive X and positive Z direction.
|
||||
- *What features are in the world generation settings?*<br>
|
||||
**Winter Mode:** This setting changes the generation style to a snowy theme and adds snow layers to the ground.<br>
|
||||
**Scale Factor:** The scale factor determines the size of the generated world.<br>
|
||||
**Custom BBOX Input:** This setting allows you to manually input the bounding box coordinates for the area you want to generate.<br>
|
||||
**Floodfill-Timeout (Sec):** This setting determines the maximum time the floodfill algorithm is allowed to run before being terminated. Increasing this value may improve the generation of large water areas but may also increase processing time.<br>
|
||||
**Ground Height:** This setting determines the base height of the generated world and can be adjusted to create different terrain types.
|
||||
|
||||
## :memo: ToDo and Known Bugs
|
||||
Feel free to choose an item from the To-Do or Known Bugs list, or bring your own idea to the table. Bug reports shall be raised as a Github issue. Contributions are highly welcome and appreciated!
|
||||
- [ ] Fix compilation for Linux
|
||||
- [ ] Rotate maps (https://github.com/louis-e/arnis/issues/97)
|
||||
- [ ] Fix coastal cities generation duration time (water_areas.rs)
|
||||
- [ ] Add street names as signs
|
||||
- [ ] Add support for older Minecraft versions (<=1.16.5) (https://github.com/louis-e/arnis/issues/124, https://github.com/louis-e/arnis/issues/137)
|
||||
- [ ] Mapping real coordinates to Minecraft coordinates (https://github.com/louis-e/arnis/issues/29)
|
||||
- [ ] Add interior to buildings
|
||||
- [ ] Implement house roof types
|
||||
- [ ] Evaluate and implement elevation (https://github.com/louis-e/arnis/issues/66)
|
||||
- [ ] Add support for inner attribute in multipolygons and multipolygon elements other than buildings
|
||||
- [ ] Fix Github Action Workflow for releasing MacOS Binary
|
||||
- [ ] Evaluate and implement faster region saving
|
||||
- [ ] Refactor bridges implementation
|
||||
- [ ] Refactor railway implementation
|
||||
- [ ] Better code documentation
|
||||
- [ ] Refactor fountain structure implementation
|
||||
- [ ] Luanti Support (https://github.com/louis-e/arnis/issues/120)
|
||||
- [ ] Minecraft Bedrock Edition Support (https://github.com/louis-e/arnis/issues/148)
|
||||
- [x] Support multipolygons (https://github.com/louis-e/arnis/issues/112, https://github.com/louis-e/arnis/issues/114)
|
||||
- [x] Memory optimization
|
||||
- [x] Design and implement a GUI
|
||||
- [x] Automatic new world creation instead of using an existing world
|
||||
- [x] Fix faulty empty chunks ([https://github.com/owengage/fastnbt/issues/120](https://github.com/owengage/fastnbt/issues/120)) (workaround found)
|
||||
- [x] Setup fork of [https://github.com/aaronr/bboxfinder.com](https://github.com/aaronr/bboxfinder.com) for easy bbox picking
|
||||
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.
|
||||
|
||||
## :trophy: Open Source
|
||||
#### Key objectives of this project
|
||||
- **Modularity**: Ensure that all components (e.g., data fetching, processing, and world generation) are cleanly separated into distinct modules for better maintainability and scalability.
|
||||
- **Performance Optimization**: Utilize Rust’s memory safety and concurrency features to optimize the performance of the world generation process.
|
||||
- **Performance Optimization**: We aim to keep a good performance and speed of the world generation process.
|
||||
- **Comprehensive Documentation**: Detailed in-code documentation for a clear structure and logic.
|
||||
- **User-Friendly Experience**: Focus on making the project easy to use for end users.
|
||||
- **Cross-Platform Support**: Ensure the project runs smoothly on Windows, macOS, and Linux.
|
||||
- **Cross-Platform Support**: We want this project to run smoothly on Windows, macOS, and Linux.
|
||||
|
||||
#### 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. We encourage discussions and suggestions to ensure the project remains modular, optimized, and easy to use for the community. You can use the parameter --debug to get a more detailed output of the processed values, which can be helpful for debugging and development. 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.
|
||||
|
||||
Build and run it using: ```cargo run --release -- --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```<br>
|
||||
For the GUI: ```cargo run --release```<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>
|
||||
|
||||
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
|
||||
|
||||
@@ -116,6 +49,22 @@ After your pull request was merged, I will take care of regularly creating updat
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## :newspaper: Academic & Press Recognition
|
||||
|
||||
<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.
|
||||
|
||||
[Floodcraft: Game-based Interactive Learning Environment using Minecraft for Flood Mitigation and Preparedness for K-12 Education](https://www.researchgate.net/publication/384644535_Floodcraft_Game-based_Interactive_Learning_Environment_using_Minecraft_for_Flood_Mitigation_and_Preparedness_for_K-12_Education)
|
||||
|
||||
[Hackaday: Bringing OpenStreetMap Data into Minecraft](https://hackaday.com/2024/12/30/bringing-openstreetmap-data-into-minecraft/)
|
||||
|
||||
[TomsHardware: Minecraft Tool Lets You Create Scale Replicas of Real-World Locations](https://www.tomshardware.com/video-games/pc-gaming/minecraft-tool-lets-you-create-scale-replicas-of-real-world-locations-arnis-uses-geospatial-data-from-openstreetmap-to-generate-minecraft-maps)
|
||||
|
||||
[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 (c) 2022-2025 Louis Erbkamm (louis-e)
|
||||
|
||||
@@ -131,7 +80,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
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.
|
||||
|
||||
|
||||
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"
|
||||
|
BIN
assets/git/banner.png
Normal file
|
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 |
BIN
assets/git/documentation.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/git/gui.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
assets/git/preview.jpg
Normal file
|
After Width: | Height: | Size: 790 KiB |
BIN
assets/git/recognition.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
assets/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
assets/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/icons/icon.icns
Normal file
BIN
assets/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
assets/icons/icon.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
assets/minecraft/icon.png
Normal file
|
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: 198 KiB |
BIN
gitassets/mc.gif
|
Before Width: | Height: | Size: 9.7 MiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,49 +0,0 @@
|
||||
/* Conditional stylesheet for IE. */
|
||||
|
||||
.leaflet-draw-toolbar {
|
||||
border: 3px solid #999;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar a {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar a:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions {
|
||||
left: 32px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions li {
|
||||
display: inline;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.leaflet-edit-marker-selected {
|
||||
border: 4px dashed #fe93c2;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions a {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions a:hover {
|
||||
background-color: #a5a5a5;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions-top a {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions-bottom a {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions-top.leaflet-draw-actions-bottom a {
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.leaflet-control {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
width: 21px;
|
||||
_width: 27px;
|
||||
margin: 0 auto;
|
||||
_margin-top: -3px;
|
||||
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom,
|
||||
.leaflet-control-layers {
|
||||
border: 3px solid #999;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-layers,
|
||||
.leaflet-control-scale-line {
|
||||
background: white;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
.leaflet-control-attribution {
|
||||
border-top: 1px solid #bbb;
|
||||
border-left: 1px solid #bbb;
|
||||
}
|
||||
@@ -1,757 +0,0 @@
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
background:#fff;
|
||||
font:15px/25px 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
color:#404040;
|
||||
color:rgba(0,0,0,0.75);
|
||||
outline:0;
|
||||
overflow:hidden;
|
||||
-ms-touch-action:none;
|
||||
}
|
||||
.leaflet-container *,
|
||||
.leaflet-container *:after,
|
||||
.leaflet-container *:before {
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing:border-box;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.leaflet-container h1,
|
||||
.leaflet-container h2,
|
||||
.leaflet-container h3,
|
||||
.leaflet-container h4,
|
||||
.leaflet-container h5,
|
||||
.leaflet-container h6,
|
||||
.leaflet-container p {
|
||||
font-size:15px;
|
||||
line-height:25px;
|
||||
margin:0 0 10px;
|
||||
}
|
||||
.mapbox-small,
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale,
|
||||
.leaflet-container input,
|
||||
.leaflet-container textarea,
|
||||
.leaflet-container label,
|
||||
.leaflet-container small {
|
||||
font-size:12px;
|
||||
line-height:20px;
|
||||
}
|
||||
|
||||
.leaflet-container a {
|
||||
color:#3887BE;
|
||||
font-weight:normal;
|
||||
text-decoration:none;
|
||||
}
|
||||
.leaflet-container a:hover { color:#63b6e5; }
|
||||
.leaflet-container.dark a { color:#63b6e5; }
|
||||
.leaflet-container.dark a:hover { color:#8fcaec; }
|
||||
|
||||
.leaflet-container.dark .mapbox-button,
|
||||
.leaflet-container .mapbox-button {
|
||||
background-color:#3887be;
|
||||
display:inline-block;
|
||||
height:40px;
|
||||
line-height:40px;
|
||||
text-decoration:none;
|
||||
color:#fff;
|
||||
font-size:12px;
|
||||
white-space:nowrap;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.leaflet-container.dark .mapbox-button:hover,
|
||||
.leaflet-container .mapbox-button:hover {
|
||||
color:#fff;
|
||||
background-color:#3bb2d0;
|
||||
}
|
||||
|
||||
/* Base Leaflet
|
||||
------------------------------------------------------- */
|
||||
.leaflet-map-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-pane,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-overlay-pane,
|
||||
.leaflet-shadow-pane,
|
||||
.leaflet-marker-pane,
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-overlay-pane svg,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position:absolute;
|
||||
left:0;
|
||||
top:0;
|
||||
}
|
||||
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-drag:none;
|
||||
-webkit-user-select:none;
|
||||
-moz-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.leaflet-tile {
|
||||
filter:inherit;
|
||||
visibility:hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility:inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width:0;
|
||||
height:0;
|
||||
}
|
||||
|
||||
.leaflet-tile-pane { z-index:2; }
|
||||
.leaflet-objects-pane { z-index:3; }
|
||||
.leaflet-overlay-pane { z-index:4; }
|
||||
.leaflet-shadow-pane { z-index:5; }
|
||||
.leaflet-marker-pane { z-index:6; }
|
||||
.leaflet-popup-pane { z-index:7; }
|
||||
|
||||
.leaflet-control {
|
||||
position:relative;
|
||||
z-index:7;
|
||||
pointer-events:auto;
|
||||
float:left;
|
||||
clear:both;
|
||||
}
|
||||
.leaflet-right .leaflet-control { float:right; }
|
||||
.leaflet-top .leaflet-control { margin-top:10px; }
|
||||
.leaflet-bottom .leaflet-control { margin-bottom:10px; }
|
||||
.leaflet-left .leaflet-control { margin-left:10px; }
|
||||
.leaflet-right .leaflet-control { margin-right:10px; }
|
||||
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position:absolute;
|
||||
z-index:1000;
|
||||
pointer-events:none;
|
||||
}
|
||||
.leaflet-top { top:0; }
|
||||
.leaflet-right { right:0; }
|
||||
.leaflet-bottom { bottom:0; }
|
||||
.leaflet-left { left:0; }
|
||||
|
||||
/* zoom and fade animations */
|
||||
.leaflet-fade-anim .leaflet-tile,
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity:0;
|
||||
-webkit-transition:opacity 0.2s linear;
|
||||
-moz-transition:opacity 0.2s linear;
|
||||
-o-transition:opacity 0.2s linear;
|
||||
transition:opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-tile-loaded,
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity:1;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition:-webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile,
|
||||
.leaflet-touching .leaflet-zoom-animated {
|
||||
-webkit-transition:none;
|
||||
-moz-transition:none;
|
||||
-o-transition:none;
|
||||
transition:none;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide { visibility: hidden; }
|
||||
|
||||
/* cursors */
|
||||
.map-clickable,
|
||||
.leaflet-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor:auto;
|
||||
}
|
||||
.leaflet-container {
|
||||
cursor:-webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
}
|
||||
.leaflet-dragging,
|
||||
.leaflet-dragging .map-clickable,
|
||||
.leaflet-dragging .leaflet-clickable,
|
||||
.leaflet-dragging .leaflet-container {
|
||||
cursor:move;
|
||||
cursor:-webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
}
|
||||
|
||||
.leaflet-zoom-box {
|
||||
background:#fff;
|
||||
border:2px dotted #202020;
|
||||
opacity:0.5;
|
||||
}
|
||||
|
||||
/* general toolbar styles */
|
||||
.leaflet-control-layers,
|
||||
.leaflet-bar {
|
||||
background-color:#fff;
|
||||
border:1px solid #999;
|
||||
border-color:rgba(0,0,0,0.4);
|
||||
border-radius:3px;
|
||||
box-shadow:none;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-bar a:hover {
|
||||
color:#404040;
|
||||
color:rgba(0,0,0,0.75);
|
||||
border-bottom:1px solid #ddd;
|
||||
border-bottom-color:rgba(0,0,0,0.10);
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:active {
|
||||
background-color:#f8f8f8;
|
||||
cursor:pointer;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-radius:3px 3px 0 0;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom:none;
|
||||
border-radius:0 0 3px 3px;
|
||||
}
|
||||
.leaflet-bar a:only-of-type {
|
||||
border-radius:3px;
|
||||
}
|
||||
|
||||
.leaflet-bar .leaflet-disabled {
|
||||
cursor:default;
|
||||
opacity:0.75;
|
||||
}
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
display:block;
|
||||
content:'';
|
||||
text-indent:-999em;
|
||||
}
|
||||
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display:none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display:block;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded {
|
||||
background:#fff;
|
||||
padding:6px 10px 6px 6px;
|
||||
color:#404040;
|
||||
color:rgba(0,0,0,0.75);
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top:2px;
|
||||
position:relative;
|
||||
top:1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height:0;
|
||||
border-top:1px solid #ddd;
|
||||
border-top-color:rgba(0,0,0,0.10);
|
||||
margin:5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background-color:rgba(255,255,255,0.25);
|
||||
margin:0;
|
||||
box-shadow:none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.map-info-container a:hover {
|
||||
color:inherit;
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding:0 5px;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale { margin-left:5px; }
|
||||
.leaflet-bottom .leaflet-control-scale { margin-bottom:5px; }
|
||||
|
||||
.leaflet-control-scale-line {
|
||||
background-color:rgba(255,255,255,0.5);
|
||||
border:1px solid #999;
|
||||
border-color:rgba(0,0,0,0.4);
|
||||
border-top:none;
|
||||
padding:2px 5px 1px;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top:2px solid #ddd;
|
||||
border-top-color:rgba(0,0,0,0.10);
|
||||
border-bottom:none;
|
||||
margin-top:-2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom:2px solid #777;
|
||||
}
|
||||
|
||||
/* popup */
|
||||
.leaflet-popup {
|
||||
position:absolute;
|
||||
text-align:center;
|
||||
pointer-events:none;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding:1px;
|
||||
text-align:left;
|
||||
pointer-events:all;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
padding:10px 10px 15px;
|
||||
margin:0;
|
||||
line-height:inherit;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width:20px;
|
||||
height:20px;
|
||||
margin:0 auto;
|
||||
position:relative;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width:0;
|
||||
height:0;
|
||||
margin:0;
|
||||
border-left:10px solid transparent;
|
||||
border-right:10px solid transparent;
|
||||
border-top:10px solid #fff;
|
||||
box-shadow:none;
|
||||
}
|
||||
.leaflet-popup-close-button {
|
||||
text-indent:-999em;
|
||||
position:absolute;
|
||||
top:0;right:0;
|
||||
pointer-events:all;
|
||||
}
|
||||
.leaflet-popup-close-button:hover {
|
||||
background-color:#f8f8f8;
|
||||
}
|
||||
|
||||
.leaflet-popup-scrolled {
|
||||
overflow:auto;
|
||||
border-bottom:1px solid #ddd;
|
||||
border-top:1px solid #ddd;
|
||||
}
|
||||
|
||||
/* div icon */
|
||||
.leaflet-div-icon {
|
||||
background:#fff;
|
||||
border:1px solid #999;
|
||||
border-color:rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-editing-icon {
|
||||
border-radius:3px;
|
||||
}
|
||||
|
||||
/* Leaflet + Mapbox
|
||||
------------------------------------------------------- */
|
||||
.leaflet-bar a,
|
||||
.mapbox-icon,
|
||||
.map-tooltip.closable .close,
|
||||
.leaflet-control-layers-toggle,
|
||||
.leaflet-popup-close-button,
|
||||
.mapbox-button-icon:before {
|
||||
content:'';
|
||||
display:inline-block;
|
||||
width:26px;
|
||||
height:26px;
|
||||
vertical-align:middle;
|
||||
background-repeat:no-repeat;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
display:block;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out,
|
||||
.leaflet-popup-close-button,
|
||||
.leaflet-control-layers-toggle,
|
||||
.leaflet-container.dark .map-tooltip .close,
|
||||
.map-tooltip .close,
|
||||
.mapbox-icon {
|
||||
background-image:url(./images/icons-404040.png);
|
||||
background-repeat:no-repeat;
|
||||
background-size:26px 260px;
|
||||
}
|
||||
.mapbox-button-icon:before,
|
||||
.leaflet-container.dark .leaflet-control-zoom-in,
|
||||
.leaflet-container.dark .leaflet-control-zoom-out,
|
||||
.leaflet-container.dark .leaflet-control-layers-toggle,
|
||||
.leaflet-container.dark .mapbox-icon {
|
||||
background-image:url(./images/icons-ffffff.png);
|
||||
background-size:26px 260px;
|
||||
}
|
||||
.leaflet-bar .leaflet-control-zoom-in { background-position:0 0; }
|
||||
.leaflet-bar .leaflet-control-zoom-out { background-position:0 -26px; }
|
||||
.map-tooltip .close, .leaflet-popup-close-button { background-position:0 -52px; }
|
||||
.mapbox-icon-info { background-position:0 -78px; }
|
||||
.leaflet-control-layers-toggle { background-position:0 -104px; }
|
||||
.mapbox-icon-share:before, .mapbox-icon-share { background-position:0 -130px; }
|
||||
.mapbox-icon-geocoder:before, .mapbox-icon-geocoder { background-position:0 -156px; }
|
||||
.mapbox-icon-facebook:before, .mapbox-icon-facebook { background-position:0 -182px; }
|
||||
.mapbox-icon-twitter:before, .mapbox-icon-twitter { background-position:0 -208px; }
|
||||
.mapbox-icon-pinterest:before, .mapbox-icon-pinterest { background-position:0 -234px; }
|
||||
|
||||
@media
|
||||
(-webkit-min-device-pixel-ratio:2),
|
||||
(min-resolution:192dpi) {
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out,
|
||||
.leaflet-popup-close-button,
|
||||
.leaflet-control-layers-toggle,
|
||||
.mapbox-icon {
|
||||
background-image:url(./images/icons-404040@2x.png);
|
||||
}
|
||||
.mapbox-button-icon:before,
|
||||
.leaflet-container.dark .leaflet-control-zoom-in,
|
||||
.leaflet-container.dark .leaflet-control-zoom-out,
|
||||
.leaflet-container.dark .leaflet-control-layers-toggle,
|
||||
.leaflet-container.dark .mapbox-icon {
|
||||
background-image:url(./images/icons-ffffff@2x.png);
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper,
|
||||
.map-legends,
|
||||
.map-tooltip {
|
||||
background:#fff;
|
||||
border-radius:3px;
|
||||
box-shadow:0 1px 2px rgba(0,0,0,0.10);
|
||||
}
|
||||
.map-legends,
|
||||
.map-tooltip {
|
||||
max-width:300px;
|
||||
}
|
||||
.map-legends .map-legend {
|
||||
padding:10px;
|
||||
}
|
||||
.map-tooltip {
|
||||
z-index:999999;
|
||||
padding:10px;
|
||||
min-width:180px;
|
||||
max-height:400px;
|
||||
overflow:auto;
|
||||
opacity:1;
|
||||
-webkit-transition:opacity 150ms;
|
||||
-moz-transition:opacity 150ms;
|
||||
-o-transition:opacity 150ms;
|
||||
transition:opacity 150ms;
|
||||
}
|
||||
|
||||
.map-tooltip .close {
|
||||
text-indent:-999em;
|
||||
overflow:hidden;
|
||||
display:none;
|
||||
}
|
||||
.map-tooltip.closable .close {
|
||||
position:absolute;
|
||||
top:0;right:0;
|
||||
border-radius:3px;
|
||||
}
|
||||
.map-tooltip.closable .close:active {
|
||||
background-color:#f8f8f8;
|
||||
}
|
||||
|
||||
.leaflet-control-interaction {
|
||||
position:absolute;
|
||||
top:10px;
|
||||
right:10px;
|
||||
width:300px;
|
||||
}
|
||||
.leaflet-popup-content .marker-title {
|
||||
font-weight:bold;
|
||||
}
|
||||
.leaflet-control .mapbox-button {
|
||||
background-color:#fff;
|
||||
border:1px solid #ddd;
|
||||
border-color:rgba(0,0,0,0.10);
|
||||
padding:5px 10px;
|
||||
border-radius:3px;
|
||||
}
|
||||
|
||||
/* Share modal
|
||||
------------------------------------------------------- */
|
||||
.mapbox-modal > div {
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
height:100%;
|
||||
z-index:-1;
|
||||
overflow-y:auto;
|
||||
}
|
||||
.mapbox-modal.active > div {
|
||||
z-index:99999;
|
||||
transition:all .2s, z-index 0 0;
|
||||
}
|
||||
|
||||
.mapbox-modal .mapbox-modal-mask {
|
||||
background:rgba(0,0,0,0.5);
|
||||
opacity:0;
|
||||
}
|
||||
.mapbox-modal.active .mapbox-modal-mask { opacity:1; }
|
||||
|
||||
.mapbox-modal .mapbox-modal-content {
|
||||
-webkit-transform:translateY(-100%);
|
||||
-moz-transform:translateY(-100%);
|
||||
-ms-transform:translateY(-100%);
|
||||
transform:translateY(-100%);
|
||||
}
|
||||
.mapbox-modal.active .mapbox-modal-content {
|
||||
-webkit-transform:translateY(0);
|
||||
-moz-transform:translateY(0);
|
||||
-ms-transform:translateY(0);
|
||||
transform:translateY(0);
|
||||
}
|
||||
|
||||
.mapbox-modal-body {
|
||||
position:relative;
|
||||
background:#fff;
|
||||
padding:20px;
|
||||
z-index:1000;
|
||||
width:50%;
|
||||
margin:20px 0 20px 25%;
|
||||
}
|
||||
.mapbox-share-buttons {
|
||||
margin:0 0 20px;
|
||||
}
|
||||
.mapbox-share-buttons a {
|
||||
width:33.3333%;
|
||||
border-left:1px solid #fff;
|
||||
text-align:center;
|
||||
border-radius:0;
|
||||
}
|
||||
.mapbox-share-buttons a:last-child { border-radius:0 3px 3px 0; }
|
||||
.mapbox-share-buttons a:first-child { border:none; border-radius:3px 0 0 3px; }
|
||||
|
||||
.mapbox-modal input {
|
||||
width:100%;
|
||||
height:40px;
|
||||
padding:10px;
|
||||
border:1px solid #ddd;
|
||||
border-color:rgba(0,0,0,0.10);
|
||||
color:rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* Info Control
|
||||
------------------------------------------------------- */
|
||||
.leaflet-control.mapbox-control-info {
|
||||
margin:5px 30px 10px 10px;
|
||||
min-height:26px;
|
||||
}
|
||||
.leaflet-control.mapbox-control-info-right {
|
||||
margin:5px 10px 10px 30px;
|
||||
}
|
||||
|
||||
.mapbox-info-toggle {
|
||||
background-color:#fff;
|
||||
background-color:rgba(255,255,255,0.25);
|
||||
border-radius:50%;
|
||||
position:absolute;
|
||||
bottom:0;left:0;
|
||||
z-index:1;
|
||||
}
|
||||
.mapbox-control-info-right .mapbox-info-toggle { left:auto; right:0; }
|
||||
.mapbox-control-info.active .mapbox-info-toggle { background-color:#fff; }
|
||||
.mapbox-info-toggle:hover { background-color:rgba(255,255,255,0.5); }
|
||||
|
||||
.map-info-container {
|
||||
background:#fff;
|
||||
background:rgba(255,255,255,0.75);
|
||||
padding:3px 5px 3px 15px;
|
||||
display:none;
|
||||
position:relative;
|
||||
bottom:0;
|
||||
left:13px;
|
||||
border-radius:3px;
|
||||
}
|
||||
.mapbox-control-info.active .map-info-container { display:inline-block; }
|
||||
.mapbox-control-info-right .map-info-container {
|
||||
left:auto;
|
||||
right:13px;
|
||||
padding:3px 15px 3px 5px;
|
||||
}
|
||||
|
||||
/* Geocoder
|
||||
------------------------------------------------------- */
|
||||
.leaflet-control-mapbox-geocoder {
|
||||
position:relative;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder.searching {
|
||||
opacity:0.75;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder .leaflet-control-mapbox-geocoder-wrap {
|
||||
background:#fff;
|
||||
position:absolute;
|
||||
border:1px solid #999;
|
||||
border-color:rgba(0,0,0,0.4);
|
||||
border-bottom-width:0;
|
||||
overflow:hidden;
|
||||
left:26px;
|
||||
height:27px;
|
||||
width:0;
|
||||
top:-1px;
|
||||
border-radius:0 3px 3px 0;
|
||||
opacity:0;
|
||||
-webkit-transition:opacity 100ms;
|
||||
-moz-transition:opacity 100ms;
|
||||
-o-transition:opacity 100ms;
|
||||
transition:opacity 100ms;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder.active .leaflet-control-mapbox-geocoder-wrap {
|
||||
width:180px;
|
||||
opacity:1;
|
||||
}
|
||||
.leaflet-bar .leaflet-control-mapbox-geocoder-toggle,
|
||||
.leaflet-bar .leaflet-control-mapbox-geocoder-toggle:hover {
|
||||
border-bottom:none;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder-toggle {
|
||||
border-radius:3px;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder.active,
|
||||
.leaflet-control-mapbox-geocoder.active .leaflet-control-mapbox-geocoder-toggle {
|
||||
border-top-right-radius:0;
|
||||
border-bottom-right-radius:0;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder .leaflet-control-mapbox-geocoder-form input {
|
||||
background:transparent;
|
||||
border:0;
|
||||
width:180px;
|
||||
padding:0 0 0 10px;
|
||||
height:26px;
|
||||
outline:none;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder-results {
|
||||
width:180px;
|
||||
position:absolute;
|
||||
left:26px;
|
||||
top:25px;
|
||||
border-radius:0 0 3px 3px;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder.active .leaflet-control-mapbox-geocoder-results {
|
||||
background:#fff;
|
||||
border:1px solid #999;
|
||||
border-color:rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.leaflet-control-mapbox-geocoder-results a,
|
||||
.leaflet-control-mapbox-geocoder-results span {
|
||||
padding:0 10px;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
display:block;
|
||||
width:100%;
|
||||
font-size:12px;
|
||||
line-height:26px;
|
||||
text-align:left;
|
||||
overflow:hidden;
|
||||
}
|
||||
.leaflet-control-mapbox-geocoder-results a:first-child {
|
||||
border-top:1px solid #999;
|
||||
border-top-color:rgba(0,0,0,0.4);
|
||||
border-radius:0;
|
||||
}
|
||||
.leaflet-container.dark .leaflet-control .leaflet-control-mapbox-geocoder-results a:hover,
|
||||
.leaflet-control-mapbox-geocoder-results a:hover {
|
||||
background:#f8f8f8;
|
||||
opacity:1;
|
||||
}
|
||||
|
||||
/* Dark Theme
|
||||
------------------------------------------------------- */
|
||||
.leaflet-container.dark .leaflet-bar {
|
||||
background-color:#404040;
|
||||
border-color:#202020;
|
||||
border-color:rgba(0,0,0,0.75);
|
||||
}
|
||||
.leaflet-container.dark .leaflet-bar a {
|
||||
color:#404040;
|
||||
border-color:rgba(0,0,0,0.5);
|
||||
}
|
||||
.leaflet-container.dark .leaflet-bar a:active,
|
||||
.leaflet-container.dark .leaflet-bar a:hover {
|
||||
background-color:#505050;
|
||||
}
|
||||
|
||||
.leaflet-container.dark .mapbox-info-toggle,
|
||||
.leaflet-container.dark .map-info-container,
|
||||
.leaflet-container.dark .leaflet-control-attribution {
|
||||
background-color:rgba(0,0,0,0.25);
|
||||
color:#f8f8f8;
|
||||
}
|
||||
.leaflet-container.dark .leaflet-bar a.leaflet-disabled,
|
||||
.leaflet-container.dark .leaflet-control .mapbox-button.disabled {
|
||||
background-color:#252525;
|
||||
color:#404040;
|
||||
}
|
||||
.leaflet-container.dark .leaflet-control-mapbox-geocoder > div {
|
||||
border-color:#202020;
|
||||
border-color:rgba(0,0,0,0.75);
|
||||
}
|
||||
.leaflet-container.dark .leaflet-control .leaflet-control-mapbox-geocoder-results a {
|
||||
border-color:#ddd #202020;
|
||||
border-color:rgba(0,0,0,0.10) rgba(0,0,0,0.75);
|
||||
}
|
||||
.leaflet-container.dark .leaflet-control .leaflet-control-mapbox-geocoder-results span {
|
||||
border-color:#202020;
|
||||
border-color:rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
/* Larger Screens
|
||||
------------------------------------------------------- */
|
||||
@media only screen and (max-width:800px) {
|
||||
.mapbox-modal-body {
|
||||
width:83.3333%;
|
||||
margin-left:8.3333%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smaller Screens
|
||||
------------------------------------------------------- */
|
||||
@media only screen and (max-width:640px) {
|
||||
.mapbox-modal-body {
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin:0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Browser Fixes
|
||||
------------------------------------------------------- */
|
||||
/* Map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container img { max-width:none!important; }
|
||||
/* Stupid Android 2 doesn't understand "max-width: none" properly */
|
||||
.leaflet-container img.leaflet-image-layer { max-width:15000px!important; }
|
||||
/* Android chrome makes tiles disappear without this */
|
||||
.leaflet-tile-container img { -webkit-backface-visibility:hidden; }
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg { -moz-user-select:none; }
|
||||
/* Older IEs don't support the translateY property for display animation */
|
||||
.leaflet-oldie .mapbox-modal .mapbox-modal-content { display:none; }
|
||||
.leaflet-oldie .mapbox-modal.active .mapbox-modal-content { display:block; }
|
||||
.map-tooltip { width:280px\8; /* < IE9 */ }
|
||||
@@ -1,379 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #ececec;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 1vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.arnis:hover {
|
||||
filter: drop-shadow(0 0 2em #b3b3b3);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #575757;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.map-box,
|
||||
.controls-box {
|
||||
width: 45%;
|
||||
background: #575757;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.controls-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.controls-box .progress-section {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #d6d6d6;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
margin-top: 10px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #656565;
|
||||
}
|
||||
|
||||
#selected-directory {
|
||||
font-size: 1em;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.progress-section h2 {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: #4caf50;
|
||||
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 {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #ececec;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tooltip button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
width: 90%;
|
||||
background-color: #808080;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: -100%;
|
||||
left: 5%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #808080 transparent;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.controls-box button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Customization Settings */
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #797979;
|
||||
padding: 20px;
|
||||
border: 1px solid #797979;
|
||||
border-radius: 10px;
|
||||
width: 400px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
color: #e9e9e9;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#winter-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
.winter-toggle-container, .scale-slider-container {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.scale-slider-container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#scale-value-slider {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
#slider-value {
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bbox-input-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bbox-input-container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#bbox-coords {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #fecc44;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#bbox-coords:focus {
|
||||
outline: none;
|
||||
border-color: #fecc44;
|
||||
box-shadow: 0 0 5px #fecc44;
|
||||
}
|
||||
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.start-button:hover {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
width: 40px !important;
|
||||
height: 38px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.settings-button .gear-icon::before {
|
||||
content: "⚙️";
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Logo Animation */
|
||||
#arnis-logo {
|
||||
width: 35%;
|
||||
height: auto;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
animation: zoomInLogo 1s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Keyframe Animation */
|
||||
@keyframes zoomInLogo {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,148 +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>
|
||||
</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%;" data-localize="choose_world">
|
||||
Choose World
|
||||
<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>
|
||||
<span class="tooltiptext" style="font-size: 0.8em; line-height: 1.2;">
|
||||
Please select a Minecraft world that can be overwritten, as the generation process will replace existing structures in the chosen world!
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<!-- Winter Mode Toggle Button -->
|
||||
<div class="winter-toggle-container">
|
||||
<label for="winter-toggle" data-localize="winter_mode">Winter Mode:</label>
|
||||
<input type="checkbox" id="winter-toggle" name="winter-toggle">
|
||||
</div>
|
||||
|
||||
<!-- World Scale Slider -->
|
||||
<div class="scale-slider-container">
|
||||
<label for="scale-value-slider" data-localize="world_scale">World Scale:</label>
|
||||
<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>
|
||||
|
||||
<!-- Bounding Box Input -->
|
||||
<div class="bbox-input-container">
|
||||
<label for="bbox-coords" data-localize="custom_bounding_box">Custom Bounding Box:</label>
|
||||
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" style="width: 280px;" autocomplete="one-time-code" placeholder="Format: lat,lng,lat,lng">
|
||||
</div>
|
||||
|
||||
<!-- Floodfill Timeout Input -->
|
||||
<div class="timeout-input-container">
|
||||
<label for="floodfill-timeout" data-localize="floodfill_timeout">Floodfill Timeout (sec):</label>
|
||||
<input type="number" id="floodfill-timeout" name="floodfill-timeout" min="0" step="1" value="20" style="width: 100px;" placeholder="Seconds">
|
||||
</div><br>
|
||||
|
||||
<!-- Ground Level Input -->
|
||||
<div class="ground-level-input-container">
|
||||
<label for="ground-level" data-localize="ground_level">Ground Level:</label>
|
||||
<input type="number" id="ground-level" name="ground-level" min="-64" max="290" value="-62" style="width: 100px;" placeholder="Ground Level">
|
||||
</div>
|
||||
|
||||
<!-- License and Credits Button -->
|
||||
<button type="button" id="license-button" class="license-button" onclick="openLicense()" data-localize="license_and_credits">License and Credits</button>
|
||||
</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">
|
||||
© <span id="current-year"></span> Arnis v<span id="version-placeholder">x.x.x</span> by louis-e
|
||||
</a>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,497 +0,0 @@
|
||||
import { licenseText } from './license.js';
|
||||
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
|
||||
// Initialize elements and start the demo progress
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
registerMessageEvent();
|
||||
window.selectWorld = selectWorld;
|
||||
window.startGeneration = startGeneration;
|
||||
setupProgressListener();
|
||||
initSettings();
|
||||
initWorldPicker();
|
||||
handleBboxInput();
|
||||
const language = detectLanguage();
|
||||
const localization = await loadLocalization(language);
|
||||
await applyLocalization(localization);
|
||||
initFooter();
|
||||
await checkForUpdates();
|
||||
});
|
||||
|
||||
async function loadLocalization(language) {
|
||||
const response = await fetch(`./locales/${language}.json`);
|
||||
const localization = await response.json();
|
||||
return localization;
|
||||
}
|
||||
|
||||
async function applyLocalization(localization) {
|
||||
const selectLocationElement = document.querySelector("h2[data-localize='select_location']");
|
||||
if (selectLocationElement) {
|
||||
selectLocationElement.textContent = localization.select_location;
|
||||
}
|
||||
|
||||
const bboxTextElement = document.getElementById("bbox-text");
|
||||
if (bboxTextElement) {
|
||||
bboxTextElement.textContent = localization.zoom_in_and_choose;
|
||||
}
|
||||
|
||||
const selectWorldElement = document.querySelector("h2[data-localize='select_world']");
|
||||
if (selectWorldElement) {
|
||||
selectWorldElement.textContent = localization.select_world;
|
||||
}
|
||||
|
||||
const chooseWorldButton = document.querySelector("button[data-localize='choose_world']");
|
||||
if (chooseWorldButton) {
|
||||
chooseWorldButton.firstChild.textContent = localization.choose_world;
|
||||
}
|
||||
|
||||
const selectedWorldElement = document.getElementById("selected-world");
|
||||
if (selectedWorldElement) {
|
||||
selectedWorldElement.textContent = localization.no_world_selected;
|
||||
}
|
||||
|
||||
const startButtonElement = document.getElementById("start-button");
|
||||
if (startButtonElement) {
|
||||
startButtonElement.textContent = localization.start_generation;
|
||||
}
|
||||
|
||||
const progressElement = document.querySelector("h2[data-localize='progress']");
|
||||
if (progressElement) {
|
||||
progressElement.textContent = localization.progress;
|
||||
}
|
||||
|
||||
const chooseWorldModalTitle = document.querySelector("h2[data-localize='choose_world_modal_title']");
|
||||
if (chooseWorldModalTitle) {
|
||||
chooseWorldModalTitle.textContent = localization.choose_world_modal_title;
|
||||
}
|
||||
|
||||
const selectExistingWorldButton = document.querySelector("button[data-localize='select_existing_world']");
|
||||
if (selectExistingWorldButton) {
|
||||
selectExistingWorldButton.textContent = localization.select_existing_world;
|
||||
}
|
||||
|
||||
const generateNewWorldButton = document.querySelector("button[data-localize='generate_new_world']");
|
||||
if (generateNewWorldButton) {
|
||||
generateNewWorldButton.textContent = localization.generate_new_world;
|
||||
}
|
||||
|
||||
const customizationSettingsTitle = document.querySelector("h2[data-localize='customization_settings']");
|
||||
if (customizationSettingsTitle) {
|
||||
customizationSettingsTitle.textContent = localization.customization_settings;
|
||||
}
|
||||
|
||||
const winterModeLabel = document.querySelector("label[data-localize='winter_mode']");
|
||||
if (winterModeLabel) {
|
||||
winterModeLabel.textContent = localization.winter_mode;
|
||||
}
|
||||
|
||||
const worldScaleLabel = document.querySelector("label[data-localize='world_scale']");
|
||||
if (worldScaleLabel) {
|
||||
worldScaleLabel.textContent = localization.world_scale;
|
||||
}
|
||||
|
||||
const customBoundingBoxLabel = document.querySelector("label[data-localize='custom_bounding_box']");
|
||||
if (customBoundingBoxLabel) {
|
||||
customBoundingBoxLabel.textContent = localization.custom_bounding_box;
|
||||
}
|
||||
|
||||
const floodfillTimeoutLabel = document.querySelector("label[data-localize='floodfill_timeout']");
|
||||
if (floodfillTimeoutLabel) {
|
||||
floodfillTimeoutLabel.textContent = localization.floodfill_timeout;
|
||||
}
|
||||
|
||||
const groundLevelLabel = document.querySelector("label[data-localize='ground_level']");
|
||||
if (groundLevelLabel) {
|
||||
groundLevelLabel.textContent = localization.ground_level;
|
||||
}
|
||||
|
||||
const footerLinkElement = document.querySelector(".footer-link");
|
||||
if (footerLinkElement) {
|
||||
footerLinkElement.innerHTML = localization.footer_text.replace("{year}", '<span id="current-year"></span>').replace("{version}", '<span id="version-placeholder"></span>');
|
||||
}
|
||||
|
||||
// Update error messages
|
||||
window.localization = localization;
|
||||
}
|
||||
|
||||
function detectLanguage() {
|
||||
const lang = navigator.language || navigator.userLanguage;
|
||||
const langCode = lang.split('-')[0];
|
||||
switch (langCode) {
|
||||
case 'es':
|
||||
return 'es';
|
||||
case 'ru':
|
||||
return 'ru';
|
||||
case 'de':
|
||||
return 'de';
|
||||
case 'zh':
|
||||
return 'zh';
|
||||
case 'uk':
|
||||
return 'ua';
|
||||
case 'pl':
|
||||
return 'pl';
|
||||
case 'ko':
|
||||
return 'ko';
|
||||
case 'sv':
|
||||
return 'sv';
|
||||
default:
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to initialize the footer with the current year and version
|
||||
async function initFooter() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentYearElement = document.getElementById("current-year");
|
||||
if (currentYearElement) {
|
||||
currentYearElement.textContent = currentYear;
|
||||
}
|
||||
|
||||
try {
|
||||
const version = await invoke('gui_get_version');
|
||||
const versionPlaceholder = document.getElementById("version-placeholder");
|
||||
if (versionPlaceholder) {
|
||||
versionPlaceholder.textContent = version;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check for updates and display a notification if available
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
const isUpdateAvailable = await invoke('gui_check_for_updates');
|
||||
if (isUpdateAvailable) {
|
||||
const footer = document.querySelector(".footer");
|
||||
const updateMessage = document.createElement("a");
|
||||
updateMessage.href = "https://github.com/louis-e/arnis/releases";
|
||||
updateMessage.target = "_blank";
|
||||
updateMessage.style.color = "#fecc44";
|
||||
updateMessage.style.marginTop = "-5px";
|
||||
updateMessage.style.fontSize = "0.95em";
|
||||
updateMessage.style.display = "block";
|
||||
updateMessage.style.textDecoration = "none";
|
||||
|
||||
updateMessage.textContent = window.localization.new_version_available;
|
||||
footer.style.marginTop = "15px";
|
||||
footer.appendChild(updateMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check for updates: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to register the event listener for bbox updates from iframe
|
||||
function registerMessageEvent() {
|
||||
window.addEventListener('message', function (event) {
|
||||
const bboxText = event.data.bboxText;
|
||||
|
||||
if (bboxText) {
|
||||
console.log("Updated BBOX Coordinates:", bboxText);
|
||||
displayBboxInfoText(bboxText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to set up the progress bar listener
|
||||
function setupProgressListener() {
|
||||
const progressBar = document.getElementById("progress-bar");
|
||||
const progressMessage = document.getElementById("progress-message");
|
||||
const progressDetail = document.getElementById("progress-detail");
|
||||
|
||||
window.__TAURI__.event.listen("progress-update", (event) => {
|
||||
const { progress, message } = event.payload;
|
||||
|
||||
if (progress != -1) {
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressDetail.textContent = `${Math.round(progress)}%`;
|
||||
}
|
||||
|
||||
if (message != "") {
|
||||
progressMessage.textContent = message;
|
||||
|
||||
if (message.startsWith("Error!")) {
|
||||
progressMessage.style.color = "#fa7878";
|
||||
generationButtonEnabled = true;
|
||||
} else if (message.startsWith("Done!")) {
|
||||
progressMessage.style.color = "#7bd864";
|
||||
generationButtonEnabled = true;
|
||||
} else {
|
||||
progressMessage.style.color = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSettings() {
|
||||
// Settings
|
||||
const settingsModal = document.getElementById("settings-modal");
|
||||
const slider = document.getElementById("scale-value-slider");
|
||||
const sliderValue = document.getElementById("slider-value");
|
||||
|
||||
// Open settings modal
|
||||
function openSettings() {
|
||||
settingsModal.style.display = "flex";
|
||||
settingsModal.style.justifyContent = "center";
|
||||
settingsModal.style.alignItems = "center";
|
||||
}
|
||||
|
||||
// Close settings modal
|
||||
function closeSettings() {
|
||||
settingsModal.style.display = "none";
|
||||
}
|
||||
|
||||
window.openSettings = openSettings;
|
||||
window.closeSettings = closeSettings;
|
||||
|
||||
// Update slider value display
|
||||
slider.addEventListener("input", () => {
|
||||
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
|
||||
});
|
||||
|
||||
|
||||
/// License and Credits
|
||||
function openLicense() {
|
||||
const licenseModal = document.getElementById("license-modal");
|
||||
const licenseContent = document.getElementById("license-content");
|
||||
|
||||
// Render the license text as HTML
|
||||
licenseContent.innerHTML = licenseText;
|
||||
|
||||
// Show the modal
|
||||
licenseModal.style.display = "flex";
|
||||
licenseModal.style.justifyContent = "center";
|
||||
licenseModal.style.alignItems = "center";
|
||||
}
|
||||
|
||||
function closeLicense() {
|
||||
const licenseModal = document.getElementById("license-modal");
|
||||
licenseModal.style.display = "none";
|
||||
}
|
||||
|
||||
window.openLicense = openLicense;
|
||||
window.closeLicense = closeLicense;
|
||||
}
|
||||
|
||||
function initWorldPicker() {
|
||||
// World Picker
|
||||
const worldPickerModal = document.getElementById("world-modal");
|
||||
|
||||
// Open world picker modal
|
||||
function openWorldPicker() {
|
||||
worldPickerModal.style.display = "flex";
|
||||
worldPickerModal.style.justifyContent = "center";
|
||||
worldPickerModal.style.alignItems = "center";
|
||||
}
|
||||
|
||||
// Close world picker modal
|
||||
function closeWorldPicker() {
|
||||
worldPickerModal.style.display = "none";
|
||||
}
|
||||
|
||||
window.openWorldPicker = openWorldPicker;
|
||||
window.closeWorldPicker = closeWorldPicker;
|
||||
}
|
||||
|
||||
// Function to validate and handle bbox input
|
||||
function handleBboxInput() {
|
||||
const inputBox = document.getElementById("bbox-coords");
|
||||
const bboxInfo = document.getElementById("bbox-info");
|
||||
|
||||
inputBox.addEventListener("input", function () {
|
||||
const input = inputBox.value.trim();
|
||||
|
||||
if (input === "") {
|
||||
bboxInfo.textContent = "";
|
||||
bboxInfo.style.color = "";
|
||||
selectedBBox = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular expression to validate bbox input (supports both comma and space-separated formats)
|
||||
const bboxPattern = /^(-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)$/;
|
||||
|
||||
if (bboxPattern.test(input)) {
|
||||
const matches = input.match(bboxPattern);
|
||||
|
||||
// Extract coordinates (Lat / Lng order expected)
|
||||
const lat1 = parseFloat(matches[1]);
|
||||
const lng1 = parseFloat(matches[3]);
|
||||
const lat2 = parseFloat(matches[5]);
|
||||
const lng2 = parseFloat(matches[7]);
|
||||
|
||||
// Validate latitude and longitude ranges in the expected Lat / Lng order
|
||||
if (
|
||||
lat1 >= -90 && lat1 <= 90 &&
|
||||
lng1 >= -180 && lng1 <= 180 &&
|
||||
lat2 >= -90 && lat2 <= 90 &&
|
||||
lng2 >= -180 && lng2 <= 180
|
||||
) {
|
||||
// Input is valid; trigger the event
|
||||
const bboxText = `${lat1} ${lng1} ${lat2} ${lng2}`;
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { bboxText } }));
|
||||
|
||||
// Show custom bbox on the map
|
||||
let map_container = document.querySelector('.map-container');
|
||||
map_container.setAttribute('src', `maps.html#${lat1},${lng1},${lat2},${lng2}`);
|
||||
map_container.contentWindow.location.reload();
|
||||
|
||||
// Update the info text
|
||||
bboxInfo.textContent = window.localization.custom_selection_confirmed;
|
||||
bboxInfo.style.color = "#7bd864";
|
||||
} else {
|
||||
// Valid numbers but invalid order or range
|
||||
bboxInfo.textContent = window.localization.error_coordinates_out_of_range;
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
selectedBBox = "";
|
||||
}
|
||||
} else {
|
||||
// Input doesn't match the required format
|
||||
bboxInfo.textContent = window.localization.invalid_format;
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
selectedBBox = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to calculate the bounding box "size" in square meters based on latitude and longitude
|
||||
function calculateBBoxSize(lng1, lat1, lng2, lat2) {
|
||||
// Approximate distance calculation using Haversine formula or geodesic formula
|
||||
const toRad = (angle) => (angle * Math.PI) / 180;
|
||||
const R = 6371000; // Earth radius in meters
|
||||
|
||||
const latDistance = toRad(lat2 - lat1);
|
||||
const lngDistance = toRad(lng2 - lng1);
|
||||
|
||||
const a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||
Math.sin(lngDistance / 2) * Math.sin(lngDistance / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
// Width and height of the box
|
||||
const height = R * latDistance;
|
||||
const width = R * lngDistance;
|
||||
|
||||
return Math.abs(width * height);
|
||||
}
|
||||
|
||||
// Function to normalize longitude to the range [-180, 180]
|
||||
function normalizeLongitude(lon) {
|
||||
return ((lon + 180) % 360 + 360) % 360 - 180;
|
||||
}
|
||||
|
||||
const threshold1 = 30000000.00;
|
||||
const threshold2 = 45000000.00;
|
||||
let selectedBBox = "";
|
||||
|
||||
// Function to handle incoming bbox data
|
||||
function displayBboxInfoText(bboxText) {
|
||||
let [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
|
||||
|
||||
// Normalize longitudes
|
||||
lat1 = parseFloat(normalizeLongitude(lat1).toFixed(6));
|
||||
lat2 = parseFloat(normalizeLongitude(lat2).toFixed(6));
|
||||
selectedBBox = `${lng1} ${lat1} ${lng2} ${lat2}`;
|
||||
|
||||
const bboxInfo = document.getElementById("bbox-info");
|
||||
|
||||
// Reset the info text if the bbox is 0,0,0,0
|
||||
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
|
||||
bboxInfo.textContent = "";
|
||||
selectedBBox = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the size of the selected bbox
|
||||
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
||||
|
||||
if (selectedSize > threshold2) {
|
||||
bboxInfo.textContent = window.localization.area_too_large;
|
||||
bboxInfo.style.color = "#fa7878";
|
||||
} else if (selectedSize > threshold1) {
|
||||
bboxInfo.textContent = window.localization.area_extensive;
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
} else {
|
||||
bboxInfo.textContent = window.localization.selection_confirmed;
|
||||
bboxInfo.style.color = "#7bd864";
|
||||
}
|
||||
}
|
||||
|
||||
let worldPath = "";
|
||||
async function selectWorld(generate_new_world) {
|
||||
try {
|
||||
const worldName = await invoke('gui_select_world', { generateNew: generate_new_world } );
|
||||
if (worldName) {
|
||||
worldPath = worldName;
|
||||
const lastSegment = worldName.split(/[\\/]/).pop();
|
||||
document.getElementById('selected-world').textContent = lastSegment;
|
||||
document.getElementById('selected-world').style.color = "#fecc44";
|
||||
}
|
||||
} catch (error) {
|
||||
handleWorldSelectionError(error);
|
||||
}
|
||||
|
||||
closeWorldPicker();
|
||||
}
|
||||
|
||||
function handleWorldSelectionError(errorCode) {
|
||||
const errorMessages = {
|
||||
1: window.localization.minecraft_directory_not_found,
|
||||
2: window.localization.world_in_use,
|
||||
3: window.localization.failed_to_create_world,
|
||||
4: window.localization.no_world_selected_error
|
||||
};
|
||||
|
||||
const errorMessage = errorMessages[errorCode] || "Unknown error";
|
||||
document.getElementById('selected-world').textContent = errorMessage;
|
||||
document.getElementById('selected-world').style.color = "#fa7878";
|
||||
worldPath = "";
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
let generationButtonEnabled = true;
|
||||
async function startGeneration() {
|
||||
try {
|
||||
if (generationButtonEnabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
|
||||
document.getElementById('bbox-info').textContent = window.localization.select_location_first;
|
||||
document.getElementById('bbox-info').style.color = "#fa7878";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!worldPath || worldPath === "") {
|
||||
document.getElementById('selected-world').textContent = window.localization.select_minecraft_world_first;
|
||||
document.getElementById('selected-world').style.color = "#fa7878";
|
||||
return;
|
||||
}
|
||||
|
||||
var winter_mode = document.getElementById("winter-toggle").checked;
|
||||
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
||||
var floodfill_timeout = parseInt(document.getElementById("floodfill-timeout").value, 10);
|
||||
var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
||||
|
||||
// Validate floodfill_timeout and ground_level
|
||||
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
|
||||
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
|
||||
|
||||
// Pass the bounding box and selected world to the Rust backend
|
||||
await invoke("gui_start_generation", {
|
||||
bboxText: selectedBBox,
|
||||
selectedWorld: worldPath,
|
||||
worldScale: scale,
|
||||
groundLevel: ground_level,
|
||||
winterMode: winter_mode,
|
||||
floodfillTimeout: floodfill_timeout,
|
||||
});
|
||||
|
||||
console.log("Generation process started.");
|
||||
generationButtonEnabled = false;
|
||||
} catch (error) {
|
||||
console.error("Error starting generation:", error);
|
||||
generationButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
2766
gui-src/js/maps/leaflet.draw-src.js
vendored
@@ -1,158 +0,0 @@
|
||||
(function( definition ) { // execute immeidately
|
||||
|
||||
if ( typeof module !== 'undefined' &&
|
||||
typeof module.exports !== 'undefined' ) {
|
||||
module.exports = definition();
|
||||
}
|
||||
else if ( typeof window === "object" ) {
|
||||
// example run syntax: BBOX_T( { 'url' : '/js/maps/testdata.js' } );
|
||||
window.BBOX_T = definition();
|
||||
}
|
||||
|
||||
|
||||
})( function() {
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
**
|
||||
** constructor
|
||||
**
|
||||
*/
|
||||
var TestRunner = function( options ) {
|
||||
options || ( options = {} );
|
||||
|
||||
if( !this || !(this instanceof TestRunner )){
|
||||
return new TestRunner( options );
|
||||
}
|
||||
|
||||
this.test_url = options.url || "";
|
||||
|
||||
this.global_setup(); // execute immediately
|
||||
};
|
||||
|
||||
/*
|
||||
**
|
||||
** functions
|
||||
**
|
||||
*/
|
||||
TestRunner.prototype.global_setup = function() {
|
||||
|
||||
var self = this; // hold ref to instance
|
||||
|
||||
$.ajax({
|
||||
'url' : this.test_url ,
|
||||
'dataType' : 'json'
|
||||
})
|
||||
.done( function( json_data ) {
|
||||
self.run_this_mother.call( self, json_data );
|
||||
})
|
||||
.fail( function( error ) {
|
||||
console.log( "The test data didn't load: ", error );
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
TestRunner.prototype.single_setup = function() {
|
||||
this.get_layer_count();
|
||||
};
|
||||
|
||||
TestRunner.prototype.tear_down = function() {
|
||||
if( this._draw_delete_handler ){
|
||||
this._draw_delete_handler.off('draw:deleted');
|
||||
}
|
||||
};
|
||||
|
||||
TestRunner.prototype.run_this_mother = function( json_data ) {
|
||||
for( var key in json_data ){
|
||||
console.log( "[ RUNNING ]: test " + json_data[key]['type'] + "->" + "simple=" + json_data[key]['simple'] );
|
||||
var data = json_data[key]['data'];
|
||||
if( json_data[key]['type'] === 'geojson' ) {
|
||||
data = JSON.stringify( data );
|
||||
}
|
||||
|
||||
/*
|
||||
** run different tests
|
||||
** the context here is jQuery, so change
|
||||
** to reference the instance
|
||||
*/
|
||||
this.single_setup();
|
||||
|
||||
this.test_parsing( data, json_data );
|
||||
this.test_add2map( json_data );
|
||||
this.test_deletable( json_data );
|
||||
|
||||
this.tear_down();
|
||||
}
|
||||
};
|
||||
|
||||
TestRunner.prototype.test_deletable = function(identifier){ // TODO: this needs work
|
||||
var toolbar = null;
|
||||
// get the right toolbar, depending on attributes
|
||||
for( var key in drawControl._toolbars ){
|
||||
var tbar = drawControl._toolbars[key];
|
||||
if ( !(tbar instanceof L.EditToolbar ) ){
|
||||
continue;
|
||||
}
|
||||
|
||||
toolbar = tbar; // set the right one;
|
||||
}
|
||||
|
||||
// create delete handler that makes sure things are deleted
|
||||
this._draw_delete_handler = map.on('draw:deleted', function (e) {
|
||||
try {
|
||||
e.layers.eachLayer(function (l) {
|
||||
drawnItems.removeLayer(l);
|
||||
});
|
||||
console.warn( "[ PASSED ]: test_deletable" );
|
||||
}
|
||||
catch ( err ) {
|
||||
console.error( "[ DELETE TEST FAIL ]: ", err.message, identifier );
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// loop through this toolbars featureGroup, delete layers
|
||||
if ( !toolbar._activeMode ) {
|
||||
toolbar._modes['remove'].button.click(); // enable deletable
|
||||
}
|
||||
for( var indx in toolbar.options['featureGroup']._layers ) {
|
||||
try {
|
||||
var lyr = toolbar.options['featureGroup']._layers[indx];
|
||||
lyr.fire( 'click' ); // triggers delete
|
||||
}
|
||||
catch ( err ){
|
||||
console.error( "[ DELETE TEST FAIL ]: ", err.message, identifier );
|
||||
}
|
||||
}
|
||||
// WTF?
|
||||
$('a[title="Save changes."]')[0].click(); // disable deletable
|
||||
|
||||
};
|
||||
|
||||
TestRunner.prototype.test_add2map = function(identifier){
|
||||
var current_num = Object.keys( map._layers ).length;
|
||||
if( current_num <= this.num_layers_before_parse ){
|
||||
console.error( "[ ADD2MAP TEST FAIL ]: ", identifier );
|
||||
}
|
||||
else {
|
||||
console.warn( "[ PASSED ]: test_add2map" );
|
||||
}
|
||||
};
|
||||
|
||||
TestRunner.prototype.get_layer_count = function(){
|
||||
this.num_layers_before_parse = Object.keys( map._layers ).length;
|
||||
};
|
||||
|
||||
TestRunner.prototype.test_parsing = function( data, identifier ){
|
||||
var is_valid = FormatSniffer( { data : data } ).sniff();
|
||||
if ( !is_valid ) {
|
||||
console.error( "[ PARSE TEST FAIL ]: ", identifier );
|
||||
}
|
||||
else {
|
||||
console.warn( "[ PASSED ]: test_parsing" );
|
||||
}
|
||||
};
|
||||
|
||||
return TestRunner; // return class def
|
||||
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"select_location": "장소 선택",
|
||||
"zoom_in_and_choose": "줌 인하고 직사각형 도구를 사용하여 영역을 선택하세요.",
|
||||
"select_world": "세계 선택",
|
||||
"choose_world": "세계 선택",
|
||||
"no_world_selected": "선택된 세계 없음",
|
||||
"start_generation": "생성 시작",
|
||||
"progress": "진행",
|
||||
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
||||
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
||||
"invalid_format": "잘못된 형식입니다. 'lat,lng,lat,lng' 또는 'lat lng lat lng' 형식을 사용하세요.",
|
||||
"select_a_location_first": "먼저 위치를 선택하세요!",
|
||||
"select_a_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
||||
"generation_process_started": "생성 프로세스가 시작되었습니다.",
|
||||
"winter_mode": "겨울 모드:",
|
||||
"world_scale": "세계 규모:",
|
||||
"custom_bounding_box": "사용자 지정 경계 상자:",
|
||||
"floodfill_timeout": "채우기 시간 초과 (초):",
|
||||
"ground_level": "지면 레벨:",
|
||||
"choose_world_modal_title": "세계 선택",
|
||||
"select_existing_world": "이미 존재하는 세계 선택",
|
||||
"generate_new_world": "새 세계 생성",
|
||||
"customization_settings": "사용자 지정 설정",
|
||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||
"new_version_available": "새로운 버전이 있습니다! 여기를 클릭하여 다운로드하세요.",
|
||||
"minecraft_directory_not_found": "마인크래프트 디렉토리가 발견되지 않았습니다",
|
||||
"world_in_use": "선택한 세계가 현재 사용 중입니다",
|
||||
"failed_to_create_world": "새 세계 생성에 실패했습니다",
|
||||
"no_world_selected_error": "선택된 세계 없음 오류",
|
||||
"select_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
||||
"select_location_first": "먼저 위치를 선택하세요!",
|
||||
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
||||
"area_extensive": "이 지역은 꽤 광범위하여 значитель한 시간과 자원이 필요할 수 있습니다.",
|
||||
"selection_confirmed": "선택이 확인되었습니다!"
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"select_location": "Wybierz lokalizację",
|
||||
"zoom_in_and_choose": "Przybliż i zaznacz obszar za pomocą prostokątnego narzędzia",
|
||||
"select_world": "Wybierz świat",
|
||||
"choose_world": "Wybierz świat",
|
||||
"no_world_selected": "Nie wybrano świata",
|
||||
"start_generation": "Rozpocznij generowanie",
|
||||
"progress": "Postęp",
|
||||
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
||||
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub nieprawidłowo uporządkowane (wymagane Lat przed Lng).",
|
||||
"invalid_format": "Nieprawidłowy format. Użyj 'lat,lng,lat,lng' lub 'lat lng lat lng'.",
|
||||
"select_a_location_first": "Najpierw wybierz lokalizację!",
|
||||
"select_a_minecraft_world_first": "Najpierw wybierz świat Minecrafta!",
|
||||
"generation_process_started": "Proces generowania rozpoczęty.",
|
||||
"winter_mode": "Tryb zimowy:",
|
||||
"world_scale": "Skala świata:",
|
||||
"custom_bounding_box": "Niestandardowe obramowanie obszaru:",
|
||||
"floodfill_timeout": "Limit czasu wypełniania (sek):",
|
||||
"ground_level": "Wysokość obszaru:",
|
||||
"choose_world_modal_title": "Wybierz świat",
|
||||
"select_existing_world": "Wybierz istniejący świat",
|
||||
"generate_new_world": "Generuj nowy świat",
|
||||
"customization_settings": "Ustawienia personalizacji",
|
||||
"footer_text": "© {year} Arnis v{version} przez louis-e",
|
||||
"new_version_available": "Dostępna jest nowa wersja! Kliknij tutaj, aby ją pobrać.",
|
||||
"minecraft_directory_not_found": "Nie znaleziono katalogu Minecrafta",
|
||||
"world_in_use": "Wybrany świat jest obecnie używany",
|
||||
"failed_to_create_world": "Nie udało się utworzyć nowego świata",
|
||||
"no_world_selected_error": "Nie wybrano świata",
|
||||
"select_minecraft_world_first": "Najpierw wybierz świat Minecrafta!",
|
||||
"select_location_first": "Najpierw wybierz lokalizację!",
|
||||
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć typowe limity obliczeniowe.",
|
||||
"area_extensive": "Ten obszar jest dość rozległy i może wymagać znacznego czasu i zasobów.",
|
||||
"selection_confirmed": "Wybór potwierdzony!"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 89 KiB |
BIN
icons/32x32.png
|
Before Width: | Height: | Size: 2.9 KiB |
BIN
icons/icon.icns
BIN
icons/icon.ico
|
Before Width: | Height: | Size: 221 KiB |
BIN
icons/icon.png
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
175
src/args.rs
@@ -1,103 +1,154 @@
|
||||
use clap::{ArgGroup, Parser};
|
||||
use colored::Colorize;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Command-line arguments parser
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about)]
|
||||
#[command(group(
|
||||
ArgGroup::new("location")
|
||||
.required(true)
|
||||
.args(&["bbox", "file"])
|
||||
))]
|
||||
pub struct Args {
|
||||
/// Bounding box of the area (min_lng,min_lat,max_lng,max_lat) (required)
|
||||
#[arg(long, allow_hyphen_values = true)]
|
||||
pub bbox: Option<String>,
|
||||
/// Bounding box of the area (min_lat,min_lng,max_lat,max_lng) (required)
|
||||
#[arg(long, allow_hyphen_values = true, value_parser = LLBBox::from_str)]
|
||||
pub bbox: LLBBox,
|
||||
|
||||
/// JSON file containing OSM data (optional)
|
||||
#[arg(long)]
|
||||
#[arg(long, group = "location")]
|
||||
pub file: Option<String>,
|
||||
|
||||
/// JSON file to save OSM data to (optional)
|
||||
#[arg(long, group = "location")]
|
||||
pub save_json_file: Option<String>,
|
||||
|
||||
/// Path to the Minecraft world (required)
|
||||
#[arg(long, required = true)]
|
||||
pub path: String,
|
||||
#[arg(long, value_parser = validate_minecraft_world_path)]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Downloader method (requests/curl/wget) (optional)
|
||||
#[arg(long, default_value = "requests")]
|
||||
pub downloader: String,
|
||||
|
||||
/// World scale to use, in blocks per meter
|
||||
#[arg(long, default_value = "1.0")]
|
||||
#[arg(long, default_value_t = 1.0)]
|
||||
pub scale: f64,
|
||||
|
||||
/// Ground level to use in the Minecraft world
|
||||
#[arg(long, default_value_t = -62)]
|
||||
pub ground_level: i32,
|
||||
|
||||
/// Enable winter mode (default: false)
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub winter: bool,
|
||||
/// Enable terrain (optional)
|
||||
#[arg(long)]
|
||||
pub terrain: bool,
|
||||
|
||||
/// Enable interior generation (optional)
|
||||
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
|
||||
pub interior: bool,
|
||||
|
||||
/// Enable roof generation (optional)
|
||||
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
|
||||
pub roof: bool,
|
||||
|
||||
/// Enable filling ground (optional)
|
||||
#[arg(long, default_value_t = false, action = clap::ArgAction::SetFalse)]
|
||||
pub fillground: bool,
|
||||
|
||||
/// Enable debug mode (optional)
|
||||
#[arg(long, default_value_t = false, action = clap::ArgAction::SetTrue)]
|
||||
#[arg(long)]
|
||||
pub debug: bool,
|
||||
|
||||
/// Set floodfill timeout (seconds) (optional)
|
||||
#[arg(long, value_parser = parse_duration)]
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// Number of parallel threads (0 = auto, uses available cores - 1)
|
||||
#[arg(long, default_value_t = 0)]
|
||||
pub threads: usize,
|
||||
|
||||
/// Number of regions to batch per processing unit (1 = one region, 2 = 2x2=4 regions, etc.)
|
||||
/// Larger batches reduce element duplication overhead but use more memory per unit
|
||||
#[arg(long, default_value_t = 2)]
|
||||
pub region_batch_size: usize,
|
||||
|
||||
/// Disable parallel processing (process sequentially) - DEFAULT due to correctness issues
|
||||
#[arg(long, default_value_t = true)]
|
||||
pub no_parallel: bool,
|
||||
|
||||
/// Force parallel processing (experimental, may have visual bugs)
|
||||
#[arg(long)]
|
||||
pub force_parallel: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn run(&self) {
|
||||
// Validating the world path
|
||||
let mc_world_path: &Path = Path::new(&self.path);
|
||||
if !mc_world_path.join("region").exists() {
|
||||
eprintln!(
|
||||
"{}",
|
||||
"Error! No Minecraft world found at the given path"
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Validating bbox if provided
|
||||
if let Some(bbox) = &self.bbox {
|
||||
if !validate_bounding_box(bbox) {
|
||||
eprintln!("{}", "Error! Invalid bbox input".red().bold());
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||
let mc_world_path = PathBuf::from(path);
|
||||
if !mc_world_path.exists() {
|
||||
return Err(format!("Path does not exist: {path}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the bounding box string
|
||||
fn validate_bounding_box(bbox: &str) -> bool {
|
||||
let parts: Vec<&str> = bbox.split(',').collect();
|
||||
if parts.len() != 4 {
|
||||
return false;
|
||||
if !mc_world_path.is_dir() {
|
||||
return Err(format!("Path is not a directory: {path}"));
|
||||
}
|
||||
|
||||
let min_lng: f64 = parts[0].parse().ok().unwrap_or(0.0);
|
||||
let min_lat: f64 = parts[1].parse().ok().unwrap_or(0.0);
|
||||
let max_lng: f64 = parts[2].parse().ok().unwrap_or(0.0);
|
||||
let max_lat: f64 = parts[3].parse().ok().unwrap_or(0.0);
|
||||
|
||||
if !(-180.0..=180.0).contains(&min_lng) || !(-180.0..=180.0).contains(&max_lng) {
|
||||
return false;
|
||||
let region = mc_world_path.join("region");
|
||||
if !region.is_dir() {
|
||||
return Err(format!("No Minecraft world found at {region:?}"));
|
||||
}
|
||||
|
||||
if !(-90.0..=90.0).contains(&min_lat) || !(-90.0..=90.0).contains(&max_lat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
min_lng < max_lng && min_lat < max_lat
|
||||
Ok(mc_world_path)
|
||||
}
|
||||
|
||||
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||
let seconds = arg.parse()?;
|
||||
Ok(std::time::Duration::from_secs(seconds))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn minecraft_tmpdir() -> tempfile::TempDir {
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
// create a `region` directory in the tempdir
|
||||
let region_path = tmpdir.path().join("region");
|
||||
std::fs::create_dir(®ion_path).unwrap();
|
||||
tmpdir
|
||||
}
|
||||
#[test]
|
||||
fn test_flags() {
|
||||
let tmpdir = minecraft_tmpdir();
|
||||
let tmp_path = tmpdir.path().to_str().unwrap();
|
||||
|
||||
// Test that terrain/debug are SetTrue
|
||||
let cmd = [
|
||||
"arnis",
|
||||
"--path",
|
||||
tmp_path,
|
||||
"--bbox",
|
||||
"1,2,3,4",
|
||||
"--terrain",
|
||||
"--debug",
|
||||
];
|
||||
let args = Args::parse_from(cmd.iter());
|
||||
assert!(args.debug);
|
||||
assert!(args.terrain);
|
||||
|
||||
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
|
||||
let args = Args::parse_from(cmd.iter());
|
||||
assert!(!args.debug);
|
||||
assert!(!args.terrain);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_options() {
|
||||
let tmpdir = minecraft_tmpdir();
|
||||
let tmp_path = tmpdir.path().to_str().unwrap();
|
||||
|
||||
let cmd = ["arnis"];
|
||||
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
||||
|
||||
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
|
||||
assert!(Args::try_parse_from(cmd.iter()).is_ok());
|
||||
|
||||
let cmd = ["arnis", "--path", tmp_path, "--file", ""];
|
||||
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
||||
|
||||
// The --gui flag isn't used here, ugh. TODO clean up main.rs and its argparse usage.
|
||||
// let cmd = ["arnis", "--gui"];
|
||||
// assert!(Args::try_parse_from(cmd.iter()).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -7,22 +7,93 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::colors::RGBTuple;
|
||||
|
||||
// Enums for stair properties
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum StairFacing {
|
||||
North,
|
||||
East,
|
||||
South,
|
||||
West,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum StairShape {
|
||||
Straight,
|
||||
InnerLeft,
|
||||
InnerRight,
|
||||
OuterLeft,
|
||||
OuterRight,
|
||||
}
|
||||
|
||||
impl StairFacing {
|
||||
#[inline(always)]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
StairFacing::North => "north",
|
||||
StairFacing::East => "east",
|
||||
StairFacing::South => "south",
|
||||
StairFacing::West => "west",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StairShape {
|
||||
#[inline(always)]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
StairShape::Straight => "straight",
|
||||
StairShape::InnerLeft => "inner_left",
|
||||
StairShape::InnerRight => "inner_right",
|
||||
StairShape::OuterLeft => "outer_left",
|
||||
StairShape::OuterRight => "outer_right",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type definitions for better readability
|
||||
type ColorTuple = (u8, u8, u8);
|
||||
type BlockOptions = &'static [Block];
|
||||
type ColorBlockMapping = (ColorTuple, BlockOptions);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct Block {
|
||||
id: u8,
|
||||
}
|
||||
|
||||
// Extended block with dynamic properties
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BlockWithProperties {
|
||||
pub block: Block,
|
||||
pub properties: Option<Value>,
|
||||
}
|
||||
|
||||
impl BlockWithProperties {
|
||||
pub fn new(block: Block, properties: Option<Value>) -> Self {
|
||||
Self { block, properties }
|
||||
}
|
||||
|
||||
pub fn simple(block: Block) -> Self {
|
||||
Self {
|
||||
block,
|
||||
properties: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Block {
|
||||
#[inline(always)]
|
||||
const fn new(id: u8) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn id(&self) -> u8 {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn namespace(&self) -> &str {
|
||||
"mincraft"
|
||||
"minecraft"
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
@@ -41,7 +112,7 @@ impl Block {
|
||||
11 => "chiseled_stone_bricks",
|
||||
12 => "cobblestone_wall",
|
||||
13 => "cobblestone",
|
||||
14 => "cracked_polished_blackstone_bricks",
|
||||
14 => "polished_blackstone_bricks",
|
||||
15 => "cracked_stone_bricks",
|
||||
16 => "crimson_planks",
|
||||
17 => "cut_sandstone",
|
||||
@@ -52,11 +123,11 @@ impl Block {
|
||||
22 => "dirt",
|
||||
23 => "end_stone_bricks",
|
||||
24 => "farmland",
|
||||
25 => "glass_pane",
|
||||
25 => "glass",
|
||||
26 => "glowstone",
|
||||
27 => "granite",
|
||||
28 => "grass_block",
|
||||
29 => "tall_grass",
|
||||
29 => "short_grass",
|
||||
30 => "gravel",
|
||||
31 => "gray_concrete",
|
||||
32 => "gray_terracotta",
|
||||
@@ -74,7 +145,7 @@ impl Block {
|
||||
44 => "mossy_cobblestone",
|
||||
45 => "mud_bricks",
|
||||
46 => "nether_bricks",
|
||||
47 => "nether_bricks",
|
||||
47 => "netherite_block",
|
||||
48 => "oak_fence",
|
||||
49 => "oak_leaves",
|
||||
50 => "oak_log",
|
||||
@@ -84,7 +155,7 @@ impl Block {
|
||||
54 => "podzol",
|
||||
55 => "polished_andesite",
|
||||
56 => "polished_basalt",
|
||||
57 => "polished_blackstone_bricks",
|
||||
57 => "quartz_block",
|
||||
58 => "polished_blackstone",
|
||||
59 => "polished_deepslate",
|
||||
60 => "polished_diorite",
|
||||
@@ -141,12 +212,79 @@ impl Block {
|
||||
111 => "snow_block",
|
||||
112 => "snow",
|
||||
113 => "oak_sign",
|
||||
114 => "andesite_wall",
|
||||
115 => "stone_brick_wall",
|
||||
116..=125 => "rail",
|
||||
126 => "coarse_dirt",
|
||||
127 => "iron_ore",
|
||||
128 => "coal_ore",
|
||||
129 => "gold_ore",
|
||||
130 => "copper_ore",
|
||||
131 => "clay",
|
||||
132 => "dirt_path",
|
||||
133 => "ice",
|
||||
134 => "packed_ice",
|
||||
135 => "mud",
|
||||
136 => "dead_bush",
|
||||
137..=138 => "tall_grass",
|
||||
139 => "crafting_table",
|
||||
140 => "furnace",
|
||||
141 => "white_carpet",
|
||||
142 => "bookshelf",
|
||||
143 => "oak_pressure_plate",
|
||||
144 => "oak_stairs",
|
||||
155 => "chest",
|
||||
156 => "red_carpet",
|
||||
157 => "anvil",
|
||||
158 => "note_block",
|
||||
159 => "oak_door",
|
||||
160 => "brewing_stand",
|
||||
161 => "red_bed", // North head
|
||||
162 => "red_bed", // North foot
|
||||
163 => "red_bed", // East head
|
||||
164 => "red_bed", // East foot
|
||||
165 => "red_bed", // South head
|
||||
166 => "red_bed", // South foot
|
||||
167 => "red_bed", // West head
|
||||
168 => "red_bed", // West foot
|
||||
169 => "gray_stained_glass",
|
||||
170 => "light_gray_stained_glass",
|
||||
171 => "brown_stained_glass",
|
||||
172 => "tinted_glass",
|
||||
173 => "oak_trapdoor",
|
||||
174 => "brown_concrete",
|
||||
175 => "black_terracotta",
|
||||
176 => "brown_terracotta",
|
||||
177 => "stone_brick_stairs",
|
||||
178 => "mud_brick_stairs",
|
||||
179 => "polished_blackstone_brick_stairs",
|
||||
180 => "brick_stairs",
|
||||
181 => "polished_granite_stairs",
|
||||
182 => "end_stone_brick_stairs",
|
||||
183 => "polished_diorite_stairs",
|
||||
184 => "smooth_sandstone_stairs",
|
||||
185 => "quartz_stairs",
|
||||
186 => "polished_andesite_stairs",
|
||||
187 => "nether_brick_stairs",
|
||||
188 => "fern",
|
||||
_ => panic!("Invalid id"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn properties(&self) -> Option<Value> {
|
||||
match self.id {
|
||||
3 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
49 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
105 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("age".to_string(), Value::String("7".to_string()));
|
||||
@@ -187,11 +325,200 @@ impl Block {
|
||||
map
|
||||
})),
|
||||
|
||||
116 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
"shape".to_string(),
|
||||
Value::String("north_south".to_string()),
|
||||
);
|
||||
map
|
||||
})),
|
||||
|
||||
117 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("shape".to_string(), Value::String("east_west".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
118 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
"shape".to_string(),
|
||||
Value::String("ascending_east".to_string()),
|
||||
);
|
||||
map
|
||||
})),
|
||||
|
||||
119 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
"shape".to_string(),
|
||||
Value::String("ascending_west".to_string()),
|
||||
);
|
||||
map
|
||||
})),
|
||||
|
||||
120 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
"shape".to_string(),
|
||||
Value::String("ascending_north".to_string()),
|
||||
);
|
||||
map
|
||||
})),
|
||||
|
||||
121 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
"shape".to_string(),
|
||||
Value::String("ascending_south".to_string()),
|
||||
);
|
||||
map
|
||||
})),
|
||||
|
||||
122 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("shape".to_string(), Value::String("north_east".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
123 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("shape".to_string(), Value::String("north_west".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
124 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("shape".to_string(), Value::String("south_east".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
125 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("shape".to_string(), Value::String("south_west".to_string()));
|
||||
map
|
||||
})),
|
||||
137 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||
map
|
||||
})),
|
||||
138 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
// Red bed variations by direction and part
|
||||
161 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||
map.insert("part".to_string(), Value::String("head".to_string()));
|
||||
map
|
||||
})),
|
||||
162 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||
map.insert("part".to_string(), Value::String("foot".to_string()));
|
||||
map
|
||||
})),
|
||||
163 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||
map.insert("part".to_string(), Value::String("head".to_string()));
|
||||
map
|
||||
})),
|
||||
164 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||
map.insert("part".to_string(), Value::String("foot".to_string()));
|
||||
map
|
||||
})),
|
||||
165 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||
map.insert("part".to_string(), Value::String("head".to_string()));
|
||||
map
|
||||
})),
|
||||
166 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||
map.insert("part".to_string(), Value::String("foot".to_string()));
|
||||
map
|
||||
})),
|
||||
167 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||
map.insert("part".to_string(), Value::String("head".to_string()));
|
||||
map
|
||||
})),
|
||||
168 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||
map.insert("part".to_string(), Value::String("foot".to_string()));
|
||||
map
|
||||
})),
|
||||
173 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for stair blocks with properties
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
static STAIR_CACHE: Lazy<Mutex<HashMap<(u8, StairFacing, StairShape), BlockWithProperties>>> =
|
||||
Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
// General function to create any stair block with facing and shape properties
|
||||
pub fn create_stair_with_properties(
|
||||
base_stair_block: Block,
|
||||
facing: StairFacing,
|
||||
shape: StairShape,
|
||||
) -> BlockWithProperties {
|
||||
let cache_key = (base_stair_block.id(), facing, shape);
|
||||
|
||||
// Check cache first
|
||||
{
|
||||
let cache = STAIR_CACHE.lock().unwrap();
|
||||
if let Some(cached_block) = cache.get(&cache_key) {
|
||||
return cached_block.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Create properties
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
"facing".to_string(),
|
||||
Value::String(facing.as_str().to_string()),
|
||||
);
|
||||
|
||||
// Only add shape if it's not straight (default)
|
||||
if !matches!(shape, StairShape::Straight) {
|
||||
map.insert(
|
||||
"shape".to_string(),
|
||||
Value::String(shape.as_str().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let properties = Value::Compound(map);
|
||||
let block_with_props = BlockWithProperties::new(base_stair_block, Some(properties));
|
||||
|
||||
// Cache the result
|
||||
{
|
||||
let mut cache = STAIR_CACHE.lock().unwrap();
|
||||
cache.insert(cache_key, block_with_props.clone());
|
||||
}
|
||||
|
||||
block_with_props
|
||||
}
|
||||
|
||||
// Lazy static blocks
|
||||
pub const ACACIA_PLANKS: Block = Block::new(0);
|
||||
pub const AIR: Block = Block::new(1);
|
||||
@@ -207,7 +534,7 @@ pub const CAULDRON: Block = Block::new(10);
|
||||
pub const CHISELED_STONE_BRICKS: Block = Block::new(11);
|
||||
pub const COBBLESTONE_WALL: Block = Block::new(12);
|
||||
pub const COBBLESTONE: Block = Block::new(13);
|
||||
pub const CRACKED_POLISHED_BLACKSTONE_BRICKS: Block = Block::new(14);
|
||||
pub const POLISHED_BLACKSTONE_BRICKS: Block = Block::new(14);
|
||||
pub const CRACKED_STONE_BRICKS: Block = Block::new(15);
|
||||
pub const CRIMSON_PLANKS: Block = Block::new(16);
|
||||
pub const CUT_SANDSTONE: Block = Block::new(17);
|
||||
@@ -240,7 +567,7 @@ pub const MOSS_BLOCK: Block = Block::new(43);
|
||||
pub const MOSSY_COBBLESTONE: Block = Block::new(44);
|
||||
pub const MUD_BRICKS: Block = Block::new(45);
|
||||
pub const NETHER_BRICK: Block = Block::new(46);
|
||||
pub const NETHER_BRICKS: Block = Block::new(47);
|
||||
pub const NETHERITE_BLOCK: Block = Block::new(47);
|
||||
pub const OAK_FENCE: Block = Block::new(48);
|
||||
pub const OAK_LEAVES: Block = Block::new(49);
|
||||
pub const OAK_LOG: Block = Block::new(50);
|
||||
@@ -250,7 +577,7 @@ pub const ORANGE_TERRACOTTA: Block = Block::new(53);
|
||||
pub const PODZOL: Block = Block::new(54);
|
||||
pub const POLISHED_ANDESITE: Block = Block::new(55);
|
||||
pub const POLISHED_BASALT: Block = Block::new(56);
|
||||
pub const POLISHED_BLACKSTONE_BRICKS: Block = Block::new(57);
|
||||
pub const QUARTZ_BLOCK: Block = Block::new(57);
|
||||
pub const POLISHED_BLACKSTONE: Block = Block::new(58);
|
||||
pub const POLISHED_DEEPSLATE: Block = Block::new(59);
|
||||
pub const POLISHED_DIORITE: Block = Block::new(60);
|
||||
@@ -261,7 +588,7 @@ pub const PURPUR_PILLAR: Block = Block::new(64);
|
||||
pub const QUARTZ_BRICKS: Block = Block::new(65);
|
||||
pub const RAIL: Block = Block::new(66);
|
||||
pub const RED_FLOWER: Block = Block::new(67);
|
||||
pub const RED_NETHER_BRICKS: Block = Block::new(68);
|
||||
pub const RED_NETHER_BRICK: Block = Block::new(68);
|
||||
pub const RED_TERRACOTTA: Block = Block::new(69);
|
||||
pub const RED_WOOL: Block = Block::new(70);
|
||||
pub const SAND: Block = Block::new(71);
|
||||
@@ -301,105 +628,382 @@ pub const YELLOW_TERRACOTTA: Block = Block::new(104);
|
||||
pub const SNOW_BLOCK: Block = Block::new(111);
|
||||
pub const SNOW_LAYER: Block = Block::new(112);
|
||||
pub const SIGN: Block = Block::new(113);
|
||||
|
||||
pub const ANDESITE_WALL: Block = Block::new(114);
|
||||
pub const STONE_BRICK_WALL: Block = Block::new(115);
|
||||
pub const CARROTS: Block = Block::new(105);
|
||||
pub const DARK_OAK_DOOR_LOWER: Block = Block::new(106);
|
||||
pub const DARK_OAK_DOOR_UPPER: Block = Block::new(107);
|
||||
pub const POTATOES: Block = Block::new(108);
|
||||
pub const WHEAT: Block = Block::new(109);
|
||||
|
||||
pub const BEDROCK: Block = Block::new(110);
|
||||
pub const RAIL_NORTH_SOUTH: Block = Block::new(116);
|
||||
pub const RAIL_EAST_WEST: Block = Block::new(117);
|
||||
pub const RAIL_ASCENDING_EAST: Block = Block::new(118);
|
||||
pub const RAIL_ASCENDING_WEST: Block = Block::new(119);
|
||||
pub const RAIL_ASCENDING_NORTH: Block = Block::new(120);
|
||||
pub const RAIL_ASCENDING_SOUTH: Block = Block::new(121);
|
||||
pub const RAIL_NORTH_EAST: Block = Block::new(122);
|
||||
pub const RAIL_NORTH_WEST: Block = Block::new(123);
|
||||
pub const RAIL_SOUTH_EAST: Block = Block::new(124);
|
||||
pub const RAIL_SOUTH_WEST: Block = Block::new(125);
|
||||
pub const COARSE_DIRT: Block = Block::new(126);
|
||||
pub const IRON_ORE: Block = Block::new(127);
|
||||
pub const COAL_ORE: Block = Block::new(128);
|
||||
pub const GOLD_ORE: Block = Block::new(129);
|
||||
pub const COPPER_ORE: Block = Block::new(130);
|
||||
pub const CLAY: Block = Block::new(131);
|
||||
pub const DIRT_PATH: Block = Block::new(132);
|
||||
pub const ICE: Block = Block::new(133);
|
||||
pub const PACKED_ICE: Block = Block::new(134);
|
||||
pub const MUD: Block = Block::new(135);
|
||||
pub const DEAD_BUSH: Block = Block::new(136);
|
||||
pub const TALL_GRASS_BOTTOM: Block = Block::new(137);
|
||||
pub const TALL_GRASS_TOP: Block = Block::new(138);
|
||||
pub const CRAFTING_TABLE: Block = Block::new(139);
|
||||
pub const FURNACE: Block = Block::new(140);
|
||||
pub const WHITE_CARPET: Block = Block::new(141);
|
||||
pub const BOOKSHELF: Block = Block::new(142);
|
||||
pub const OAK_PRESSURE_PLATE: Block = Block::new(143);
|
||||
pub const OAK_STAIRS: Block = Block::new(144);
|
||||
pub const CHEST: Block = Block::new(155);
|
||||
pub const RED_CARPET: Block = Block::new(156);
|
||||
pub const ANVIL: Block = Block::new(157);
|
||||
pub const NOTE_BLOCK: Block = Block::new(158);
|
||||
pub const OAK_DOOR: Block = Block::new(159);
|
||||
pub const BREWING_STAND: Block = Block::new(160);
|
||||
pub const RED_BED_NORTH_HEAD: Block = Block::new(161);
|
||||
pub const RED_BED_NORTH_FOOT: Block = Block::new(162);
|
||||
pub const RED_BED_EAST_HEAD: Block = Block::new(163);
|
||||
pub const RED_BED_EAST_FOOT: Block = Block::new(164);
|
||||
pub const RED_BED_SOUTH_HEAD: Block = Block::new(165);
|
||||
pub const RED_BED_SOUTH_FOOT: Block = Block::new(166);
|
||||
pub const RED_BED_WEST_HEAD: Block = Block::new(167);
|
||||
pub const RED_BED_WEST_FOOT: Block = Block::new(168);
|
||||
pub const GRAY_STAINED_GLASS: Block = Block::new(169);
|
||||
pub const LIGHT_GRAY_STAINED_GLASS: Block = Block::new(170);
|
||||
pub const BROWN_STAINED_GLASS: Block = Block::new(171);
|
||||
pub const TINTED_GLASS: Block = Block::new(172);
|
||||
pub const OAK_TRAPDOOR: Block = Block::new(173);
|
||||
pub const BROWN_CONCRETE: Block = Block::new(174);
|
||||
pub const BLACK_TERRACOTTA: Block = Block::new(175);
|
||||
pub const BROWN_TERRACOTTA: Block = Block::new(176);
|
||||
pub const STONE_BRICK_STAIRS: Block = Block::new(177);
|
||||
pub const MUD_BRICK_STAIRS: Block = Block::new(178);
|
||||
pub const POLISHED_BLACKSTONE_BRICK_STAIRS: Block = Block::new(179);
|
||||
pub const BRICK_STAIRS: Block = Block::new(180);
|
||||
pub const POLISHED_GRANITE_STAIRS: Block = Block::new(181);
|
||||
pub const END_STONE_BRICK_STAIRS: Block = Block::new(182);
|
||||
pub const POLISHED_DIORITE_STAIRS: Block = Block::new(183);
|
||||
pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
||||
pub const FERN: Block = Block::new(188);
|
||||
|
||||
// Variations for building corners
|
||||
pub fn building_corner_variations() -> Vec<Block> {
|
||||
vec![
|
||||
/// Maps a block to its corresponding stair variant
|
||||
#[inline]
|
||||
pub fn get_stair_block_for_material(material: Block) -> Block {
|
||||
match material {
|
||||
STONE_BRICKS => STONE_BRICK_STAIRS,
|
||||
MUD_BRICKS => MUD_BRICK_STAIRS,
|
||||
OAK_PLANKS => OAK_STAIRS,
|
||||
POLISHED_ANDESITE => STONE_BRICK_STAIRS,
|
||||
SMOOTH_STONE => POLISHED_ANDESITE_STAIRS,
|
||||
OAK_PLANKS => OAK_STAIRS,
|
||||
ANDESITE => STONE_BRICK_STAIRS,
|
||||
CHISELED_STONE_BRICKS => STONE_BRICK_STAIRS,
|
||||
BLACK_TERRACOTTA => POLISHED_BLACKSTONE_BRICK_STAIRS,
|
||||
BLACKSTONE => POLISHED_BLACKSTONE_BRICK_STAIRS,
|
||||
BLUE_TERRACOTTA => MUD_BRICK_STAIRS,
|
||||
BRICK => BRICK_STAIRS,
|
||||
BROWN_CONCRETE => MUD_BRICK_STAIRS,
|
||||
BROWN_TERRACOTTA => MUD_BRICK_STAIRS,
|
||||
DEEPSLATE_BRICKS => STONE_BRICK_STAIRS,
|
||||
END_STONE_BRICKS => END_STONE_BRICK_STAIRS,
|
||||
GRAY_CONCRETE => POLISHED_BLACKSTONE_BRICK_STAIRS,
|
||||
GRAY_TERRACOTTA => MUD_BRICK_STAIRS,
|
||||
LIGHT_BLUE_TERRACOTTA => STONE_BRICK_STAIRS,
|
||||
LIGHT_GRAY_CONCRETE => STONE_BRICK_STAIRS,
|
||||
NETHER_BRICK => NETHER_BRICK_STAIRS,
|
||||
POLISHED_BLACKSTONE => POLISHED_BLACKSTONE_BRICK_STAIRS,
|
||||
POLISHED_BLACKSTONE_BRICKS => POLISHED_BLACKSTONE_BRICK_STAIRS,
|
||||
POLISHED_DEEPSLATE => STONE_BRICK_STAIRS,
|
||||
POLISHED_GRANITE => POLISHED_GRANITE_STAIRS,
|
||||
QUARTZ_BLOCK => POLISHED_DIORITE_STAIRS,
|
||||
QUARTZ_BRICKS => POLISHED_DIORITE_STAIRS,
|
||||
SANDSTONE => SMOOTH_SANDSTONE_STAIRS,
|
||||
SMOOTH_SANDSTONE => SMOOTH_SANDSTONE_STAIRS,
|
||||
WHITE_CONCRETE => QUARTZ_STAIRS,
|
||||
WHITE_TERRACOTTA => MUD_BRICK_STAIRS,
|
||||
_ => STONE_BRICK_STAIRS,
|
||||
}
|
||||
}
|
||||
|
||||
// Window variations for different building types
|
||||
pub static WINDOW_VARIATIONS: [Block; 7] = [
|
||||
GLASS,
|
||||
GRAY_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
WHITE_STAINED_GLASS,
|
||||
TINTED_GLASS,
|
||||
];
|
||||
|
||||
// Window types for different building styles
|
||||
pub fn get_window_block_for_building_type(building_type: &str) -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
match building_type {
|
||||
"residential" | "house" | "apartment" => {
|
||||
let residential_windows = [
|
||||
GLASS,
|
||||
WHITE_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
];
|
||||
residential_windows[rng.gen_range(0..residential_windows.len())]
|
||||
}
|
||||
"hospital" | "school" | "university" => {
|
||||
let institutional_windows = [GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS];
|
||||
institutional_windows[rng.gen_range(0..institutional_windows.len())]
|
||||
}
|
||||
"hotel" | "restaurant" => {
|
||||
let hospitality_windows = [GLASS, WHITE_STAINED_GLASS];
|
||||
hospitality_windows[rng.gen_range(0..hospitality_windows.len())]
|
||||
}
|
||||
"industrial" | "warehouse" => {
|
||||
let industrial_windows = [
|
||||
GLASS,
|
||||
GRAY_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
];
|
||||
industrial_windows[rng.gen_range(0..industrial_windows.len())]
|
||||
}
|
||||
_ => WINDOW_VARIATIONS[rng.gen_range(0..WINDOW_VARIATIONS.len())],
|
||||
}
|
||||
}
|
||||
|
||||
// Random floor block selection
|
||||
pub fn get_random_floor_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let floor_options = [
|
||||
WHITE_CONCRETE,
|
||||
GRAY_CONCRETE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
POLISHED_ANDESITE,
|
||||
SMOOTH_STONE,
|
||||
STONE_BRICKS,
|
||||
COBBLESTONE,
|
||||
BRICK,
|
||||
MOSSY_COBBLESTONE,
|
||||
SANDSTONE,
|
||||
RED_NETHER_BRICKS,
|
||||
MUD_BRICKS,
|
||||
OAK_PLANKS,
|
||||
];
|
||||
floor_options[rng.gen_range(0..floor_options.len())]
|
||||
}
|
||||
|
||||
// Define all predefined colors with their blocks
|
||||
static DEFINED_COLORS: &[ColorBlockMapping] = &[
|
||||
((233, 107, 57), &[BRICK, NETHER_BRICK]),
|
||||
(
|
||||
(18, 12, 13),
|
||||
&[POLISHED_BLACKSTONE_BRICKS, BLACKSTONE, DEEPSLATE_BRICKS],
|
||||
),
|
||||
((76, 127, 153), &[LIGHT_BLUE_TERRACOTTA]),
|
||||
(
|
||||
(0, 0, 0),
|
||||
&[DEEPSLATE_BRICKS, BLACKSTONE, POLISHED_BLACKSTONE],
|
||||
),
|
||||
(
|
||||
(186, 195, 142),
|
||||
&[
|
||||
END_STONE_BRICKS,
|
||||
SANDSTONE,
|
||||
SMOOTH_SANDSTONE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
],
|
||||
),
|
||||
(
|
||||
(57, 41, 35),
|
||||
&[BROWN_TERRACOTTA, BROWN_CONCRETE, MUD_BRICKS, BRICK],
|
||||
),
|
||||
(
|
||||
(112, 108, 138),
|
||||
&[LIGHT_BLUE_TERRACOTTA, GRAY_TERRACOTTA, GRAY_CONCRETE],
|
||||
),
|
||||
(
|
||||
(122, 92, 66),
|
||||
&[MUD_BRICKS, BROWN_TERRACOTTA, SANDSTONE, BRICK],
|
||||
),
|
||||
((24, 13, 14), &[NETHER_BRICK, BLACKSTONE, DEEPSLATE_BRICKS]),
|
||||
(
|
||||
(159, 82, 36),
|
||||
&[
|
||||
BROWN_TERRACOTTA,
|
||||
BRICK,
|
||||
POLISHED_GRANITE,
|
||||
BROWN_CONCRETE,
|
||||
NETHERITE_BLOCK,
|
||||
POLISHED_DEEPSLATE,
|
||||
],
|
||||
),
|
||||
(
|
||||
(128, 128, 128),
|
||||
&[
|
||||
POLISHED_ANDESITE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
SMOOTH_STONE,
|
||||
STONE_BRICKS,
|
||||
],
|
||||
),
|
||||
(
|
||||
(174, 173, 174),
|
||||
&[
|
||||
POLISHED_ANDESITE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
SMOOTH_STONE,
|
||||
STONE_BRICKS,
|
||||
],
|
||||
),
|
||||
((141, 101, 142), &[STONE_BRICKS, BRICK, MUD_BRICKS]),
|
||||
(
|
||||
(142, 60, 46),
|
||||
&[
|
||||
BLACK_TERRACOTTA,
|
||||
NETHERITE_BLOCK,
|
||||
NETHER_BRICK,
|
||||
POLISHED_GRANITE,
|
||||
POLISHED_DEEPSLATE,
|
||||
BROWN_TERRACOTTA,
|
||||
],
|
||||
),
|
||||
(
|
||||
(153, 83, 28),
|
||||
&[
|
||||
BLACK_TERRACOTTA,
|
||||
POLISHED_GRANITE,
|
||||
BROWN_CONCRETE,
|
||||
BROWN_TERRACOTTA,
|
||||
STONE_BRICKS,
|
||||
],
|
||||
),
|
||||
(
|
||||
(224, 216, 175),
|
||||
&[
|
||||
SMOOTH_SANDSTONE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
POLISHED_ANDESITE,
|
||||
SMOOTH_STONE,
|
||||
],
|
||||
),
|
||||
(
|
||||
(188, 182, 179),
|
||||
&[
|
||||
SMOOTH_SANDSTONE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
QUARTZ_BRICKS,
|
||||
POLISHED_ANDESITE,
|
||||
SMOOTH_STONE,
|
||||
],
|
||||
),
|
||||
(
|
||||
(35, 86, 85),
|
||||
&[
|
||||
POLISHED_BLACKSTONE_BRICKS,
|
||||
BLUE_TERRACOTTA,
|
||||
LIGHT_BLUE_TERRACOTTA,
|
||||
],
|
||||
),
|
||||
(
|
||||
(255, 255, 255),
|
||||
&[WHITE_CONCRETE, QUARTZ_BRICKS, QUARTZ_BLOCK],
|
||||
),
|
||||
(
|
||||
(209, 177, 161),
|
||||
&[
|
||||
WHITE_TERRACOTTA,
|
||||
SMOOTH_SANDSTONE,
|
||||
SMOOTH_STONE,
|
||||
SANDSTONE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
],
|
||||
),
|
||||
((191, 147, 42), &[SMOOTH_SANDSTONE, SANDSTONE, SMOOTH_STONE]),
|
||||
];
|
||||
|
||||
// Function to randomly select building wall block with alternatives
|
||||
pub fn get_building_wall_block_for_color(color: RGBTuple) -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// Find the closest color match
|
||||
let closest_color = DEFINED_COLORS
|
||||
.iter()
|
||||
.min_by_key(|(defined_color, _)| crate::colors::rgb_distance(&color, defined_color));
|
||||
|
||||
if let Some((_, options)) = closest_color {
|
||||
options[rng.gen_range(0..options.len())]
|
||||
} else {
|
||||
// This should never happen, but fallback just in case
|
||||
get_fallback_building_block()
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get a random fallback building block when no color attribute is specified
|
||||
pub fn get_fallback_building_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let fallback_options = [
|
||||
BLACKSTONE,
|
||||
SMOOTH_QUARTZ,
|
||||
CHISELED_STONE_BRICKS,
|
||||
POLISHED_BASALT,
|
||||
CUT_SANDSTONE,
|
||||
POLISHED_BLACKSTONE_BRICKS,
|
||||
ANDESITE,
|
||||
GRANITE,
|
||||
DIORITE,
|
||||
CRACKED_STONE_BRICKS,
|
||||
PRISMARINE,
|
||||
BLUE_TERRACOTTA,
|
||||
BLACK_TERRACOTTA,
|
||||
BRICK,
|
||||
BROWN_CONCRETE,
|
||||
BROWN_TERRACOTTA,
|
||||
DEEPSLATE_BRICKS,
|
||||
END_STONE_BRICKS,
|
||||
GRAY_CONCRETE,
|
||||
GRAY_TERRACOTTA,
|
||||
LIGHT_BLUE_TERRACOTTA,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
MUD_BRICKS,
|
||||
NETHER_BRICK,
|
||||
POLISHED_ANDESITE,
|
||||
POLISHED_BLACKSTONE,
|
||||
POLISHED_BLACKSTONE_BRICKS,
|
||||
POLISHED_DEEPSLATE,
|
||||
POLISHED_GRANITE,
|
||||
QUARTZ_BLOCK,
|
||||
QUARTZ_BRICKS,
|
||||
]
|
||||
SANDSTONE,
|
||||
SMOOTH_SANDSTONE,
|
||||
SMOOTH_STONE,
|
||||
STONE_BRICKS,
|
||||
WHITE_CONCRETE,
|
||||
WHITE_TERRACOTTA,
|
||||
OAK_PLANKS,
|
||||
];
|
||||
fallback_options[rng.gen_range(0..fallback_options.len())]
|
||||
}
|
||||
|
||||
// Variations for building walls
|
||||
pub fn building_wall_variations() -> Vec<Block> {
|
||||
building_wall_color_map()
|
||||
.into_iter()
|
||||
.map(|(_, block)| block)
|
||||
.collect()
|
||||
}
|
||||
// Function to get a random castle wall block
|
||||
pub fn get_castle_wall_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// https://wiki.openstreetmap.org/wiki/Key:building:colour
|
||||
pub fn building_wall_color_map() -> Vec<(RGBTuple, Block)> {
|
||||
vec![
|
||||
((233, 107, 57), BRICK),
|
||||
((18, 12, 13), CRACKED_POLISHED_BLACKSTONE_BRICKS),
|
||||
((76, 127, 153), CYAN_CONCRETE),
|
||||
((0, 0, 0), DEEPSLATE_BRICKS),
|
||||
((186, 195, 142), END_STONE_BRICKS),
|
||||
((57, 41, 35), GRAY_TERRACOTTA),
|
||||
((112, 108, 138), LIGHT_BLUE_TERRACOTTA),
|
||||
((122, 92, 66), MUD_BRICKS),
|
||||
((24, 13, 14), NETHER_BRICKS),
|
||||
((159, 82, 36), ORANGE_TERRACOTTA),
|
||||
((128, 128, 128), POLISHED_ANDESITE),
|
||||
((174, 173, 174), POLISHED_DIORITE),
|
||||
((141, 101, 142), PURPUR_PILLAR),
|
||||
((142, 60, 46), RED_TERRACOTTA),
|
||||
((153, 83, 28), SMOOTH_RED_SANDSTONE),
|
||||
((224, 216, 175), SMOOTH_SANDSTONE),
|
||||
((188, 182, 179), SMOOTH_STONE),
|
||||
((35, 86, 85), WARPED_PLANKS),
|
||||
((255, 255, 255), WHITE_CONCRETE),
|
||||
((209, 177, 161), WHITE_TERRACOTTA),
|
||||
((191, 147, 42), YELLOW_TERRACOTTA),
|
||||
]
|
||||
}
|
||||
|
||||
// Variations for building floors
|
||||
pub fn building_floor_variations() -> Vec<Block> {
|
||||
building_wall_color_map()
|
||||
.into_iter()
|
||||
.map(|(_, block)| block)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn building_floor_color_map() -> Vec<(RGBTuple, Block)> {
|
||||
vec![
|
||||
((181, 101, 59), ACACIA_PLANKS),
|
||||
((22, 15, 16), BLACKSTONE),
|
||||
((104, 51, 74), CRIMSON_PLANKS),
|
||||
((82, 55, 26), DARK_OAK_PLANKS),
|
||||
((182, 133, 99), JUNGLE_PLANKS),
|
||||
((33, 128, 185), LIGHT_BLUE_CONCRETE),
|
||||
((78, 103, 43), MOSS_BLOCK),
|
||||
((171, 138, 88), OAK_PLANKS),
|
||||
((0, 128, 0), OXIDIZED_COPPER),
|
||||
((18, 12, 13), POLISHED_BLACKSTONE),
|
||||
((64, 64, 64), POLISHED_DEEPSLATE),
|
||||
((255, 255, 255), POLISHED_DIORITE),
|
||||
((143, 96, 79), POLISHED_GRANITE),
|
||||
((141, 101, 142), PURPUR_BLOCK),
|
||||
((128, 0, 0), RED_NETHER_BRICKS),
|
||||
((153, 83, 28), SMOOTH_RED_SANDSTONE),
|
||||
((128, 96, 57), SPRUCE_PLANKS),
|
||||
((128, 128, 128), STONE_BRICKS),
|
||||
((150, 93, 68), TERRACOTTA),
|
||||
((35, 86, 85), WARPED_PLANKS),
|
||||
]
|
||||
let castle_wall_options = [
|
||||
STONE_BRICKS,
|
||||
CHISELED_STONE_BRICKS,
|
||||
CRACKED_STONE_BRICKS,
|
||||
COBBLESTONE,
|
||||
MOSSY_COBBLESTONE,
|
||||
DEEPSLATE_BRICKS,
|
||||
POLISHED_ANDESITE,
|
||||
ANDESITE,
|
||||
SMOOTH_STONE,
|
||||
BRICK,
|
||||
];
|
||||
castle_wall_options[rng.gen_range(0..castle_wall_options.len())]
|
||||
}
|
||||
|
||||
@@ -8,26 +8,32 @@ pub fn bresenham_line(
|
||||
y2: i32,
|
||||
z2: i32,
|
||||
) -> Vec<(i32, i32, i32)> {
|
||||
let mut points: Vec<(i32, i32, i32)> = Vec::new();
|
||||
// Calculate max possible points needed
|
||||
let dx = if x2 > x1 { x2 - x1 } else { x1 - x2 };
|
||||
let dy = if y2 > y1 { y2 - y1 } else { y1 - y2 };
|
||||
let dz = if z2 > z1 { z2 - z1 } else { z1 - z2 };
|
||||
|
||||
let dx: i32 = (x2 - x1).abs();
|
||||
let dy: i32 = (y2 - y1).abs();
|
||||
let dz: i32 = (z2 - z1).abs();
|
||||
// Pre-allocate vector with exact size needed
|
||||
let capacity = dx.max(dy).max(dz) + 1;
|
||||
let mut points = Vec::with_capacity(capacity as usize);
|
||||
points.reserve_exact(capacity as usize);
|
||||
|
||||
let xs: i32 = if x1 < x2 { 1 } else { -1 };
|
||||
let ys: i32 = if y1 < y2 { 1 } else { -1 };
|
||||
let zs: i32 = if z1 < z2 { 1 } else { -1 };
|
||||
let xs = if x1 < x2 { 1 } else { -1 };
|
||||
let ys = if y1 < y2 { 1 } else { -1 };
|
||||
let zs = if z1 < z2 { 1 } else { -1 };
|
||||
|
||||
let mut x: i32 = x1;
|
||||
let mut y: i32 = y1;
|
||||
let mut z: i32 = z1;
|
||||
let mut x = x1;
|
||||
let mut y = y1;
|
||||
let mut z = z1;
|
||||
|
||||
// Determine dominant axis once, outside the loop
|
||||
if dx >= dy && dx >= dz {
|
||||
let mut p1: i32 = 2 * dy - dx;
|
||||
let mut p2: i32 = 2 * dz - dx;
|
||||
let mut p1 = 2 * dy - dx;
|
||||
let mut p2 = 2 * dz - dx;
|
||||
|
||||
while x != x2 {
|
||||
points.push((x, y, z));
|
||||
|
||||
if p1 >= 0 {
|
||||
y += ys;
|
||||
p1 -= 2 * dx;
|
||||
@@ -41,11 +47,12 @@ pub fn bresenham_line(
|
||||
x += xs;
|
||||
}
|
||||
} else if dy >= dx && dy >= dz {
|
||||
let mut p1: i32 = 2 * dx - dy;
|
||||
let mut p2: i32 = 2 * dz - dy;
|
||||
let mut p1 = 2 * dx - dy;
|
||||
let mut p2 = 2 * dz - dy;
|
||||
|
||||
while y != y2 {
|
||||
points.push((x, y, z));
|
||||
|
||||
if p1 >= 0 {
|
||||
x += xs;
|
||||
p1 -= 2 * dy;
|
||||
@@ -59,11 +66,12 @@ pub fn bresenham_line(
|
||||
y += ys;
|
||||
}
|
||||
} else {
|
||||
let mut p1: i32 = 2 * dy - dz;
|
||||
let mut p2: i32 = 2 * dx - dz;
|
||||
let mut p1 = 2 * dy - dz;
|
||||
let mut p2 = 2 * dx - dz;
|
||||
|
||||
while z != z2 {
|
||||
points.push((x, y, z));
|
||||
|
||||
if p1 >= 0 {
|
||||
y += ys;
|
||||
p1 -= 2 * dz;
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -32,16 +32,16 @@ fn full_hex_color_to_rgb_tuple(text: &str) -> Option<RGBTuple> {
|
||||
fn short_hex_color_to_rgb_tuple(text: &str) -> Option<RGBTuple> {
|
||||
if text.len() != 4
|
||||
|| !text.starts_with("#")
|
||||
|| text.chars().skip(1).all(|c: char| c.is_ascii_hexdigit())
|
||||
|| !text.chars().skip(1).all(|c: char| c.is_ascii_hexdigit())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let r: u8 = u8::from_str_radix(&text[1..2], 16).unwrap();
|
||||
let r: u8 = r | r << 4;
|
||||
let r: u8 = r | (r << 4);
|
||||
let g: u8 = u8::from_str_radix(&text[2..3], 16).unwrap();
|
||||
let g: u8 = g | g << 4;
|
||||
let g: u8 = g | (g << 4);
|
||||
let b: u8 = u8::from_str_radix(&text[3..4], 16).unwrap();
|
||||
let b: u8 = b | b << 4;
|
||||
let b: u8 = b | (b << 4);
|
||||
Some((r, g, b))
|
||||
}
|
||||
|
||||
|
||||
7
src/coordinate_system/cartesian/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod xzbbox;
|
||||
mod xzpoint;
|
||||
mod xzvector;
|
||||
|
||||
pub use xzbbox::XZBBox;
|
||||
pub use xzpoint::XZPoint;
|
||||
pub use xzvector::XZVector;
|
||||
4
src/coordinate_system/cartesian/xzbbox/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod rectangle;
|
||||
mod xzbbox_enum;
|
||||
|
||||
pub use xzbbox_enum::XZBBox;
|
||||
112
src/coordinate_system/cartesian/xzbbox/rectangle.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::coordinate_system::cartesian::{XZPoint, XZVector};
|
||||
use std::fmt;
|
||||
use std::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
/// An underlying shape of XZBBox enum.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct XZBBoxRect {
|
||||
/// The "bottom-left" vertex of the rectangle
|
||||
min: XZPoint,
|
||||
|
||||
/// The "top-right" vertex of the rectangle
|
||||
max: XZPoint,
|
||||
}
|
||||
|
||||
impl XZBBoxRect {
|
||||
pub fn new(min: XZPoint, max: XZPoint) -> Result<Self, String> {
|
||||
let blockx_ge_1 = max.x - min.x >= 0;
|
||||
let blockz_ge_1 = max.z - min.z >= 0;
|
||||
|
||||
if !blockx_ge_1 {
|
||||
return Err(format!(
|
||||
"Invalid XZBBox::Rect: max.x should >= min.x, but encountered {} -> {}",
|
||||
min.x, max.x
|
||||
));
|
||||
}
|
||||
|
||||
if !blockz_ge_1 {
|
||||
return Err(format!(
|
||||
"Invalid XZBBox::Rect: max.z should >= min.z, but encountered {} -> {}",
|
||||
min.z, max.z
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self { min, max })
|
||||
}
|
||||
|
||||
pub fn min(&self) -> XZPoint {
|
||||
self.min
|
||||
}
|
||||
|
||||
pub fn max(&self) -> XZPoint {
|
||||
self.max
|
||||
}
|
||||
|
||||
/// Total number of blocks covered in this 2D bbox
|
||||
pub fn total_blocks(&self) -> u64 {
|
||||
(self.total_blocks_x() as u64) * (self.total_blocks_z() as u64)
|
||||
}
|
||||
|
||||
/// Total number of blocks covered in x direction
|
||||
pub fn total_blocks_x(&self) -> u32 {
|
||||
let nx = self.max.x - self.min.x + 1;
|
||||
nx as u32
|
||||
}
|
||||
|
||||
/// Total number of blocks covered in z direction
|
||||
pub fn total_blocks_z(&self) -> u32 {
|
||||
let nz = self.max.z - self.min.z + 1;
|
||||
nz as u32
|
||||
}
|
||||
|
||||
/// Check whether an XZPoint is covered
|
||||
pub fn contains(&self, xzpoint: &XZPoint) -> bool {
|
||||
xzpoint.x >= self.min.x
|
||||
&& xzpoint.x <= self.max.x
|
||||
&& xzpoint.z >= self.min.z
|
||||
&& xzpoint.z <= self.max.z
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for XZBBoxRect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Rect({} -> {})", self.min, self.max)
|
||||
}
|
||||
}
|
||||
|
||||
// below are associated +- operators
|
||||
impl Add<XZVector> for XZBBoxRect {
|
||||
type Output = XZBBoxRect;
|
||||
|
||||
fn add(self, other: XZVector) -> Self {
|
||||
Self {
|
||||
min: self.min + other,
|
||||
max: self.max + other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<XZVector> for XZBBoxRect {
|
||||
fn add_assign(&mut self, other: XZVector) {
|
||||
self.min += other;
|
||||
self.max += other;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<XZVector> for XZBBoxRect {
|
||||
type Output = XZBBoxRect;
|
||||
|
||||
fn sub(self, other: XZVector) -> Self {
|
||||
Self {
|
||||
min: self.min - other,
|
||||
max: self.max - other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<XZVector> for XZBBoxRect {
|
||||
fn sub_assign(&mut self, other: XZVector) {
|
||||
self.min -= other;
|
||||
self.max -= other;
|
||||
}
|
||||
}
|
||||
202
src/coordinate_system/cartesian/xzbbox/xzbbox_enum.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use super::rectangle::XZBBoxRect;
|
||||
use crate::coordinate_system::cartesian::{XZPoint, XZVector};
|
||||
use std::fmt;
|
||||
use std::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
/// Bounding Box in minecraft XZ space with varied shapes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum XZBBox {
|
||||
Rect(XZBBoxRect),
|
||||
}
|
||||
|
||||
impl XZBBox {
|
||||
/// Construct rectangle shape bbox from the x and z lengths of the world, originated at (0, 0)
|
||||
pub fn rect_from_xz_lengths(length_x: f64, length_z: f64) -> Result<Self, String> {
|
||||
let lenx_ge_0 = length_x >= 0.0;
|
||||
let lenz_ge_0 = length_z >= 0.0;
|
||||
let lenx_overflow = length_x > i32::MAX as f64;
|
||||
let lenz_overflow = length_z > i32::MAX as f64;
|
||||
|
||||
if !lenx_ge_0 {
|
||||
return Err(format!(
|
||||
"Invalid XZBBox::Rect from xz lengths: length x should >=0 , but encountered {length_x}"
|
||||
));
|
||||
}
|
||||
|
||||
if !lenz_ge_0 {
|
||||
return Err(format!(
|
||||
"Invalid XZBBox::Rect from xz lengths: length z should >=0 , but encountered {length_x}"
|
||||
));
|
||||
}
|
||||
|
||||
if lenx_overflow {
|
||||
return Err(format!(
|
||||
"Invalid XZBBox::Rect from xz lengths: length x too large for i32: {length_x}"
|
||||
));
|
||||
}
|
||||
|
||||
if lenz_overflow {
|
||||
return Err(format!(
|
||||
"Invalid XZBBox::Rect from xz lengths: length z too large for i32: {length_z}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self::Rect(XZBBoxRect::new(
|
||||
XZPoint { x: 0, z: 0 },
|
||||
XZPoint {
|
||||
x: length_x as i32,
|
||||
z: length_z as i32,
|
||||
},
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Check whether an XZPoint is covered
|
||||
pub fn contains(&self, xzpoint: &XZPoint) -> bool {
|
||||
match self {
|
||||
Self::Rect(r) => r.contains(xzpoint),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the circumscribed rectangle of the current XZBBox shape
|
||||
pub fn bounding_rect(&self) -> XZBBoxRect {
|
||||
match self {
|
||||
Self::Rect(r) => *r,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the min x in all covered blocks
|
||||
pub fn min_x(&self) -> i32 {
|
||||
self.bounding_rect().min().x
|
||||
}
|
||||
|
||||
/// Return the max x in all covered blocks
|
||||
pub fn max_x(&self) -> i32 {
|
||||
self.bounding_rect().max().x
|
||||
}
|
||||
|
||||
/// Return the min z in all covered blocks
|
||||
pub fn min_z(&self) -> i32 {
|
||||
self.bounding_rect().min().z
|
||||
}
|
||||
|
||||
/// Return the max z in all covered blocks
|
||||
pub fn max_z(&self) -> i32 {
|
||||
self.bounding_rect().max().z
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for XZBBox {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Rect(r) => write!(f, "XZBBox::{r}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// below are associated +- operators
|
||||
impl Add<XZVector> for XZBBox {
|
||||
type Output = XZBBox;
|
||||
|
||||
fn add(self, other: XZVector) -> XZBBox {
|
||||
match self {
|
||||
Self::Rect(r) => Self::Rect(r + other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<XZVector> for XZBBox {
|
||||
fn add_assign(&mut self, other: XZVector) {
|
||||
match self {
|
||||
Self::Rect(r) => *r += other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<XZVector> for XZBBox {
|
||||
type Output = XZBBox;
|
||||
|
||||
fn sub(self, other: XZVector) -> XZBBox {
|
||||
match self {
|
||||
Self::Rect(r) => Self::Rect(r - other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<XZVector> for XZBBox {
|
||||
fn sub_assign(&mut self, other: XZVector) {
|
||||
match self {
|
||||
Self::Rect(r) => *r -= other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_inputs() {
|
||||
// 2 * 2
|
||||
let obj = XZBBox::rect_from_xz_lengths(1.0, 1.0);
|
||||
assert!(obj.is_ok());
|
||||
let obj = obj.unwrap();
|
||||
assert_eq!(obj.bounding_rect().total_blocks_x(), 2);
|
||||
assert_eq!(obj.bounding_rect().total_blocks_z(), 2);
|
||||
assert_eq!(obj.bounding_rect().total_blocks(), 4);
|
||||
assert_eq!(obj.min_x(), 0);
|
||||
assert_eq!(obj.max_x(), 1);
|
||||
assert_eq!(obj.min_z(), 0);
|
||||
assert_eq!(obj.max_z(), 1);
|
||||
|
||||
// edge cases
|
||||
// 1 * 2
|
||||
let obj = XZBBox::rect_from_xz_lengths(0.0, 1.0);
|
||||
assert!(obj.is_ok());
|
||||
let obj = obj.unwrap();
|
||||
assert_eq!(obj.bounding_rect().total_blocks_x(), 1);
|
||||
assert_eq!(obj.bounding_rect().total_blocks_z(), 2);
|
||||
assert_eq!(obj.bounding_rect().total_blocks(), 2);
|
||||
assert_eq!(obj.min_x(), 0);
|
||||
assert_eq!(obj.max_x(), 0);
|
||||
assert_eq!(obj.min_z(), 0);
|
||||
assert_eq!(obj.max_z(), 1);
|
||||
|
||||
// 2 * 1
|
||||
let obj = XZBBox::rect_from_xz_lengths(1.0, 0.0);
|
||||
assert!(obj.is_ok());
|
||||
let obj = obj.unwrap();
|
||||
assert_eq!(obj.bounding_rect().total_blocks_x(), 2);
|
||||
assert_eq!(obj.bounding_rect().total_blocks_z(), 1);
|
||||
assert_eq!(obj.bounding_rect().total_blocks(), 2);
|
||||
assert_eq!(obj.min_x(), 0);
|
||||
assert_eq!(obj.max_x(), 1);
|
||||
assert_eq!(obj.min_z(), 0);
|
||||
assert_eq!(obj.max_z(), 0);
|
||||
|
||||
// normal case
|
||||
let obj = XZBBox::rect_from_xz_lengths(123.4, 322.5);
|
||||
assert!(obj.is_ok());
|
||||
let obj = obj.unwrap();
|
||||
assert_eq!(obj.bounding_rect().total_blocks_x(), 124);
|
||||
assert_eq!(obj.bounding_rect().total_blocks_z(), 323);
|
||||
assert_eq!(obj.bounding_rect().total_blocks(), 124 * 323);
|
||||
assert_eq!(obj.min_x(), 0);
|
||||
assert_eq!(obj.max_x(), 123);
|
||||
assert_eq!(obj.min_z(), 0);
|
||||
assert_eq!(obj.max_z(), 322);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::excessive_precision)]
|
||||
fn test_invalid_inputs() {
|
||||
assert!(XZBBox::rect_from_xz_lengths(-1.0, 1.5).is_err());
|
||||
assert!(XZBBox::rect_from_xz_lengths(1323.5, -3287238791.395).is_err());
|
||||
assert!(XZBBox::rect_from_xz_lengths(-239928341323.29389498, -3287238791.938395).is_err());
|
||||
assert!(XZBBox::rect_from_xz_lengths(-0.1, 1.5).is_err());
|
||||
assert!(XZBBox::rect_from_xz_lengths(-0.5, 1.5).is_err());
|
||||
assert!(XZBBox::rect_from_xz_lengths(123948761293874123.2398, -0.5).is_err());
|
||||
|
||||
assert!(XZBBox::rect_from_xz_lengths(i32::MAX as f64 + 10.0, -0.5).is_err());
|
||||
assert!(XZBBox::rect_from_xz_lengths(0.2, i32::MAX as f64 + 10.0).is_err());
|
||||
}
|
||||
}
|
||||
70
src/coordinate_system/cartesian/xzpoint.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use super::xzvector::XZVector;
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone, PartialEq)]
|
||||
pub struct XZPoint {
|
||||
pub x: i32,
|
||||
pub z: i32,
|
||||
}
|
||||
|
||||
impl XZPoint {
|
||||
pub fn new(x: i32, z: i32) -> Self {
|
||||
Self { x, z }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for XZPoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "XZPoint({}, {})", self.x, self.z)
|
||||
}
|
||||
}
|
||||
|
||||
// below are associated +- operators
|
||||
impl Add<XZVector> for XZPoint {
|
||||
type Output = XZPoint;
|
||||
|
||||
fn add(self, other: XZVector) -> XZPoint {
|
||||
XZPoint {
|
||||
x: self.x + other.dx,
|
||||
z: self.z + other.dz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<XZVector> for XZPoint {
|
||||
fn add_assign(&mut self, other: XZVector) {
|
||||
self.x += other.dx;
|
||||
self.z += other.dz;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for XZPoint {
|
||||
type Output = XZVector;
|
||||
|
||||
fn sub(self, other: XZPoint) -> XZVector {
|
||||
XZVector {
|
||||
dx: self.x - other.x,
|
||||
dz: self.z - other.z,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<XZVector> for XZPoint {
|
||||
type Output = XZPoint;
|
||||
|
||||
fn sub(self, other: XZVector) -> XZPoint {
|
||||
XZPoint {
|
||||
x: self.x - other.dx,
|
||||
z: self.z - other.dz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<XZVector> for XZPoint {
|
||||
fn sub_assign(&mut self, other: XZVector) {
|
||||
self.x -= other.dx;
|
||||
self.z -= other.dz;
|
||||
}
|
||||
}
|
||||
56
src/coordinate_system/cartesian/xzvector.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
/// Vector between two points in minecraft xz space.
|
||||
#[derive(Debug, Deserialize, Copy, Clone, PartialEq)]
|
||||
pub struct XZVector {
|
||||
/// Increment in x direction
|
||||
pub dx: i32,
|
||||
|
||||
/// Increment in z direction
|
||||
pub dz: i32,
|
||||
}
|
||||
|
||||
impl fmt::Display for XZVector {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "XZVector({}, {})", self.dx, self.dz)
|
||||
}
|
||||
}
|
||||
|
||||
// below are associated +- operators
|
||||
impl Add for XZVector {
|
||||
type Output = XZVector;
|
||||
|
||||
fn add(self, other: XZVector) -> XZVector {
|
||||
XZVector {
|
||||
dx: self.dx + other.dx,
|
||||
dz: self.dz + other.dz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for XZVector {
|
||||
fn add_assign(&mut self, other: XZVector) {
|
||||
self.dx += other.dx;
|
||||
self.dz += other.dz;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for XZVector {
|
||||
type Output = XZVector;
|
||||
|
||||
fn sub(self, other: XZVector) -> XZVector {
|
||||
XZVector {
|
||||
dx: self.dx - other.dx,
|
||||
dz: self.dz - other.dz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign for XZVector {
|
||||
fn sub_assign(&mut self, other: XZVector) {
|
||||
self.dx -= other.dx;
|
||||
self.dz -= other.dz;
|
||||
}
|
||||
}
|
||||
125
src/coordinate_system/geographic/llbbox.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use super::llpoint::LLPoint;
|
||||
|
||||
/// A checked Bounding Box.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct LLBBox {
|
||||
/// The "bottom-left" vertex of the rectangle
|
||||
min: LLPoint,
|
||||
|
||||
/// The "top-right" vertex of the rectangle
|
||||
max: LLPoint,
|
||||
}
|
||||
|
||||
impl LLBBox {
|
||||
pub fn new(min_lat: f64, min_lng: f64, max_lat: f64, max_lng: f64) -> Result<Self, String> {
|
||||
if min_lng >= max_lng {
|
||||
return Err(format!(
|
||||
"Invalid LLBBox: min_lng {min_lng} >= max_lng {max_lng}"
|
||||
));
|
||||
}
|
||||
if min_lat >= max_lat {
|
||||
return Err(format!(
|
||||
"Invalid LLBBox: min_lat {min_lat} >= max_lat {max_lat}"
|
||||
));
|
||||
}
|
||||
|
||||
let min = LLPoint::new(min_lat, min_lng)?;
|
||||
let max = LLPoint::new(max_lat, max_lng)?;
|
||||
|
||||
Ok(Self { min, max })
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Result<Self, String> {
|
||||
let [min_lat, min_lng, max_lat, max_lng]: [f64; 4] = s
|
||||
.split([',', ' '])
|
||||
.map(|e| e.parse().unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
// So, the GUI does Lat/Lng and no GDAL (comma-sep values), which is the exact opposite of
|
||||
// what bboxfinder.com does. :facepalm: (bboxfinder is wrong here: Lat comes first!)
|
||||
// DO NOT MODIFY THIS! It's correct. The CLI/GUI is passing you the numbers incorrectly.
|
||||
Self::new(min_lat, min_lng, max_lat, max_lng)
|
||||
}
|
||||
|
||||
pub fn min(&self) -> LLPoint {
|
||||
self.min
|
||||
}
|
||||
|
||||
pub fn max(&self) -> LLPoint {
|
||||
self.max
|
||||
}
|
||||
|
||||
pub fn contains(&self, llpoint: &LLPoint) -> bool {
|
||||
llpoint.lat() >= self.min().lat()
|
||||
&& llpoint.lat() <= self.max().lat()
|
||||
&& llpoint.lng() >= self.min().lng()
|
||||
&& llpoint.lng() <= self.max().lng()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_input() {
|
||||
assert!(LLBBox::new(0., 0., 1., 1.).is_ok());
|
||||
|
||||
assert!(LLBBox::new(1., 2., 3., 4.).is_ok());
|
||||
|
||||
// Arnis, Germany
|
||||
assert!(LLBBox::new(54.627053, 9.927928, 54.634902, 9.937563).is_ok());
|
||||
|
||||
// Royal Observatory Greenwich, London, UK
|
||||
assert!(LLBBox::new(51.470000, -0.015000, 51.480000, 0.015000).is_ok());
|
||||
|
||||
// The Bund, Shanghai, China
|
||||
assert!(LLBBox::new(31.23256, 121.46768, 31.24993, 121.50394).is_ok());
|
||||
|
||||
// Santa Monica, Los Angeles, US
|
||||
assert!(LLBBox::new(34.00348, -118.51226, 34.02033, -118.47600).is_ok());
|
||||
|
||||
// Sydney Opera House, Sydney, Australia
|
||||
assert!(LLBBox::new(-33.861035, 151.204137, -33.852597, 151.222268).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_commas() {
|
||||
const ARNIS_STR: &str = "9.927928,54.627053,9.937563,54.634902";
|
||||
|
||||
let bbox_result = LLBBox::from_str(ARNIS_STR);
|
||||
assert!(bbox_result.is_ok());
|
||||
|
||||
let arnis_correct: LLBBox = LLBBox {
|
||||
min: LLPoint::new(9.927928, 54.627053).unwrap(),
|
||||
max: LLPoint::new(9.937563, 54.634902).unwrap(),
|
||||
};
|
||||
|
||||
assert_eq!(bbox_result.unwrap(), arnis_correct);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_spaces() {
|
||||
const ARNIS_SPACE_STR: &str = "9.927928 54.627053 9.937563 54.634902";
|
||||
|
||||
let bbox_result = LLBBox::from_str(ARNIS_SPACE_STR);
|
||||
assert!(bbox_result.is_ok());
|
||||
|
||||
let arnis_correct: LLBBox = LLBBox {
|
||||
min: LLPoint::new(9.927928, 54.627053).unwrap(),
|
||||
max: LLPoint::new(9.937563, 54.634902).unwrap(),
|
||||
};
|
||||
|
||||
assert_eq!(bbox_result.unwrap(), arnis_correct);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_order() {
|
||||
// Violates values in vals_in_order
|
||||
assert!(LLBBox::new(0., 0., 0., 0.).is_err());
|
||||
assert!(LLBBox::new(1., 0., 0., 1.).is_err());
|
||||
assert!(LLBBox::new(0., 1., 1., 0.).is_err());
|
||||
}
|
||||
}
|
||||
60
src/coordinate_system/geographic/llpoint.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Bounds-checked longitude and latitude.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct LLPoint {
|
||||
lat: f64,
|
||||
lng: f64,
|
||||
}
|
||||
|
||||
impl LLPoint {
|
||||
pub fn new(lat: f64, lng: f64) -> Result<Self, String> {
|
||||
let lat_in_range = (-90.0..=90.0).contains(&lat);
|
||||
let lng_in_range = (-180.0..=180.0).contains(&lng);
|
||||
|
||||
if !lat_in_range {
|
||||
return Err(format!("Latitude {lat} not in range -90.0..=90.0"));
|
||||
}
|
||||
|
||||
if !lng_in_range {
|
||||
return Err(format!("Longitude {lng} not in range -180.0..=180.0"));
|
||||
}
|
||||
|
||||
Ok(Self { lat, lng })
|
||||
}
|
||||
|
||||
pub fn lat(&self) -> f64 {
|
||||
self.lat
|
||||
}
|
||||
|
||||
pub fn lng(&self) -> f64 {
|
||||
self.lng
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_input() {
|
||||
assert!(LLPoint::new(0., 0.).is_ok());
|
||||
|
||||
// latitude extremes
|
||||
assert!(LLPoint::new(-90.0, 0.).is_ok());
|
||||
assert!(LLPoint::new(90.0, 0.).is_ok());
|
||||
|
||||
// longitude extremes
|
||||
assert!(LLPoint::new(0., -180.0).is_ok());
|
||||
assert!(LLPoint::new(0., 180.0).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_bounds() {
|
||||
// latitude out-of-bounds
|
||||
assert!(LLPoint::new(-91., 0.).is_err());
|
||||
assert!(LLPoint::new(91., 0.).is_err());
|
||||
|
||||
// longitude out-of-bounds
|
||||
assert!(LLPoint::new(0., -181.).is_err());
|
||||
assert!(LLPoint::new(0., 181.).is_err());
|
||||
}
|
||||
}
|
||||
5
src/coordinate_system/geographic/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod llbbox;
|
||||
mod llpoint;
|
||||
|
||||
pub use llbbox::LLBBox;
|
||||
pub use llpoint::LLPoint;
|
||||
3
src/coordinate_system/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod cartesian;
|
||||
pub mod geographic;
|
||||
pub mod transformation;
|
||||
185
src/coordinate_system/transformation.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use super::cartesian::{XZBBox, XZPoint};
|
||||
use super::geographic::{LLBBox, LLPoint};
|
||||
|
||||
/// Transform geographic space (within llbbox) to a local tangential cartesian space (within xzbbox)
|
||||
pub struct CoordTransformer {
|
||||
len_lat: f64,
|
||||
len_lng: f64,
|
||||
scale_factor_x: f64,
|
||||
scale_factor_z: f64,
|
||||
min_lat: f64,
|
||||
min_lng: f64,
|
||||
}
|
||||
|
||||
impl CoordTransformer {
|
||||
pub fn scale_factor_x(&self) -> f64 {
|
||||
self.scale_factor_x
|
||||
}
|
||||
|
||||
pub fn scale_factor_z(&self) -> f64 {
|
||||
self.scale_factor_z
|
||||
}
|
||||
|
||||
pub fn llbbox_to_xzbbox(
|
||||
llbbox: &LLBBox,
|
||||
scale: f64,
|
||||
) -> Result<(CoordTransformer, XZBBox), String> {
|
||||
let err_header = "Construct LLBBox to XZBBox transformation failed".to_string();
|
||||
|
||||
if scale <= 0.0 {
|
||||
return Err(format!("{}: scale <= 0.0", &err_header));
|
||||
}
|
||||
|
||||
let (scale_factor_z, scale_factor_x) = geo_distance(llbbox.min(), llbbox.max());
|
||||
let scale_factor_z: f64 = scale_factor_z.floor() * scale;
|
||||
let scale_factor_x: f64 = scale_factor_x.floor() * scale;
|
||||
|
||||
let xzbbox = XZBBox::rect_from_xz_lengths(scale_factor_x, scale_factor_z)
|
||||
.map_err(|e| format!("{}:\n{}", &err_header, e))?;
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
len_lat: llbbox.max().lat() - llbbox.min().lat(),
|
||||
len_lng: llbbox.max().lng() - llbbox.min().lng(),
|
||||
scale_factor_x,
|
||||
scale_factor_z,
|
||||
min_lat: llbbox.min().lat(),
|
||||
min_lng: llbbox.min().lng(),
|
||||
},
|
||||
xzbbox,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn transform_point(&self, llpoint: LLPoint) -> XZPoint {
|
||||
// Calculate the relative position within the bounding box
|
||||
let rel_x: f64 = (llpoint.lng() - self.min_lng) / self.len_lng;
|
||||
let rel_z: f64 = 1.0 - (llpoint.lat() - self.min_lat) / self.len_lat;
|
||||
|
||||
// Apply scaling factors for each dimension and convert to Minecraft coordinates
|
||||
let x: i32 = (rel_x * self.scale_factor_x) as i32;
|
||||
let z: i32 = (rel_z * self.scale_factor_z) as i32;
|
||||
|
||||
XZPoint::new(x, z)
|
||||
}
|
||||
}
|
||||
|
||||
// (lat meters, lon meters)
|
||||
#[inline]
|
||||
pub fn geo_distance(a: LLPoint, b: LLPoint) -> (f64, f64) {
|
||||
let z: f64 = lat_distance(a.lat(), b.lat());
|
||||
|
||||
// distance between two lons depends on their latitude. In this case we'll just average them
|
||||
let x: f64 = lon_distance((a.lat() + b.lat()) / 2.0, a.lng(), b.lng());
|
||||
|
||||
(z, x)
|
||||
}
|
||||
|
||||
// Haversine but optimized for a latitude delta of 0
|
||||
// returns meters
|
||||
fn lon_distance(lat: f64, lon1: f64, lon2: f64) -> f64 {
|
||||
const R: f64 = 6_371_000.0;
|
||||
let d_lon: f64 = (lon2 - lon1).to_radians();
|
||||
let a: f64 =
|
||||
lat.to_radians().cos() * lat.to_radians().cos() * (d_lon / 2.0).sin() * (d_lon / 2.0).sin();
|
||||
let c: f64 = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
|
||||
|
||||
R * c
|
||||
}
|
||||
|
||||
// Haversine but optimized for a longitude delta of 0
|
||||
// returns meters
|
||||
fn lat_distance(lat1: f64, lat2: f64) -> f64 {
|
||||
const R: f64 = 6_371_000.0;
|
||||
let d_lat: f64 = (lat2 - lat1).to_radians();
|
||||
let a: f64 = (d_lat / 2.0).sin() * (d_lat / 2.0).sin();
|
||||
let c: f64 = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
|
||||
|
||||
R * c
|
||||
}
|
||||
|
||||
// copied legacy code
|
||||
// Function to convert latitude and longitude to Minecraft coordinates.
|
||||
// Function to convert latitude and longitude to Minecraft coordinates.
|
||||
#[cfg(test)]
|
||||
pub fn lat_lon_to_minecraft_coords(
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
bbox: LLBBox, // (min_lon, min_lat, max_lon, max_lat)
|
||||
scale_factor_z: f64,
|
||||
scale_factor_x: f64,
|
||||
) -> (i32, i32) {
|
||||
// Calculate the relative position within the bounding box
|
||||
let rel_x: f64 = (lon - bbox.min().lng()) / (bbox.max().lng() - bbox.min().lng());
|
||||
let rel_z: f64 = 1.0 - (lat - bbox.min().lat()) / (bbox.max().lat() - bbox.min().lat());
|
||||
|
||||
// Apply scaling factors for each dimension and convert to Minecraft coordinates
|
||||
let x: i32 = (rel_x * scale_factor_x) as i32;
|
||||
let z: i32 = (rel_z * scale_factor_z) as i32;
|
||||
|
||||
(x, z)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::test_utilities::get_llbbox_arnis;
|
||||
|
||||
fn test_llxztransform_one_scale_one_factor(
|
||||
scale: f64,
|
||||
test_latfactor: f64,
|
||||
test_lngfactor: f64,
|
||||
) {
|
||||
let llbbox = get_llbbox_arnis();
|
||||
let llpoint = LLPoint::new(
|
||||
llbbox.min().lat() + (llbbox.max().lat() - llbbox.min().lat()) * test_latfactor,
|
||||
llbbox.min().lng() + (llbbox.max().lng() - llbbox.min().lng()) * test_lngfactor,
|
||||
)
|
||||
.unwrap();
|
||||
let (transformer, xzbbox_new) = CoordTransformer::llbbox_to_xzbbox(&llbbox, scale).unwrap();
|
||||
|
||||
// legacy xzbbox creation
|
||||
let (scale_factor_z, scale_factor_x) = geo_distance(llbbox.min(), llbbox.max());
|
||||
let scale_factor_z: f64 = scale_factor_z.floor() * scale;
|
||||
let scale_factor_x: f64 = scale_factor_x.floor() * scale;
|
||||
let xzbbox_old = XZBBox::rect_from_xz_lengths(scale_factor_x, scale_factor_z).unwrap();
|
||||
|
||||
// legacy coord transform
|
||||
let (x, z) = lat_lon_to_minecraft_coords(
|
||||
llpoint.lat(),
|
||||
llpoint.lng(),
|
||||
llbbox,
|
||||
scale_factor_z,
|
||||
scale_factor_x,
|
||||
);
|
||||
// new coord transform
|
||||
let xzpoint = transformer.transform_point(llpoint);
|
||||
|
||||
assert_eq!(x, xzpoint.x);
|
||||
assert_eq!(z, xzpoint.z);
|
||||
assert_eq!(xzbbox_new.min_x(), xzbbox_old.min_x());
|
||||
assert_eq!(xzbbox_new.max_x(), xzbbox_old.max_x());
|
||||
assert_eq!(xzbbox_new.min_z(), xzbbox_old.min_z());
|
||||
assert_eq!(xzbbox_new.max_z(), xzbbox_old.max_z());
|
||||
}
|
||||
|
||||
// this ensures that transformer.transform_point == legacy lat_lon_to_minecraft_coords
|
||||
#[test]
|
||||
pub fn test_llxztransform() {
|
||||
test_llxztransform_one_scale_one_factor(1.0, 0.5, 0.5);
|
||||
test_llxztransform_one_scale_one_factor(3.0, 0.1, 0.2);
|
||||
test_llxztransform_one_scale_one_factor(10.0, -1.2, 2.0);
|
||||
test_llxztransform_one_scale_one_factor(0.4, 0.3, -0.2);
|
||||
test_llxztransform_one_scale_one_factor(0.1, 0.2, 0.7);
|
||||
}
|
||||
|
||||
// this ensures that invalid inputs can be handled correctly
|
||||
#[test]
|
||||
pub fn test_invalid_construct() {
|
||||
let llbbox = get_llbbox_arnis();
|
||||
let obj = CoordTransformer::llbbox_to_xzbbox(&llbbox, 0.0);
|
||||
assert!(obj.is_err());
|
||||
|
||||
let obj = CoordTransformer::llbbox_to_xzbbox(&llbbox, -1.2);
|
||||
assert!(obj.is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,466 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::{DIRT, GRASS_BLOCK, SNOW_BLOCK};
|
||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::element_processing::*;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::ground::Ground;
|
||||
use crate::map_renderer;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use crate::parallel_processing::{
|
||||
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 indicatif::{ProgressBar, ProgressStyle};
|
||||
use rayon::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const MIN_Y: i32 = -64;
|
||||
|
||||
/// Generation options that can be passed separately from CLI Args
|
||||
#[derive(Clone)]
|
||||
pub struct GenerationOptions {
|
||||
pub path: PathBuf,
|
||||
pub format: WorldFormat,
|
||||
pub level_name: Option<String>,
|
||||
pub spawn_point: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
pub fn generate_world(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
scale_factor_x: f64,
|
||||
scale_factor_z: f64,
|
||||
) -> Result<(), String> {
|
||||
println!("{} Processing data...", "[3/5]".bold());
|
||||
emit_gui_progress_update(10.0, "Processing data...");
|
||||
// Default to Java format when called from CLI
|
||||
let options = GenerationOptions {
|
||||
path: args.path.clone(),
|
||||
format: WorldFormat::JavaAnvil,
|
||||
level_name: None,
|
||||
spawn_point: None,
|
||||
};
|
||||
|
||||
let ground_level: i32 = args.ground_level;
|
||||
let region_dir: String = format!("{}/region", args.path);
|
||||
let mut editor: WorldEditor =
|
||||
WorldEditor::new(®ion_dir, scale_factor_x, scale_factor_z, args);
|
||||
// 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()
|
||||
};
|
||||
|
||||
editor.set_sign(
|
||||
"↑".to_string(),
|
||||
"Generated World".to_string(),
|
||||
"This direction".to_string(),
|
||||
"".to_string(),
|
||||
9,
|
||||
-61,
|
||||
9,
|
||||
6,
|
||||
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options, parallel_config)
|
||||
.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());
|
||||
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
|
||||
let elements_count: usize = elements.len();
|
||||
let mut elements = elements; // Take ownership for consuming
|
||||
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
||||
process_pb.set_style(ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"));
|
||||
|
||||
let progress_increment_prcs: f64 = 50.0 / elements_count as f64;
|
||||
let mut current_progress_prcs: f64 = 10.0;
|
||||
let progress_increment_prcs: f64 = 45.0 / elements_count as f64;
|
||||
let mut current_progress_prcs: f64 = 25.0;
|
||||
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);
|
||||
current_progress_prcs += progress_increment_prcs;
|
||||
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
||||
@@ -62,50 +478,98 @@ pub fn generate_world(
|
||||
process_pb.set_message("");
|
||||
}
|
||||
|
||||
match element {
|
||||
match &element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
buildings::generate_buildings(&mut editor, way, ground_level, args, None);
|
||||
buildings::generate_buildings(&mut editor, way, args, None, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, ground_level, args);
|
||||
highways::generate_highways(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&highway_connectivity,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
} else if way.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse(&mut editor, way, ground_level, args);
|
||||
landuse::generate_landuse(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("natural") {
|
||||
natural::generate_natural(&mut editor, element, ground_level, args);
|
||||
natural::generate_natural(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(&mut editor, element, ground_level, args);
|
||||
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("leisure") {
|
||||
leisure::generate_leisure(&mut editor, way, ground_level, args);
|
||||
leisure::generate_leisure(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("barrier") {
|
||||
barriers::generate_barriers(&mut editor, element, ground_level);
|
||||
} else if way.tags.contains_key("waterway") {
|
||||
waterways::generate_waterways(&mut editor, way, ground_level);
|
||||
barriers::generate_barriers(&mut editor, &element);
|
||||
} else if let Some(val) = way.tags.get("waterway") {
|
||||
if val == "dock" {
|
||||
// docks count as water areas
|
||||
water_areas::generate_water_area_from_way(&mut editor, way, &xzbbox);
|
||||
} else {
|
||||
waterways::generate_waterways(&mut editor, way);
|
||||
}
|
||||
} else if way.tags.contains_key("bridge") {
|
||||
//bridges::generate_bridges(&mut editor, way, ground_level); // TODO FIX
|
||||
} else if way.tags.contains_key("railway") {
|
||||
railways::generate_railways(&mut editor, way, ground_level);
|
||||
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")
|
||||
{
|
||||
highways::generate_aeroway(&mut editor, way, ground_level);
|
||||
highways::generate_aeroway(&mut editor, way, args);
|
||||
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
||||
highways::generate_siding(&mut editor, way, ground_level);
|
||||
highways::generate_siding(&mut editor, way);
|
||||
} else if way.tags.contains_key("man_made") {
|
||||
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) => {
|
||||
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
|
||||
doors::generate_doors(&mut editor, node, ground_level);
|
||||
doors::generate_doors(&mut editor, node);
|
||||
} else if node.tags.contains_key("natural")
|
||||
&& node.tags.get("natural") == Some(&"tree".to_string())
|
||||
{
|
||||
natural::generate_natural(&mut editor, element, ground_level, args);
|
||||
natural::generate_natural(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if node.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(&mut editor, element, ground_level, args);
|
||||
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if node.tags.contains_key("barrier") {
|
||||
barriers::generate_barriers(&mut editor, element, ground_level);
|
||||
barriers::generate_barrier_nodes(&mut editor, node);
|
||||
} else if node.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, ground_level, args);
|
||||
highways::generate_highways(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&highway_connectivity,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
} else if node.tags.contains_key("tourism") {
|
||||
tourisms::generate_tourisms(&mut editor, node, ground_level);
|
||||
tourisms::generate_tourisms(&mut editor, node);
|
||||
} else if node.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made_nodes(&mut editor, node);
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
@@ -113,27 +577,65 @@ pub fn generate_world(
|
||||
buildings::generate_building_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
ground_level,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
} else if rel.tags.contains_key("water") {
|
||||
water_areas::generate_water_areas(&mut editor, rel, ground_level);
|
||||
} 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();
|
||||
|
||||
// Drop remaining caches
|
||||
drop(highway_connectivity);
|
||||
drop(flood_fill_cache);
|
||||
|
||||
// Generate ground layer
|
||||
let total_blocks: u64 = (scale_factor_x as i32 + 1) as u64 * (scale_factor_z as i32 + 1) as u64;
|
||||
let total_blocks: u64 = xzbbox.bounding_rect().total_blocks();
|
||||
let desired_updates: u64 = 1500;
|
||||
let batch_size: u64 = (total_blocks / desired_updates).max(1);
|
||||
|
||||
let mut block_counter: u64 = 0;
|
||||
|
||||
println!("{} Generating ground layer...", "[4/5]".bold());
|
||||
emit_gui_progress_update(60.0, "Generating ground layer...");
|
||||
println!("{} Generating ground...", "[6/7]".bold());
|
||||
emit_gui_progress_update(70.0, "Generating ground...");
|
||||
|
||||
let ground_pb: ProgressBar = ProgressBar::new(total_blocks);
|
||||
ground_pb.set_style(
|
||||
@@ -143,38 +645,202 @@ pub fn generate_world(
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
|
||||
let mut gui_progress_grnd: f64 = 60.0;
|
||||
let mut gui_progress_grnd: f64 = 70.0;
|
||||
let mut last_emitted_progress: f64 = gui_progress_grnd;
|
||||
let total_iterations_grnd: f64 = (scale_factor_x + 1.0) * (scale_factor_z + 1.0);
|
||||
let progress_increment_grnd: f64 = 30.0 / total_iterations_grnd;
|
||||
let total_iterations_grnd: f64 = total_blocks as f64;
|
||||
let progress_increment_grnd: f64 = 20.0 / total_iterations_grnd;
|
||||
|
||||
let groundlayer_block = if args.winter { SNOW_BLOCK } else { 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 0..=(scale_factor_x as i32) {
|
||||
for z in 0..=(scale_factor_z as i32) {
|
||||
editor.set_block(groundlayer_block, x, ground_level, z, None, None);
|
||||
editor.set_block(DIRT, x, ground_level - 1, z, None, None);
|
||||
// Process ground generation chunk-by-chunk for better cache locality.
|
||||
// This keeps the same region/chunk HashMap entries hot in CPU cache,
|
||||
// rather than jumping between regions on every Z iteration.
|
||||
let min_chunk_x = xzbbox.min_x() >> 4;
|
||||
let max_chunk_x = xzbbox.max_x() >> 4;
|
||||
let min_chunk_z = xzbbox.min_z() >> 4;
|
||||
let max_chunk_z = xzbbox.max_z() >> 4;
|
||||
|
||||
block_counter += 1;
|
||||
if block_counter % batch_size == 0 {
|
||||
ground_pb.inc(batch_size);
|
||||
}
|
||||
for chunk_x in min_chunk_x..=max_chunk_x {
|
||||
for chunk_z in min_chunk_z..=max_chunk_z {
|
||||
// Calculate the block range for this chunk, clamped to bbox
|
||||
let chunk_min_x = (chunk_x << 4).max(xzbbox.min_x());
|
||||
let chunk_max_x = ((chunk_x << 4) + 15).min(xzbbox.max_x());
|
||||
let chunk_min_z = (chunk_z << 4).max(xzbbox.min_z());
|
||||
let chunk_max_z = ((chunk_z << 4) + 15).min(xzbbox.max_z());
|
||||
|
||||
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;
|
||||
for x in chunk_min_x..=chunk_max_x {
|
||||
for z in chunk_min_z..=chunk_max_z {
|
||||
// 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
|
||||
};
|
||||
|
||||
// Add default dirt and grass layer if there isn't a stone layer already
|
||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
||||
}
|
||||
|
||||
// Fill underground with stone
|
||||
if 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set sign for player orientation
|
||||
/*editor.set_sign(
|
||||
"↑".to_string(),
|
||||
"Generated World".to_string(),
|
||||
"This direction".to_string(),
|
||||
"".to_string(),
|
||||
9,
|
||||
-61,
|
||||
9,
|
||||
6,
|
||||
);*/
|
||||
|
||||
ground_pb.inc(block_counter % batch_size);
|
||||
ground_pb.finish();
|
||||
|
||||
// Save world
|
||||
editor.save();
|
||||
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
Ok(())
|
||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
||||
|
||||
// Update player spawn Y coordinate based on terrain height after generation
|
||||
#[cfg(feature = "gui")]
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
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!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.min().lng(),
|
||||
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(
|
||||
&args.path,
|
||||
bbox_string,
|
||||
args.scale,
|
||||
ground.as_ref(),
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
}
|
||||
|
||||
// For Bedrock format, emit event to open the mcworld file
|
||||
if world_format == WorldFormat::BedrockMcWorld {
|
||||
if let Some(path_str) = output_path.to_str() {
|
||||
emit_open_mcworld_file(path_str);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Information needed to generate a map preview after world generation is complete
|
||||
#[derive(Clone)]
|
||||
pub struct MapPreviewInfo {
|
||||
pub world_path: PathBuf,
|
||||
pub min_x: i32,
|
||||
pub max_x: i32,
|
||||
pub min_z: i32,
|
||||
pub max_z: i32,
|
||||
pub world_area: i64,
|
||||
}
|
||||
|
||||
impl MapPreviewInfo {
|
||||
/// Create MapPreviewInfo from world bounds
|
||||
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
|
||||
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||
Self {
|
||||
world_path,
|
||||
min_x: xzbbox.min_x(),
|
||||
max_x: xzbbox.max_x(),
|
||||
min_z: xzbbox.min_z(),
|
||||
max_z: xzbbox.max_z(),
|
||||
world_area: world_width * world_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
|
||||
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||
|
||||
/// Start map preview generation in a background thread.
|
||||
/// This should be called AFTER the world generation is complete, the session lock is released,
|
||||
/// and the GUI has been notified of 100% completion.
|
||||
///
|
||||
/// For Java worlds only, and only if the world area is within limits.
|
||||
pub fn start_map_preview_generation(info: MapPreviewInfo) {
|
||||
if info.world_area > MAX_MAP_PREVIEW_AREA {
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(
|
||||
&info.world_path,
|
||||
info.min_x,
|
||||
info.max_x,
|
||||
info.min_z,
|
||||
info.max_z,
|
||||
)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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>());
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_amenities(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
ground_level: i32,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags().get("layer") {
|
||||
@@ -25,75 +29,101 @@ pub fn generate_amenities(
|
||||
}
|
||||
|
||||
if let Some(amenity_type) = element.tags().get("amenity") {
|
||||
let first_node: Option<(i32, i32)> = element
|
||||
let first_node: Option<XZPoint> = element
|
||||
.nodes()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
||||
.next();
|
||||
match amenity_type.as_str() {
|
||||
"waste_disposal" | "waste_basket" => {
|
||||
// Place a cauldron for waste disposal or waste basket
|
||||
if let Some((x, z)) = first_node {
|
||||
editor.set_block(CAULDRON, x, ground_level + 1, z, None, None);
|
||||
if let Some(pt) = first_node {
|
||||
editor.set_block(CAULDRON, pt.x, 1, pt.z, None, None);
|
||||
}
|
||||
}
|
||||
"vending_machine" | "atm" => {
|
||||
if let Some((x, z)) = first_node {
|
||||
editor.set_block(IRON_BLOCK, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(IRON_BLOCK, x, ground_level + 2, z, None, None);
|
||||
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);
|
||||
}
|
||||
}
|
||||
"bicycle_parking" => {
|
||||
let ground_block: Block = OAK_PLANKS;
|
||||
let roof_block: Block = STONE_BLOCK_SLAB;
|
||||
|
||||
let polygon_coords: Vec<(i32, i32)> = element
|
||||
.nodes()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
// Use pre-computed flood fill from cache
|
||||
let floor_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());
|
||||
|
||||
if floor_area.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill the floor area
|
||||
for (x, z) in floor_area.iter() {
|
||||
editor.set_block(ground_block, *x, ground_level, *z, None, None);
|
||||
editor.set_block(ground_block, *x, 0, *z, None, None);
|
||||
}
|
||||
|
||||
// Place fences and roof slabs at each corner node
|
||||
for node in element.nodes() {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
// Set ground block and fences
|
||||
editor.set_block(ground_block, x, 0, z, None, None);
|
||||
for y in 1..=4 {
|
||||
editor.set_block(OAK_FENCE, x, y, z, None, None);
|
||||
}
|
||||
editor.set_block(roof_block, x, 5, z, None, None);
|
||||
}
|
||||
|
||||
// Flood fill the roof area
|
||||
for (x, z) in floor_area.iter() {
|
||||
editor.set_block(roof_block, *x, 5, *z, None, None);
|
||||
}
|
||||
}
|
||||
"bench" => {
|
||||
// Place a bench
|
||||
if let Some(pt) = first_node {
|
||||
// Use deterministic RNG for consistent bench orientation across region boundaries
|
||||
let mut rng = element_rng(element.id());
|
||||
// 50% chance to 90 degrees rotate the bench
|
||||
if rng.gen_bool(0.5) {
|
||||
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);
|
||||
} else {
|
||||
editor.set_block(SMOOTH_STONE, pt.x, 1, pt.z, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x, 1, pt.z + 1, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x, 1, pt.z - 1, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"shelter" => {
|
||||
let roof_block: Block = STONE_BRICK_SLAB;
|
||||
|
||||
// Use pre-computed flood fill from cache
|
||||
let roof_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
|
||||
|
||||
// Place fences and roof slabs at each corner node directly
|
||||
for node in element.nodes() {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
for y in 1..=4 {
|
||||
editor.set_block(ground_block, x, ground_level, z, None, None);
|
||||
editor.set_block(OAK_FENCE, x, ground_level + y, z, None, None);
|
||||
for fence_height in 1..=4 {
|
||||
editor.set_block(OAK_FENCE, x, fence_height, z, None, None);
|
||||
}
|
||||
editor.set_block(roof_block, x, ground_level + 5, z, None, None);
|
||||
editor.set_block(roof_block, x, 5, z, None, None);
|
||||
}
|
||||
|
||||
// Flood fill the roof area
|
||||
let roof_height: i32 = ground_level + 5;
|
||||
for (x, z) in floor_area.iter() {
|
||||
editor.set_block(roof_block, *x, roof_height, *z, None, None);
|
||||
}
|
||||
}
|
||||
"bench" => {
|
||||
// Place a bench
|
||||
if let Some((x, z)) = first_node {
|
||||
editor.set_block(SMOOTH_STONE, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(OAK_LOG, x + 1, ground_level + 1, z, None, None);
|
||||
editor.set_block(OAK_LOG, x - 1, ground_level + 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"vending" => {
|
||||
// Place vending machine blocks
|
||||
if let Some((x, z)) = first_node {
|
||||
editor.set_block(IRON_BLOCK, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(IRON_BLOCK, x, ground_level + 2, z, None, None);
|
||||
for (x, z) in roof_area.iter() {
|
||||
editor.set_block(roof_block, *x, 5, *z, None, None);
|
||||
}
|
||||
}
|
||||
"parking" | "fountain" => {
|
||||
// Process parking or fountain areas
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
let mut previous_node: Option<XZPoint> = None;
|
||||
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
|
||||
let mut current_amenity: Vec<(i32, i32)> = vec![];
|
||||
|
||||
@@ -102,23 +132,16 @@ pub fn generate_amenities(
|
||||
"parking" => GRAY_CONCRETE,
|
||||
_ => GRAY_CONCRETE,
|
||||
};
|
||||
|
||||
for node in element.nodes() {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
let pt: XZPoint = node.xz();
|
||||
|
||||
if let Some(prev) = previous_node {
|
||||
// Create borders for fountain or parking area
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(prev.0, ground_level, prev.1, x, ground_level, z);
|
||||
bresenham_line(prev.x, 0, prev.z, pt.x, 0, pt.z);
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
editor.set_block(
|
||||
block_type,
|
||||
bx,
|
||||
ground_level,
|
||||
bz,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
editor.set_block(block_type, bx, 0, bz, Some(&[BLACK_CONCRETE]), None);
|
||||
|
||||
// Decorative border around fountains
|
||||
if amenity_type == "fountain" {
|
||||
@@ -128,7 +151,7 @@ pub fn generate_amenities(
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
bx + dx,
|
||||
ground_level,
|
||||
0,
|
||||
bz + dz,
|
||||
None,
|
||||
None,
|
||||
@@ -144,7 +167,7 @@ pub fn generate_amenities(
|
||||
corner_addup.2 += 1;
|
||||
}
|
||||
}
|
||||
previous_node = Some((x, z));
|
||||
previous_node = Some(pt);
|
||||
}
|
||||
|
||||
// Flood-fill the interior area for parking or fountains
|
||||
@@ -157,22 +180,81 @@ pub fn generate_amenities(
|
||||
editor.set_block(
|
||||
block_type,
|
||||
x,
|
||||
ground_level,
|
||||
0,
|
||||
z,
|
||||
Some(&[BLACK_CONCRETE, GRAY_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
|
||||
// Add parking spot markings
|
||||
if amenity_type == "parking" && (x + z) % 8 == 0 && (x * z) % 32 != 0 {
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
x,
|
||||
ground_level,
|
||||
z,
|
||||
Some(&[BLACK_CONCRETE, GRAY_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
// Enhanced parking space markings
|
||||
if amenity_type == "parking" {
|
||||
// Create defined parking spaces with realistic layout
|
||||
let space_width = 4; // Width of each parking space
|
||||
let space_length = 6; // Length of each parking space
|
||||
let lane_width = 5; // Width of driving lanes
|
||||
|
||||
// Calculate which "zone" this coordinate falls into
|
||||
let zone_x = x / space_width;
|
||||
let zone_z = z / (space_length + lane_width);
|
||||
let local_x = x % space_width;
|
||||
let local_z = z % (space_length + lane_width);
|
||||
|
||||
// Create parking space boundaries (only within parking areas, not in driving lanes)
|
||||
if local_z < space_length {
|
||||
// We're in a parking space area, not in the driving lane
|
||||
if local_x == 0 {
|
||||
// Vertical parking space lines (only on the left edge)
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
x,
|
||||
0,
|
||||
z,
|
||||
Some(&[BLACK_CONCRETE, GRAY_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else if local_z == 0 {
|
||||
// Horizontal parking space lines (only on the top edge)
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
x,
|
||||
0,
|
||||
z,
|
||||
Some(&[BLACK_CONCRETE, GRAY_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if local_z == space_length {
|
||||
// Bottom edge of parking spaces (border with driving lane)
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
x,
|
||||
0,
|
||||
z,
|
||||
Some(&[BLACK_CONCRETE, GRAY_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else if local_z > space_length && local_z < space_length + lane_width
|
||||
{
|
||||
// Driving lane - use darker concrete
|
||||
editor.set_block(
|
||||
BLACK_CONCRETE,
|
||||
x,
|
||||
0,
|
||||
z,
|
||||
Some(&[GRAY_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Add light posts at parking space outline corners
|
||||
if local_x == 0 && local_z == 0 && zone_x % 3 == 0 && zone_z % 2 == 0 {
|
||||
// Light posts at regular intervals on parking space corners
|
||||
editor.set_block(COBBLESTONE_WALL, x, 1, z, None, None);
|
||||
for dy in 2..=4 {
|
||||
editor.set_block(OAK_FENCE, x, dy, z, None, None);
|
||||
}
|
||||
editor.set_block(GLOWSTONE, x, 5, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,171 @@
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedNode};
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement, ground_level: i32) {
|
||||
if let Some(barrier_type) = element.tags().get("barrier") {
|
||||
if barrier_type == "bollard" {
|
||||
if let ProcessedElement::Node(node) = element {
|
||||
editor.set_block(
|
||||
COBBLESTONE_WALL,
|
||||
node.x,
|
||||
ground_level + 1,
|
||||
node.z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
// Place bollard
|
||||
}
|
||||
} else if barrier_type == "kerb" {
|
||||
pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
// Default values
|
||||
let mut barrier_material: Block = COBBLESTONE_WALL;
|
||||
let mut barrier_height: i32 = 2;
|
||||
|
||||
match element.tags().get("barrier").map(|s| s.as_str()) {
|
||||
Some("bollard") => {
|
||||
barrier_material = COBBLESTONE_WALL;
|
||||
barrier_height = 1;
|
||||
}
|
||||
Some("kerb") => {
|
||||
// Ignore kerbs
|
||||
return;
|
||||
} else if let ProcessedElement::Way(way) = element {
|
||||
// Determine wall height
|
||||
let wall_height: i32 = element
|
||||
.tags()
|
||||
.get("height")
|
||||
.and_then(|height: &String| height.parse::<f32>().ok())
|
||||
.map(|height: f32| f32::min(3.0, height).round() as i32)
|
||||
.unwrap_or(2); // Default height is 2 if not specified or invalid
|
||||
}
|
||||
Some("hedge") => {
|
||||
barrier_material = OAK_LEAVES;
|
||||
barrier_height = 2;
|
||||
}
|
||||
Some("fence") => {
|
||||
// Handle fence sub-types
|
||||
match element.tags().get("fence_type").map(|s| s.as_str()) {
|
||||
Some("railing" | "bars" | "krest") => {
|
||||
barrier_material = STONE_BRICK_WALL;
|
||||
barrier_height = 1;
|
||||
}
|
||||
Some(
|
||||
"chain_link" | "metal" | "wire" | "barbed_wire" | "corrugated_metal"
|
||||
| "electric" | "metal_bars",
|
||||
) => {
|
||||
barrier_material = STONE_BRICK_WALL; // IRON_BARS
|
||||
barrier_height = 2;
|
||||
}
|
||||
Some("slatted" | "paling") => {
|
||||
barrier_material = OAK_FENCE;
|
||||
barrier_height = 1;
|
||||
}
|
||||
Some("wood" | "split_rail" | "panel" | "pole") => {
|
||||
barrier_material = OAK_FENCE;
|
||||
barrier_height = 2;
|
||||
}
|
||||
Some("concrete" | "stone") => {
|
||||
barrier_material = STONE_BRICK_WALL;
|
||||
barrier_height = 2;
|
||||
}
|
||||
Some("glass") => {
|
||||
barrier_material = GLASS;
|
||||
barrier_height = 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some("wall") => {
|
||||
barrier_material = STONE_BRICK_WALL;
|
||||
barrier_height = 3;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Tagged material takes priority over inferred
|
||||
if let Some(barrier_mat) = element.tags().get("material") {
|
||||
if barrier_mat == "brick" {
|
||||
barrier_material = BRICK;
|
||||
}
|
||||
if barrier_mat == "concrete" {
|
||||
barrier_material = LIGHT_GRAY_CONCRETE;
|
||||
}
|
||||
if barrier_mat == "metal" {
|
||||
barrier_material = STONE_BRICK_WALL; // IRON_BARS
|
||||
}
|
||||
}
|
||||
|
||||
// Process nodes to create the barrier wall
|
||||
for i in 1..way.nodes.len() {
|
||||
let prev: &crate::osm_parser::ProcessedNode = &way.nodes[i - 1];
|
||||
let x1: i32 = prev.x;
|
||||
let z1: i32 = prev.z;
|
||||
if let ProcessedElement::Way(way) = element {
|
||||
// Determine wall height
|
||||
let wall_height: i32 = element
|
||||
.tags()
|
||||
.get("height")
|
||||
.and_then(|height: &String| height.parse::<f32>().ok())
|
||||
.map(|height: f32| height.round() as i32)
|
||||
.unwrap_or(barrier_height);
|
||||
|
||||
let cur: &crate::osm_parser::ProcessedNode = &way.nodes[i];
|
||||
let x2: i32 = cur.x;
|
||||
let z2: i32 = cur.z;
|
||||
// Process nodes to create the barrier wall
|
||||
for i in 1..way.nodes.len() {
|
||||
let prev: &crate::osm_parser::ProcessedNode = &way.nodes[i - 1];
|
||||
let x1: i32 = prev.x;
|
||||
let z1: i32 = prev.z;
|
||||
|
||||
// Generate the line of coordinates between the two nodes
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(x1, ground_level, z1, x2, ground_level, z2);
|
||||
let cur: &crate::osm_parser::ProcessedNode = &way.nodes[i];
|
||||
let x2: i32 = cur.x;
|
||||
let z2: i32 = cur.z;
|
||||
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
// Build the barrier wall to the specified height
|
||||
for y in (ground_level + 1)..=(ground_level + wall_height) {
|
||||
editor.set_block(COBBLESTONE_WALL, bx, y, bz, None, None);
|
||||
// Barrier wall
|
||||
}
|
||||
// Generate the line of coordinates between the two nodes
|
||||
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(x1, 0, z1, x2, 0, z2);
|
||||
|
||||
// Add an optional top to the barrier if the height is more than 1
|
||||
if wall_height > 1 {
|
||||
editor.set_block(
|
||||
STONE_BRICK_SLAB,
|
||||
bx,
|
||||
ground_level + wall_height + 1,
|
||||
bz,
|
||||
None,
|
||||
None,
|
||||
); // Top of the barrier
|
||||
}
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
// Build the barrier wall to the specified height
|
||||
for y in 1..=wall_height {
|
||||
editor.set_block(barrier_material, bx, y, bz, None, None);
|
||||
}
|
||||
|
||||
// Add an optional top to the barrier if the height is more than 1
|
||||
if wall_height > 1 {
|
||||
editor.set_block(STONE_BRICK_SLAB, bx, wall_height + 1, bz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_barrier_nodes(editor: &mut WorldEditor<'_>, node: &ProcessedNode) {
|
||||
match node.tags.get("barrier").map(|s| s.as_str()) {
|
||||
Some("bollard") => {
|
||||
editor.set_block(COBBLESTONE_WALL, node.x, 1, node.z, None, None);
|
||||
}
|
||||
Some("stile" | "gate" | "swing_gate" | "lift_gate") => {
|
||||
/*editor.set_block(
|
||||
OAK_TRAPDOOR,
|
||||
node.x,
|
||||
1,
|
||||
node.z,
|
||||
Some(&[
|
||||
COBBLESTONE_WALL,
|
||||
OAK_FENCE,
|
||||
STONE_BRICK_WALL,
|
||||
OAK_LEAVES,
|
||||
STONE_BRICK_SLAB,
|
||||
]),
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
AIR,
|
||||
node.x,
|
||||
2,
|
||||
node.z,
|
||||
Some(&[
|
||||
COBBLESTONE_WALL,
|
||||
OAK_FENCE,
|
||||
STONE_BRICK_WALL,
|
||||
OAK_LEAVES,
|
||||
STONE_BRICK_SLAB,
|
||||
]),
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
AIR,
|
||||
node.x,
|
||||
3,
|
||||
node.z,
|
||||
Some(&[
|
||||
COBBLESTONE_WALL,
|
||||
OAK_FENCE,
|
||||
STONE_BRICK_WALL,
|
||||
OAK_LEAVES,
|
||||
STONE_BRICK_SLAB,
|
||||
]),
|
||||
None,
|
||||
);*/
|
||||
}
|
||||
Some("block") => {
|
||||
editor.set_block(STONE, node.x, 1, node.z, None, None);
|
||||
}
|
||||
Some("entrance") => {
|
||||
editor.set_block(AIR, node.x, 1, node.z, None, None);
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,39 +3,97 @@ use crate::bresenham::bresenham_line;
|
||||
use crate::osm_parser::ProcessedWay;
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
// TODO FIX
|
||||
// TODO FIX - This handles ways with bridge=yes tag (e.g., highway bridges)
|
||||
#[allow(dead_code)]
|
||||
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay, ground_level: i32) {
|
||||
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
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() {
|
||||
let prev = &element.nodes[i - 1];
|
||||
let cur = &element.nodes[i];
|
||||
let points = bresenham_line(prev.x, ground_level, prev.z, cur.x, ground_level, cur.z);
|
||||
|
||||
let total_length = points.len();
|
||||
let ramp_length = 6; // Length of ramp at each end
|
||||
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 ramp_length = (total_length * 0.15).clamp(6.0, 20.0) as usize; // 15% of bridge, min 6, max 20 blocks
|
||||
|
||||
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)
|
||||
(idx * bridge_height) / ramp_length
|
||||
} else if idx >= total_length - ramp_length {
|
||||
(overall_idx as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
||||
} else if overall_idx >= total_len_usize.saturating_sub(ramp_length) {
|
||||
// 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 {
|
||||
// Middle section (constant height)
|
||||
bridge_height
|
||||
};
|
||||
|
||||
let bridge_y = ground_level + height as i32;
|
||||
// Use fixed bridge deck height (max of endpoints) plus ramp offset
|
||||
let bridge_y = bridge_deck_ground_y + ramp_offset;
|
||||
|
||||
// Place bridge blocks
|
||||
for dx in -2..=2 {
|
||||
editor.set_block(LIGHT_GRAY_CONCRETE, *x + dx, bridge_y, *z, None, None);
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
*x + dx,
|
||||
bridge_y,
|
||||
*z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
accumulated_length += segment_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::block_definitions::*;
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
pub fn generate_doors(editor: &mut WorldEditor, element: &ProcessedNode, ground_level: i32) {
|
||||
pub fn generate_doors(editor: &mut WorldEditor, element: &ProcessedNode) {
|
||||
// Check if the element is a door or entrance
|
||||
if element.tags.contains_key("door") || element.tags.contains_key("entrance") {
|
||||
// Check for the "level" tag and skip doors that are not at ground level
|
||||
@@ -18,8 +18,8 @@ pub fn generate_doors(editor: &mut WorldEditor, element: &ProcessedNode, ground_
|
||||
let z: i32 = element.z;
|
||||
|
||||
// Set the ground block and the door blocks
|
||||
editor.set_block(GRAY_CONCRETE, x, ground_level, z, None, None);
|
||||
editor.set_block(DARK_OAK_DOOR_LOWER, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(DARK_OAK_DOOR_UPPER, x, ground_level + 2, z, None, None);
|
||||
editor.set_block(GRAY_CONCRETE, x, 0, z, None, None);
|
||||
editor.set_block(DARK_OAK_DOOR_LOWER, x, 1, z, None, None);
|
||||
editor.set_block(DARK_OAK_DOOR_UPPER, x, 2, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,82 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Type alias for highway connectivity map
|
||||
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||
|
||||
/// 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,
|
||||
ground_level: i32,
|
||||
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 highway_type == "street_lamp" {
|
||||
@@ -17,10 +84,11 @@ pub fn generate_highways(
|
||||
if let ProcessedElement::Node(first_node) = element {
|
||||
let x: i32 = first_node.x;
|
||||
let z: i32 = first_node.z;
|
||||
for y in 1..=4 {
|
||||
editor.set_block(OAK_FENCE, x, ground_level + y, z, None, None);
|
||||
editor.set_block(COBBLESTONE_WALL, x, 1, z, None, None);
|
||||
for dy in 2..=4 {
|
||||
editor.set_block(OAK_FENCE, x, dy, z, None, None);
|
||||
}
|
||||
editor.set_block(GLOWSTONE, x, ground_level + 5, z, None, None);
|
||||
editor.set_block(GLOWSTONE, x, 5, z, None, None);
|
||||
}
|
||||
} else if highway_type == "crossing" {
|
||||
// Handle traffic signals for crossings
|
||||
@@ -29,31 +97,28 @@ pub fn generate_highways(
|
||||
if let ProcessedElement::Node(node) = element {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
for y in 1..=3 {
|
||||
editor.set_block(COBBLESTONE_WALL, x, ground_level + y, z, None, None);
|
||||
|
||||
for dy in 1..=3 {
|
||||
editor.set_block(COBBLESTONE_WALL, x, dy, z, None, None);
|
||||
}
|
||||
|
||||
editor.set_block(GREEN_WOOL, x, ground_level + 4, z, None, None);
|
||||
editor.set_block(YELLOW_WOOL, x, ground_level + 5, z, None, None);
|
||||
editor.set_block(RED_WOOL, x, ground_level + 6, z, None, None);
|
||||
|
||||
if args.winter {
|
||||
editor.set_block(SNOW_LAYER, x, ground_level + 7, z, None, None);
|
||||
}
|
||||
editor.set_block(GREEN_WOOL, x, 4, z, None, None);
|
||||
editor.set_block(YELLOW_WOOL, x, 5, z, None, None);
|
||||
editor.set_block(RED_WOOL, x, 6, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if highway_type == "bus_stop" {
|
||||
// Handle bus stops
|
||||
if let ProcessedElement::Node(node) = element {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
for y in 1..=3 {
|
||||
editor.set_block(COBBLESTONE_WALL, x, ground_level + y, z, None, None);
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
for dy in 1..=3 {
|
||||
editor.set_block(COBBLESTONE_WALL, x, dy, z, None, None);
|
||||
}
|
||||
|
||||
editor.set_block(WHITE_WOOL, x, ground_level + 4, z, None, None);
|
||||
editor.set_block(WHITE_WOOL, x + 1, ground_level + 4, z, None, None);
|
||||
editor.set_block(WHITE_WOOL, x, 4, z, None, None);
|
||||
editor.set_block(WHITE_WOOL, x + 1, 4, z, None, None);
|
||||
}
|
||||
} else if element
|
||||
.tags()
|
||||
@@ -75,45 +140,45 @@ pub fn generate_highways(
|
||||
"wood" => OAK_PLANKS,
|
||||
"asphalt" => BLACK_CONCRETE,
|
||||
"gravel" | "fine_gravel" => GRAVEL,
|
||||
"grass" => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
}
|
||||
"dirt" => DIRT,
|
||||
"grass" => GRASS_BLOCK,
|
||||
"dirt" | "ground" | "earth" => DIRT,
|
||||
"sand" => SAND,
|
||||
"concrete" => LIGHT_GRAY_CONCRETE,
|
||||
_ => STONE, // Default to stone for unknown surfaces
|
||||
};
|
||||
}
|
||||
|
||||
// Fill the area using flood fill or by iterating through the nodes
|
||||
let polygon_coords: Vec<(i32, i32)> = way
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
// Fill the area using flood fill cache
|
||||
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 {
|
||||
editor.set_block(surface_block, x, ground_level, z, None, None);
|
||||
editor.set_block(surface_block, x, 0, z, None, None);
|
||||
}
|
||||
} else {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
let mut block_type = BLACK_CONCRETE;
|
||||
let mut block_range: i32 = 2;
|
||||
let mut add_stripe = false;
|
||||
let mut add_outline = false;
|
||||
let scale_factor = args.scale;
|
||||
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags().get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check if this is a bridge - bridges need special elevation handling
|
||||
// to span across valleys instead of following terrain
|
||||
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
|
||||
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
|
||||
|
||||
// Parse the layer value for elevation calculation
|
||||
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 level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
@@ -127,12 +192,19 @@ pub fn generate_highways(
|
||||
block_range = 1;
|
||||
}
|
||||
"path" => {
|
||||
block_type = LIGHT_GRAY_CONCRETE;
|
||||
block_type = DIRT_PATH;
|
||||
block_range = 1;
|
||||
}
|
||||
"motorway" | "primary" => {
|
||||
"motorway" | "primary" | "trunk" => {
|
||||
block_range = 5;
|
||||
add_stripe = true; // Add stripes for motorways and primary roads
|
||||
add_stripe = true;
|
||||
}
|
||||
"secondary" => {
|
||||
block_range = 4;
|
||||
add_stripe = true;
|
||||
}
|
||||
"tertiary" => {
|
||||
add_stripe = true;
|
||||
}
|
||||
"track" => {
|
||||
block_range = 1;
|
||||
@@ -141,14 +213,32 @@ pub fn generate_highways(
|
||||
block_type = GRAY_CONCRETE;
|
||||
block_range = 2;
|
||||
}
|
||||
"secondary_link" | "tertiary_link" => {
|
||||
//Exit ramps, sliproads
|
||||
block_type = BLACK_CONCRETE;
|
||||
block_range = 1;
|
||||
}
|
||||
"escape" => {
|
||||
// Sand trap for vehicles on mountainous roads
|
||||
block_type = SAND;
|
||||
block_range = 1;
|
||||
}
|
||||
"steps" => {
|
||||
//TODO: Add correct stairs respecting height, step_count, etc.
|
||||
block_type = GRAY_CONCRETE;
|
||||
block_range = 1;
|
||||
}
|
||||
|
||||
_ => {
|
||||
if let Some(lanes) = element.tags().get("lanes") {
|
||||
if lanes == "2" {
|
||||
block_range = 3;
|
||||
add_stripe = true;
|
||||
add_outline = true;
|
||||
} else if lanes != "1" {
|
||||
block_range = 4;
|
||||
add_stripe = true;
|
||||
add_outline = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +248,102 @@ pub fn generate_highways(
|
||||
return;
|
||||
};
|
||||
|
||||
if scale_factor < 1.0 {
|
||||
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
|
||||
let mut segment_index = 0;
|
||||
let total_segments = way.nodes.len() - 1;
|
||||
|
||||
for node in &way.nodes {
|
||||
if let Some(prev) = previous_node {
|
||||
let (x1, z1) = prev;
|
||||
@@ -167,14 +352,40 @@ pub fn generate_highways(
|
||||
|
||||
// Generate the line of coordinates between the two nodes
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(x1, ground_level, z1, x2, ground_level, 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
|
||||
let mut stripe_length: i32 = 0;
|
||||
let dash_length: i32 = 5; // Length of the solid part of the stripe
|
||||
let gap_length: i32 = 5; // Length of the gap 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;
|
||||
|
||||
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
|
||||
for dx in -block_range..=block_range {
|
||||
for dz in -block_range..=block_range {
|
||||
@@ -189,69 +400,230 @@ pub fn generate_highways(
|
||||
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
|
||||
if is_horizontal {
|
||||
if set_x % 2 < 1 {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
if use_absolute_y {
|
||||
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,
|
||||
ground_level,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
ground_level,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if set_z % 2 < 1 {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
if use_absolute_y {
|
||||
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,
|
||||
ground_level,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
ground_level,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
block_type,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
block_type,
|
||||
set_x,
|
||||
ground_level,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add light gray concrete outline for multi-lane roads
|
||||
if add_outline {
|
||||
// Left outline
|
||||
for dz in -block_range..=block_range {
|
||||
let outline_x = x - block_range - 1;
|
||||
let outline_z = z + dz;
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Right outline
|
||||
for dz in -block_range..=block_range {
|
||||
let outline_x = x + block_range + 1;
|
||||
let outline_z = z + dz;
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
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
|
||||
if add_stripe {
|
||||
if stripe_length < dash_length {
|
||||
let stripe_x: i32 = x;
|
||||
let stripe_z: i32 = z;
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
ground_level,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
let stripe_x: i32 = *x;
|
||||
let stripe_z: i32 = *z;
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
current_y,
|
||||
stripe_z,
|
||||
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
|
||||
@@ -261,6 +633,8 @@ pub fn generate_highways(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segment_index += 1;
|
||||
}
|
||||
previous_node = Some((node.x, node.z));
|
||||
}
|
||||
@@ -268,49 +642,220 @@ pub fn generate_highways(
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay, ground_level: i32) {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
let mut previous_node: Option<XZPoint> = None;
|
||||
let siding_block: Block = STONE_BRICK_SLAB;
|
||||
|
||||
for node in &element.nodes {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
let current_node = node.xz();
|
||||
|
||||
// Draw the siding using Bresenham's line algorithm between nodes
|
||||
if let Some(prev) = previous_node {
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(prev.0, ground_level + 1, prev.1, x, ground_level + 1, z);
|
||||
for (bx, by, bz) in bresenham_points {
|
||||
if !editor.check_for_block(
|
||||
bx,
|
||||
by - 1,
|
||||
bz,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||
) {
|
||||
editor.set_block(siding_block, bx, by, bz, None, None);
|
||||
if let Some(prev_node) = previous_node {
|
||||
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(
|
||||
prev_node.x,
|
||||
0,
|
||||
prev_node.z,
|
||||
current_node.x,
|
||||
0,
|
||||
current_node.z,
|
||||
);
|
||||
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
if !editor.check_for_block(bx, 0, bz, Some(&[BLACK_CONCRETE, WHITE_CONCRETE])) {
|
||||
editor.set_block(siding_block, bx, 1, bz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previous_node = Some((x, z));
|
||||
previous_node = Some(current_node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates an aeroway
|
||||
pub fn generate_aeroway(editor: &mut WorldEditor, way: &ProcessedWay, ground_level: i32) {
|
||||
pub fn generate_aeroway(editor: &mut WorldEditor, way: &ProcessedWay, args: &Args) {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
let surface_block = LIGHT_GRAY_CONCRETE;
|
||||
|
||||
for node in &way.nodes {
|
||||
if let Some(prev) = previous_node {
|
||||
let points = bresenham_line(prev.0, ground_level, prev.1, node.x, ground_level, node.z);
|
||||
let (x1, z1) = prev;
|
||||
let x2 = node.x;
|
||||
let z2 = node.z;
|
||||
let points = bresenham_line(x1, 0, z1, x2, 0, z2);
|
||||
let way_width: i32 = (12.0 * args.scale).ceil() as i32;
|
||||
|
||||
for (x, y, z) in points {
|
||||
for dx in -12..=1 {
|
||||
for dz in -12..=1 {
|
||||
editor.set_block(surface_block, x + dx, y, z + dz, None, None);
|
||||
for (x, _, z) in points {
|
||||
for dx in -way_width..=way_width {
|
||||
for dz in -way_width..=way_width {
|
||||
let set_x = x + dx;
|
||||
let set_z = z + dz;
|
||||
editor.set_block(surface_block, set_x, 0, set_z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,385 +1,378 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::element_processing::tree::create_tree;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::osm_parser::ProcessedWay;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_landuse(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
ground_level: i32,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
|
||||
let mut current_landuse: Vec<(i32, i32)> = vec![];
|
||||
|
||||
// Determine block type based on landuse tag
|
||||
let binding: String = "".to_string();
|
||||
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() {
|
||||
"greenfield" | "meadow" | "grass" => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
}
|
||||
"greenfield" | "meadow" | "grass" | "orchard" | "forest" => GRASS_BLOCK,
|
||||
"farmland" => FARMLAND,
|
||||
"forest" => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
"cemetery" => PODZOL,
|
||||
"construction" => COARSE_DIRT,
|
||||
"traffic_island" => STONE_BLOCK_SLAB,
|
||||
"residential" => {
|
||||
let residential_tag = element.tags.get("residential").unwrap_or(&binding);
|
||||
if residential_tag == "rural" {
|
||||
GRASS_BLOCK
|
||||
} else {
|
||||
STONE_BRICKS // Placeholder, will be randomized per-block
|
||||
}
|
||||
}
|
||||
"cemetery" => PODZOL,
|
||||
"beach" => SAND,
|
||||
"construction" => DIRT,
|
||||
"traffic_island" => STONE_BLOCK_SLAB,
|
||||
"residential" => STONE_BRICKS,
|
||||
"commercial" => SMOOTH_STONE,
|
||||
"education" => LIGHT_GRAY_CONCRETE,
|
||||
"industrial" => COBBLESTONE,
|
||||
"commercial" => SMOOTH_STONE, // Placeholder, will be randomized per-block
|
||||
"education" => POLISHED_ANDESITE,
|
||||
"religious" => POLISHED_ANDESITE,
|
||||
"industrial" => STONE, // Placeholder, will be randomized per-block
|
||||
"military" => GRAY_CONCRETE,
|
||||
"railway" => GRAVEL,
|
||||
_ => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
"landfill" => {
|
||||
// Gravel if man_made = spoil_heap or heap, coarse dirt else
|
||||
let manmade_tag = element.tags.get("man_made").unwrap_or(&binding);
|
||||
if manmade_tag == "spoil_heap" || manmade_tag == "heap" {
|
||||
GRAVEL
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
COARSE_DIRT
|
||||
}
|
||||
}
|
||||
"quarry" => STONE,
|
||||
_ => GRASS_BLOCK,
|
||||
};
|
||||
|
||||
let bresenham_block: Block = if args.winter { SNOW_BLOCK } else { GRASS_BLOCK };
|
||||
// Get the area of the landuse element using cache
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
// Process landuse nodes to fill the area
|
||||
for node in &element.nodes {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
if let Some(prev) = previous_node {
|
||||
// Generate the line of coordinates between the two nodes
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(prev.0, ground_level, prev.1, x, ground_level, z);
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
editor.set_block(bresenham_block, bx, ground_level, bz, None, None);
|
||||
for (x, z) in floor_area {
|
||||
// Apply per-block randomness for certain landuse types
|
||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 72 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 87 {
|
||||
CRACKED_STONE_BRICKS
|
||||
} else if random_value < 92 {
|
||||
STONE
|
||||
} else {
|
||||
COBBLESTONE
|
||||
}
|
||||
} else if landuse_tag == "commercial" {
|
||||
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 40 {
|
||||
SMOOTH_STONE
|
||||
} else if random_value < 70 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 90 {
|
||||
STONE
|
||||
} else {
|
||||
COBBLESTONE
|
||||
}
|
||||
} else if landuse_tag == "industrial" {
|
||||
// Industrial: primarily stone, with some stone bricks and smooth stone
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 70 {
|
||||
STONE
|
||||
} else if random_value < 90 {
|
||||
STONE_BRICKS
|
||||
} else {
|
||||
SMOOTH_STONE
|
||||
}
|
||||
} else {
|
||||
block_type
|
||||
};
|
||||
|
||||
current_landuse.push((x, z));
|
||||
corner_addup = (corner_addup.0 + x, corner_addup.1 + z, corner_addup.2 + 1);
|
||||
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);
|
||||
}
|
||||
|
||||
previous_node = Some((x, z));
|
||||
}
|
||||
|
||||
// If there are landuse nodes, flood-fill the area
|
||||
if !current_landuse.is_empty() {
|
||||
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
|
||||
for (x, z) in floor_area {
|
||||
if landuse_tag == "traffic_island" {
|
||||
editor.set_block(block_type, x, ground_level + 1, z, None, None);
|
||||
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
||||
editor.set_block(block_type, x, ground_level, z, None, Some(&[SPONGE]));
|
||||
} else {
|
||||
editor.set_block(block_type, x, ground_level, z, None, None);
|
||||
}
|
||||
|
||||
// Add specific features for different landuse types
|
||||
match landuse_tag.as_str() {
|
||||
"cemetery" => {
|
||||
if (x % 3 == 0) && (z % 3 == 0) {
|
||||
let random_choice: i32 = rng.gen_range(0..100);
|
||||
if random_choice < 15 {
|
||||
// Place graves
|
||||
if editor.check_for_block(x, ground_level, z, Some(&[PODZOL]), None) {
|
||||
if rng.gen_bool(0.5) {
|
||||
editor.set_block(
|
||||
COBBLESTONE,
|
||||
x - 1,
|
||||
ground_level + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
STONE_BRICK_SLAB,
|
||||
x - 1,
|
||||
ground_level + 2,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
STONE_BRICK_SLAB,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
STONE_BRICK_SLAB,
|
||||
x + 1,
|
||||
ground_level + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
COBBLESTONE,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z - 1,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
STONE_BRICK_SLAB,
|
||||
x,
|
||||
ground_level + 2,
|
||||
z - 1,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
STONE_BRICK_SLAB,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
editor.set_block(
|
||||
STONE_BRICK_SLAB,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z + 1,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if random_choice < 30 {
|
||||
if editor.check_for_block(x, ground_level, z, Some(&[PODZOL]), None) {
|
||||
editor.set_block(RED_FLOWER, x, ground_level + 1, z, None, None);
|
||||
}
|
||||
} else if random_choice < 33 {
|
||||
create_tree(
|
||||
editor,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
rng.gen_range(1..=3),
|
||||
args.winter,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"forest" => {
|
||||
if !editor.check_for_block(x, ground_level, z, None, Some(&[WATER])) {
|
||||
let random_choice: i32 = rng.gen_range(0..21);
|
||||
if random_choice == 20 {
|
||||
create_tree(
|
||||
editor,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
rng.gen_range(1..=3),
|
||||
args.winter,
|
||||
);
|
||||
} else if random_choice == 2 {
|
||||
let flower_block: Block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, ground_level + 1, z, None, None);
|
||||
} else if random_choice <= 1 {
|
||||
editor.set_block(GRASS, x, ground_level + 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"farmland" => {
|
||||
// Check if the current block is not water or another undesired block
|
||||
if !editor.check_for_block(x, ground_level, z, None, Some(&[WATER])) {
|
||||
if x % 15 == 0 || z % 15 == 0 {
|
||||
// Place water on the edges
|
||||
editor.set_block(WATER, x, ground_level, z, Some(&[FARMLAND]), None);
|
||||
editor.set_block(
|
||||
AIR,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
Some(&[GRASS, WHEAT, CARROTS, POTATOES]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
// Set the block below as farmland
|
||||
editor.set_block(FARMLAND, x, ground_level, z, None, None);
|
||||
|
||||
// If a random condition is met, place a special object
|
||||
if rng.gen_range(0..76) == 0 {
|
||||
let special_choice: i32 = rng.gen_range(1..=10);
|
||||
if special_choice <= 2 {
|
||||
create_tree(
|
||||
editor,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
rng.gen_range(1..=3),
|
||||
args.winter,
|
||||
);
|
||||
} else if special_choice <= 6 {
|
||||
editor.set_block(HAY_BALE, x, ground_level + 1, z, None, None);
|
||||
} else {
|
||||
editor.set_block(
|
||||
OAK_LEAVES,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Add specific features for different landuse types
|
||||
match landuse_tag.as_str() {
|
||||
"cemetery" => {
|
||||
if (x % 3 == 0) && (z % 3 == 0) {
|
||||
let random_choice: i32 = rng.gen_range(0..100);
|
||||
if random_choice < 15 {
|
||||
// Place graves
|
||||
if editor.check_for_block(x, 0, z, Some(&[PODZOL])) {
|
||||
if rng.gen_bool(0.5) {
|
||||
editor.set_block(COBBLESTONE, x - 1, 1, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x - 1, 2, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 1, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x + 1, 1, z, None, None);
|
||||
} else {
|
||||
// Set crops only if the block below is farmland
|
||||
if editor.check_for_block(
|
||||
x,
|
||||
ground_level,
|
||||
z,
|
||||
Some(&[FARMLAND]),
|
||||
None,
|
||||
) {
|
||||
let crop_choice =
|
||||
[WHEAT, CARROTS, POTATOES][rng.gen_range(0..3)];
|
||||
editor.set_block(
|
||||
crop_choice,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
editor.set_block(COBBLESTONE, x, 1, z - 1, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 2, z - 1, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 1, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 1, z + 1, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"construction" => {
|
||||
let random_choice: i32 = rng.gen_range(0..1501);
|
||||
if random_choice < 6 {
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 1, z, None, None);
|
||||
if random_choice < 2 {
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 2, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 3, z, None, None);
|
||||
} else if random_choice < 4 {
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 2, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 3, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 4, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 1, z + 1, None, None);
|
||||
} else {
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 2, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 3, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 4, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, ground_level + 5, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x - 1, ground_level + 1, z, None, None);
|
||||
editor.set_block(
|
||||
SCAFFOLDING,
|
||||
x + 1,
|
||||
ground_level + 1,
|
||||
z - 1,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if random_choice < 30 {
|
||||
let construction_items: [Block; 11] = [
|
||||
OAK_LOG,
|
||||
COBBLESTONE,
|
||||
GRAVEL,
|
||||
GLOWSTONE,
|
||||
STONE,
|
||||
COBBLESTONE_WALL,
|
||||
BLACK_CONCRETE,
|
||||
SAND,
|
||||
OAK_PLANKS,
|
||||
DIRT,
|
||||
BRICK,
|
||||
];
|
||||
editor.set_block(
|
||||
construction_items[rng.gen_range(0..construction_items.len())],
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if editor.check_for_block(x, 0, z, Some(&[PODZOL])) {
|
||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||
}
|
||||
} else if random_choice < 33 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice < 35 {
|
||||
if random_choice < 30 {
|
||||
editor.set_block(DIRT, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(DIRT, x, ground_level + 2, z, None, None);
|
||||
editor.set_block(DIRT, x + 1, ground_level + 1, z, None, None);
|
||||
editor.set_block(DIRT, x, ground_level + 1, z + 1, None, None);
|
||||
} else {
|
||||
editor.set_block(DIRT, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(DIRT, x, ground_level + 2, z, None, None);
|
||||
editor.set_block(DIRT, x - 1, ground_level + 1, z, None, None);
|
||||
editor.set_block(DIRT, x, ground_level + 1, z - 1, None, None);
|
||||
}
|
||||
} else if random_choice < 150 {
|
||||
editor.set_block(AIR, x, ground_level, z, None, Some(&[SPONGE]));
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"grass" => {
|
||||
if rng.gen_range(1..=7) != 1
|
||||
&& editor.check_for_block(
|
||||
x,
|
||||
ground_level,
|
||||
z,
|
||||
Some(&[GRASS_BLOCK, SNOW_BLOCK]),
|
||||
None,
|
||||
)
|
||||
{
|
||||
editor.set_block(GRASS, x, ground_level + 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"meadow" => {
|
||||
if editor.check_for_block(
|
||||
x,
|
||||
ground_level,
|
||||
z,
|
||||
Some(&[GRASS_BLOCK, SNOW_BLOCK]),
|
||||
None,
|
||||
) {
|
||||
let random_choice: i32 = rng.gen_range(0..1001);
|
||||
if random_choice < 5 {
|
||||
create_tree(
|
||||
editor,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
rng.gen_range(1..=3),
|
||||
args.winter,
|
||||
);
|
||||
} else if random_choice < 800 {
|
||||
editor.set_block(GRASS, x, ground_level + 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
"forest" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 20 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 2 {
|
||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
||||
1 => OAK_LEAVES,
|
||||
2 => RED_FLOWER,
|
||||
3 => BLUE_FLOWER,
|
||||
4 => YELLOW_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if random_choice <= 12 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"farmland" => {
|
||||
// Check if the current block is not water or another undesired block
|
||||
if !editor.check_for_block(x, 0, z, Some(&[WATER])) {
|
||||
if x % 9 == 0 && z % 9 == 0 {
|
||||
// Place water in dot pattern
|
||||
editor.set_block(WATER, x, 0, z, Some(&[FARMLAND]), None);
|
||||
} else if rng.gen_range(0..76) == 0 {
|
||||
let special_choice: i32 = rng.gen_range(1..=10);
|
||||
if special_choice <= 4 {
|
||||
editor.set_block(HAY_BALE, x, 1, z, None, Some(&[SPONGE]));
|
||||
} else {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, Some(&[SPONGE]));
|
||||
}
|
||||
} else {
|
||||
// Set crops only if the block below is farmland
|
||||
if editor.check_for_block(x, 0, z, Some(&[FARMLAND])) {
|
||||
let crop_choice = [WHEAT, CARROTS, POTATOES][rng.gen_range(0..3)];
|
||||
editor.set_block(crop_choice, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"construction" => {
|
||||
let random_choice: i32 = rng.gen_range(0..1501);
|
||||
if random_choice < 15 {
|
||||
editor.set_block(SCAFFOLDING, x, 1, z, None, None);
|
||||
if random_choice < 2 {
|
||||
editor.set_block(SCAFFOLDING, x, 2, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, 3, z, None, None);
|
||||
} else if random_choice < 4 {
|
||||
editor.set_block(SCAFFOLDING, x, 2, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, 3, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, 4, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, 1, z + 1, None, None);
|
||||
} else {
|
||||
editor.set_block(SCAFFOLDING, x, 2, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, 3, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, 4, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x, 5, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x - 1, 1, z, None, None);
|
||||
editor.set_block(SCAFFOLDING, x + 1, 1, z - 1, None, None);
|
||||
}
|
||||
} else if random_choice < 55 {
|
||||
let construction_items: [Block; 13] = [
|
||||
OAK_LOG,
|
||||
COBBLESTONE,
|
||||
GRAVEL,
|
||||
GLOWSTONE,
|
||||
STONE,
|
||||
COBBLESTONE_WALL,
|
||||
BLACK_CONCRETE,
|
||||
SAND,
|
||||
OAK_PLANKS,
|
||||
DIRT,
|
||||
BRICK,
|
||||
CRAFTING_TABLE,
|
||||
FURNACE,
|
||||
];
|
||||
editor.set_block(
|
||||
construction_items[rng.gen_range(0..construction_items.len())],
|
||||
x,
|
||||
1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else if random_choice < 65 {
|
||||
if random_choice < 60 {
|
||||
editor.set_block(DIRT, x, 1, z, None, None);
|
||||
editor.set_block(DIRT, x, 2, z, None, None);
|
||||
editor.set_block(DIRT, x + 1, 1, z, None, None);
|
||||
editor.set_block(DIRT, x, 1, z + 1, None, None);
|
||||
} else {
|
||||
editor.set_block(DIRT, x, 1, z, None, None);
|
||||
editor.set_block(DIRT, x, 2, z, None, None);
|
||||
editor.set_block(DIRT, x - 1, 1, z, None, None);
|
||||
editor.set_block(DIRT, x, 1, z - 1, None, None);
|
||||
}
|
||||
} else if random_choice < 100 {
|
||||
editor.set_block(GRAVEL, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else if random_choice < 115 {
|
||||
editor.set_block(SAND, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else if random_choice < 125 {
|
||||
editor.set_block(DIORITE, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else if random_choice < 145 {
|
||||
editor.set_block(BRICK, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else if random_choice < 155 {
|
||||
editor.set_block(GRANITE, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else if random_choice < 180 {
|
||||
editor.set_block(ANDESITE, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else if random_choice < 565 {
|
||||
editor.set_block(COBBLESTONE, x, 0, z, None, Some(&[SPONGE]));
|
||||
}
|
||||
}
|
||||
"grass" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, 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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"greenfield" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, 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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"meadow" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..1001);
|
||||
if random_choice < 5 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice < 6 {
|
||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||
} else if random_choice < 9 {
|
||||
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 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"orchard" => {
|
||||
if x % 18 == 0 && z % 10 == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..100) {
|
||||
0 => editor.set_block(OAK_LEAVES, 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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"quarry" => {
|
||||
// Add stone layer under it
|
||||
editor.set_block(STONE, x, -1, z, Some(&[STONE]), None);
|
||||
editor.set_block(STONE, x, -2, z, Some(&[STONE]), None);
|
||||
// Generate ore blocks
|
||||
if let Some(resource) = element.tags.get("resource") {
|
||||
let ore_block = match resource.as_str() {
|
||||
"iron_ore" => IRON_ORE,
|
||||
"coal" => COAL_ORE,
|
||||
"copper" => COPPER_ORE,
|
||||
"gold" => GOLD_ORE,
|
||||
"clay" | "kaolinite" => CLAY,
|
||||
_ => STONE,
|
||||
};
|
||||
let random_choice: i32 = rng.gen_range(0..100 + editor.get_absolute_y(x, 0, z)); // The deeper it is the more resources are there
|
||||
if random_choice < 5 {
|
||||
editor.set_block(ore_block, x, 0, z, Some(&[STONE]), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_landuse_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("landuse") {
|
||||
// Generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_landuse(
|
||||
editor,
|
||||
&member.way.clone(),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all outer ways into one with relation tags
|
||||
let mut combined_nodes = Vec::new();
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
combined_nodes.extend(member.way.nodes.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Only process if we have nodes
|
||||
if !combined_nodes.is_empty() {
|
||||
// Create combined way with relation tags
|
||||
let combined_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: combined_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate landuse area from combined way
|
||||
generate_landuse(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::element_processing::tree::create_tree;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::osm_parser::ProcessedWay;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_leisure(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
ground_level: i32,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if let Some(leisure_type) = element.tags.get("leisure") {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
@@ -20,40 +22,31 @@ pub fn generate_leisure(
|
||||
|
||||
// Determine block type based on leisure type
|
||||
let block_type: Block = match leisure_type.as_str() {
|
||||
"park" => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
"park" | "nature_reserve" | "garden" | "disc_golf_course" | "golf_course" => {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
"playground" | "recreation_ground" | "pitch" => {
|
||||
"schoolyard" => BLACK_CONCRETE,
|
||||
"playground" | "recreation_ground" | "pitch" | "beach_resort" | "dog_park" => {
|
||||
if let Some(surface) = element.tags.get("surface") {
|
||||
match surface.as_str() {
|
||||
"clay" => TERRACOTTA,
|
||||
"sand" => SAND,
|
||||
"tartan" => RED_TERRACOTTA,
|
||||
"grass" => GRASS_BLOCK,
|
||||
"dirt" => DIRT,
|
||||
"pebblestone" | "cobblestone" | "unhewn_cobblestone" => COBBLESTONE,
|
||||
_ => GREEN_STAINED_HARDENED_CLAY,
|
||||
}
|
||||
} else {
|
||||
GREEN_STAINED_HARDENED_CLAY
|
||||
}
|
||||
}
|
||||
"garden" => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
}
|
||||
"swimming_pool" => WATER,
|
||||
_ => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
}
|
||||
"swimming_pool" | "swimming_area" => WATER, //Swimming area: Area in a larger body of water for swimming
|
||||
"bathing_place" => SMOOTH_SANDSTONE, // Could be sand or concrete
|
||||
"outdoor_seating" => SMOOTH_STONE, //Usually stone or stone bricks
|
||||
"water_park" | "slipway" => LIGHT_GRAY_CONCRETE, // Water park area, not the pool. Usually is concrete
|
||||
"ice_rink" => PACKED_ICE, // TODO: Ice for Ice Rink, needs building defined
|
||||
_ => GRASS_BLOCK,
|
||||
};
|
||||
|
||||
// Process leisure area nodes
|
||||
@@ -61,12 +54,12 @@ pub fn generate_leisure(
|
||||
if let Some(prev) = previous_node {
|
||||
// Draw a line between the current and previous node
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(prev.0, ground_level, prev.1, node.x, ground_level, node.z);
|
||||
bresenham_line(prev.0, 0, prev.1, node.x, 0, node.z);
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
editor.set_block(
|
||||
block_type,
|
||||
bx,
|
||||
ground_level,
|
||||
0,
|
||||
bz,
|
||||
Some(&[
|
||||
GRASS_BLOCK,
|
||||
@@ -88,57 +81,45 @@ pub fn generate_leisure(
|
||||
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) {
|
||||
let polygon_coords: Vec<(i32, i32)> = element
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_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 {
|
||||
editor.set_block(block_type, x, ground_level, z, Some(&[GRASS_BLOCK]), None);
|
||||
editor.set_block(block_type, x, 0, z, Some(&[GRASS_BLOCK]), None);
|
||||
|
||||
// Add decorative elements for parks and gardens
|
||||
if matches!(leisure_type.as_str(), "park" | "garden")
|
||||
&& editor.check_for_block(x, ground_level, z, Some(&[GRASS_BLOCK]), None)
|
||||
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
|
||||
&& editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK]))
|
||||
{
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
let random_choice: i32 = rng.gen_range(0..1000);
|
||||
|
||||
match random_choice {
|
||||
0 => {
|
||||
// Benches
|
||||
editor.set_block(OAK_LOG, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(OAK_LOG, x + 1, ground_level + 1, z, None, None);
|
||||
editor.set_block(OAK_LOG, x - 1, ground_level + 1, z, None, None);
|
||||
}
|
||||
1..=30 => {
|
||||
0..30 => {
|
||||
// Flowers
|
||||
let flower_choice = match rng.gen_range(0..4) {
|
||||
0 => RED_FLOWER,
|
||||
1 => YELLOW_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
let flower_choice = match random_choice {
|
||||
0..10 => RED_FLOWER,
|
||||
10..20 => YELLOW_FLOWER,
|
||||
20..30 => BLUE_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_choice, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(flower_choice, x, 1, z, None, None);
|
||||
}
|
||||
31..=70 => {
|
||||
30..90 => {
|
||||
// Grass
|
||||
editor.set_block(GRASS, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
71..=80 => {
|
||||
90..105 => {
|
||||
// Oak leaves
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
}
|
||||
105..120 => {
|
||||
// Tree
|
||||
create_tree(
|
||||
editor,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
rng.gen_range(1..=3),
|
||||
args.winter,
|
||||
);
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -146,40 +127,41 @@ pub fn generate_leisure(
|
||||
|
||||
// Add playground or recreation ground features
|
||||
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
let random_choice: i32 = rng.gen_range(0..5000);
|
||||
|
||||
match random_choice {
|
||||
0..=10 => {
|
||||
0..10 => {
|
||||
// Swing set
|
||||
for y in 1..=4 {
|
||||
editor.set_block(OAK_FENCE, x - 1, ground_level + y, z, None, None);
|
||||
editor.set_block(OAK_FENCE, x + 1, ground_level + y, z, None, None);
|
||||
for y in 1..=3 {
|
||||
editor.set_block(OAK_FENCE, x - 1, y, z, None, None);
|
||||
editor.set_block(OAK_FENCE, x + 1, y, z, None, None);
|
||||
}
|
||||
editor.set_block(OAK_FENCE, x, ground_level + 4, z, None, None);
|
||||
editor.set_block(STONE_BLOCK_SLAB, x, ground_level + 2, z, None, None);
|
||||
editor.set_block(OAK_PLANKS, x - 1, 4, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x, 4, z, None, None);
|
||||
editor.set_block(OAK_PLANKS, x + 1, 4, z, None, None);
|
||||
editor.set_block(STONE_BLOCK_SLAB, x, 2, z, None, None);
|
||||
}
|
||||
11..=20 => {
|
||||
10..20 => {
|
||||
// Slide
|
||||
editor.set_block(OAK_SLAB, x, ground_level + 1, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x + 1, ground_level + 2, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x + 2, ground_level + 3, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x, 1, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x + 1, 2, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x + 2, 3, z, None, None);
|
||||
|
||||
editor.set_block(OAK_PLANKS, x + 2, ground_level + 2, z, None, None);
|
||||
editor.set_block(OAK_PLANKS, x + 2, ground_level + 1, z, None, None);
|
||||
editor.set_block(OAK_PLANKS, x + 2, 2, z, None, None);
|
||||
editor.set_block(OAK_PLANKS, x + 2, 1, z, None, None);
|
||||
|
||||
editor.set_block(LADDER, x + 2, ground_level + 2, z - 1, None, None);
|
||||
editor.set_block(LADDER, x + 2, ground_level + 1, z - 1, None, None);
|
||||
editor.set_block(LADDER, x + 2, 2, z - 1, None, None);
|
||||
editor.set_block(LADDER, x + 2, 1, z - 1, None, None);
|
||||
}
|
||||
21..=30 => {
|
||||
20..30 => {
|
||||
// Sandpit
|
||||
editor.fill_blocks(
|
||||
SAND,
|
||||
x - 3,
|
||||
ground_level,
|
||||
0,
|
||||
z - 3,
|
||||
x + 3,
|
||||
ground_level,
|
||||
0,
|
||||
z + 3,
|
||||
Some(&[GREEN_STAINED_HARDENED_CLAY]),
|
||||
None,
|
||||
@@ -192,3 +174,50 @@ pub fn generate_leisure(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_leisure_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
// First generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_leisure(
|
||||
editor,
|
||||
&member.way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Then combine all outer ways into one
|
||||
let mut combined_nodes = Vec::new();
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
combined_nodes.extend(member.way.nodes.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Create combined way with relation tags
|
||||
let combined_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: combined_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate leisure area from combined way
|
||||
generate_leisure(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
256
src/element_processing/man_made.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedNode};
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
pub fn generate_man_made(editor: &mut WorldEditor, element: &ProcessedElement, _args: &Args) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags().get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(level) = element.tags().get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(man_made_type) = element.tags().get("man_made") {
|
||||
match man_made_type.as_str() {
|
||||
"pier" => generate_pier(editor, element),
|
||||
"antenna" => generate_antenna(editor, element),
|
||||
"chimney" => generate_chimney(editor, element),
|
||||
"water_well" => generate_water_well(editor, element),
|
||||
"water_tower" => generate_water_tower(editor, element),
|
||||
"mast" => generate_antenna(editor, element),
|
||||
_ => {} // Unknown man_made type, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a pier structure with OAK_SLAB planks and OAK_LOG support pillars
|
||||
fn generate_pier(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
if let ProcessedElement::Way(way) = element {
|
||||
let nodes = &way.nodes;
|
||||
if nodes.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract pier dimensions from tags
|
||||
let pier_width = element
|
||||
.tags()
|
||||
.get("width")
|
||||
.and_then(|w| w.parse::<i32>().ok())
|
||||
.unwrap_or(3); // Default 3 blocks wide
|
||||
|
||||
let pier_height = 1; // Pier deck height above ground
|
||||
let support_spacing = 4; // Support pillars every 4 blocks
|
||||
|
||||
// Generate the pier walkway using bresenham line algorithm
|
||||
for i in 0..nodes.len() - 1 {
|
||||
let start_node = &nodes[i];
|
||||
let end_node = &nodes[i + 1];
|
||||
|
||||
let line_points =
|
||||
bresenham_line(start_node.x, 0, start_node.z, end_node.x, 0, end_node.z);
|
||||
|
||||
for (index, (center_x, _y, center_z)) in line_points.iter().enumerate() {
|
||||
// Create pier deck (3 blocks wide)
|
||||
let half_width = pier_width / 2;
|
||||
for x in (center_x - half_width)..=(center_x + half_width) {
|
||||
for z in (center_z - half_width)..=(center_z + half_width) {
|
||||
editor.set_block(OAK_SLAB, x, pier_height, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Add support pillars every few blocks
|
||||
if index % support_spacing == 0 {
|
||||
let half_width = pier_width / 2;
|
||||
|
||||
// Place support pillars at the edges of the pier
|
||||
let support_positions = [
|
||||
(center_x - half_width, center_z), // Left side
|
||||
(center_x + half_width, center_z), // Right side
|
||||
];
|
||||
|
||||
for (pillar_x, pillar_z) in support_positions {
|
||||
// Support pillars going down from pier level
|
||||
editor.set_block(OAK_LOG, pillar_x, 0, *pillar_z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an antenna/radio tower
|
||||
fn generate_antenna(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
if let Some(first_node) = element.nodes().next() {
|
||||
let x = first_node.x;
|
||||
let z = first_node.z;
|
||||
|
||||
// Extract antenna configuration from tags
|
||||
let height = match element.tags().get("height") {
|
||||
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()) {
|
||||
Some("communication") => 20,
|
||||
Some("cellular") => 15,
|
||||
_ => 20,
|
||||
},
|
||||
};
|
||||
|
||||
// Build the main tower pole
|
||||
editor.set_block(IRON_BLOCK, x, 3, z, None, None);
|
||||
for y in 4..height {
|
||||
editor.set_block(IRON_BARS, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Add structural supports every 7 blocks
|
||||
for y in (7..height).step_by(7) {
|
||||
editor.set_block(IRON_BLOCK, x, y, z, Some(&[IRON_BARS]), None);
|
||||
let support_positions = [(1, 0), (-1, 0), (0, 1), (0, -1)];
|
||||
for (dx, dz) in support_positions {
|
||||
editor.set_block(IRON_BLOCK, x + dx, y, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Equipment housing at base
|
||||
editor.fill_blocks(
|
||||
GRAY_CONCRETE,
|
||||
x - 1,
|
||||
1,
|
||||
z - 1,
|
||||
x + 1,
|
||||
2,
|
||||
z + 1,
|
||||
Some(&[GRAY_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a chimney structure
|
||||
fn generate_chimney(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
if let Some(first_node) = element.nodes().next() {
|
||||
let x = first_node.x;
|
||||
let z = first_node.z;
|
||||
let height = 25;
|
||||
|
||||
// Build 3x3 brick chimney with hole in the middle
|
||||
for y in 0..height {
|
||||
for dx in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
// Skip center block to create hole
|
||||
if dx == 0 && dz == 0 {
|
||||
continue;
|
||||
}
|
||||
editor.set_block(BRICK, x + dx, y, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a water well structure
|
||||
fn generate_water_well(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
if let Some(first_node) = element.nodes().next() {
|
||||
let x = first_node.x;
|
||||
let z = first_node.z;
|
||||
|
||||
// Build stone well structure (3x3 base with water in center)
|
||||
for dx in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
if dx == 0 && dz == 0 {
|
||||
// Water in the center
|
||||
editor.set_block(WATER, x, -1, z, None, None);
|
||||
editor.set_block(WATER, x, 0, z, None, None);
|
||||
} else {
|
||||
// Stone well walls
|
||||
editor.set_block(STONE_BRICKS, x + dx, 0, z + dz, None, None);
|
||||
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add wooden well frame structure
|
||||
editor.fill_blocks(OAK_LOG, x - 2, 1, z, x - 2, 4, z, None, None);
|
||||
editor.fill_blocks(OAK_LOG, x + 2, 1, z, x + 2, 4, z, None, None);
|
||||
|
||||
// Crossbeam with pulley system
|
||||
editor.set_block(OAK_SLAB, x - 1, 5, z, None, None);
|
||||
editor.set_block(OAK_FENCE, x, 4, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x, 5, z, None, None);
|
||||
editor.set_block(OAK_SLAB, x + 1, 5, z, None, None);
|
||||
|
||||
// Bucket hanging from center
|
||||
editor.set_block(IRON_BLOCK, x, 3, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a water tower structure
|
||||
fn generate_water_tower(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
if let Some(first_node) = element.nodes().next() {
|
||||
let x = first_node.x;
|
||||
let z = first_node.z;
|
||||
let tower_height = 20;
|
||||
let tank_height = 6;
|
||||
|
||||
// Build support legs (4 corner pillars)
|
||||
let leg_positions = [(-2, -2), (2, -2), (-2, 2), (2, 2)];
|
||||
for (dx, dz) in leg_positions {
|
||||
for y in 0..tower_height {
|
||||
editor.set_block(IRON_BLOCK, x + dx, y, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Add cross-bracing every 5 blocks for stability
|
||||
for y in (5..tower_height).step_by(5) {
|
||||
// Horizontal bracing
|
||||
for dx in -1..=1 {
|
||||
editor.set_block(SMOOTH_STONE, x + dx, y, z - 2, None, None);
|
||||
editor.set_block(SMOOTH_STONE, x + dx, y, z + 2, None, None);
|
||||
}
|
||||
for dz in -1..=1 {
|
||||
editor.set_block(SMOOTH_STONE, x - 2, y, z + dz, None, None);
|
||||
editor.set_block(SMOOTH_STONE, x + 2, y, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Build water tank at the top - simple rectangular tank
|
||||
editor.fill_blocks(
|
||||
POLISHED_ANDESITE,
|
||||
x - 3,
|
||||
tower_height,
|
||||
z - 3,
|
||||
x + 3,
|
||||
tower_height + tank_height,
|
||||
z + 3,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Add polished andesite pipe going down from the tank
|
||||
for y in 0..tower_height {
|
||||
editor.set_block(POLISHED_ANDESITE, x, y, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate man_made structures for node elements
|
||||
pub fn generate_man_made_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
if let Some(man_made_type) = node.tags.get("man_made") {
|
||||
let element = ProcessedElement::Node(node.clone());
|
||||
|
||||
match man_made_type.as_str() {
|
||||
"antenna" => generate_antenna(editor, &element),
|
||||
"chimney" => generate_chimney(editor, &element),
|
||||
"water_well" => generate_water_well(editor, &element),
|
||||
"water_tower" => generate_water_tower(editor, &element),
|
||||
"mast" => generate_antenna(editor, &element),
|
||||
_ => {} // Unknown man_made type, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ pub mod doors;
|
||||
pub mod highways;
|
||||
pub mod landuse;
|
||||
pub mod leisure;
|
||||
pub mod man_made;
|
||||
pub mod natural;
|
||||
pub mod railways;
|
||||
pub mod subprocessor;
|
||||
pub mod tourisms;
|
||||
pub mod tree;
|
||||
pub mod water_areas;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::element_processing::tree::create_tree;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_natural(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
ground_level: i32,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if let Some(natural_type) = element.tags().get("natural") {
|
||||
if natural_type == "tree" {
|
||||
@@ -19,46 +21,35 @@ pub fn generate_natural(
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
create_tree(
|
||||
editor,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
rng.gen_range(1..=3),
|
||||
args.winter,
|
||||
);
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
}
|
||||
} else {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
|
||||
let mut current_natural: Vec<(i32, i32)> = vec![];
|
||||
let binding: String = "".to_string();
|
||||
|
||||
// Determine block type based on natural tag
|
||||
let block_type: Block = match natural_type.as_str() {
|
||||
"scrub" | "grassland" | "wood" => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
}
|
||||
"beach" | "sand" => SAND,
|
||||
"tree_row" => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
}
|
||||
"wetland" | "water" => WATER,
|
||||
_ => {
|
||||
if args.winter {
|
||||
SNOW_BLOCK
|
||||
} else {
|
||||
GRASS_BLOCK
|
||||
"scrub" | "grassland" | "wood" | "heath" | "tree_row" => GRASS_BLOCK,
|
||||
"sand" | "dune" => SAND,
|
||||
"beach" | "shoal" => {
|
||||
let surface = element.tags().get("natural").unwrap_or(&binding);
|
||||
match surface.as_str() {
|
||||
"gravel" => GRAVEL,
|
||||
_ => SAND,
|
||||
}
|
||||
}
|
||||
"water" | "reef" => WATER,
|
||||
"bare_rock" => STONE,
|
||||
"blockfield" => COBBLESTONE,
|
||||
"glacier" => PACKED_ICE,
|
||||
"mud" | "wetland" => MUD,
|
||||
"mountain_range" => COBBLESTONE,
|
||||
"saddle" | "ridge" => STONE,
|
||||
"shrubbery" | "tundra" | "hill" => GRASS_BLOCK,
|
||||
"cliff" => STONE,
|
||||
_ => GRASS_BLOCK,
|
||||
};
|
||||
|
||||
let ProcessedElement::Way(way) = element else {
|
||||
@@ -73,9 +64,9 @@ pub fn generate_natural(
|
||||
if let Some(prev) = previous_node {
|
||||
// Generate the line of coordinates between the two nodes
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(prev.0, ground_level, prev.1, x, ground_level, z);
|
||||
bresenham_line(prev.0, 0, prev.1, x, 0, z);
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
editor.set_block(block_type, bx, ground_level, bz, None, None);
|
||||
editor.set_block(block_type, bx, 0, bz, None, None);
|
||||
}
|
||||
|
||||
current_natural.push((x, z));
|
||||
@@ -85,51 +76,428 @@ pub fn generate_natural(
|
||||
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) {
|
||||
let polygon_coords: Vec<(i32, i32)> = way
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_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 {
|
||||
editor.set_block(block_type, x, ground_level, z, None, None);
|
||||
|
||||
// Generate elements for "wood" and "tree_row"
|
||||
if natural_type == "wood" || natural_type == "tree_row" {
|
||||
if editor.check_for_block(x, ground_level, z, None, Some(&[WATER])) {
|
||||
continue;
|
||||
editor.set_block(block_type, x, 0, z, None, None);
|
||||
// Generate custom layer instead of dirt, must be stone on the lowest level
|
||||
match natural_type.as_str() {
|
||||
"beach" | "sand" | "dune" | "shoal" => {
|
||||
editor.set_block(SAND, x, 0, z, None, None);
|
||||
}
|
||||
|
||||
let random_choice: i32 = rng.gen_range(0..26);
|
||||
if random_choice == 25 {
|
||||
create_tree(
|
||||
editor,
|
||||
x,
|
||||
ground_level + 1,
|
||||
z,
|
||||
rng.gen_range(1..=3),
|
||||
args.winter,
|
||||
);
|
||||
} else if random_choice == 2 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, ground_level + 1, z, None, None);
|
||||
} else if random_choice <= 1 {
|
||||
editor.set_block(GRASS, x, ground_level + 1, z, None, None);
|
||||
"glacier" => {
|
||||
editor.set_block(PACKED_ICE, x, 0, z, None, None);
|
||||
editor.set_block(STONE, x, -1, z, None, None);
|
||||
}
|
||||
"bare_rock" => {
|
||||
editor.set_block(STONE, x, 0, z, None, None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Generate surface elements
|
||||
if editor.check_for_block(x, 0, z, Some(&[WATER])) {
|
||||
continue;
|
||||
}
|
||||
match natural_type.as_str() {
|
||||
"grassland" => {
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
if rng.gen_bool(0.6) {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"heath" => {
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
if random_choice < 33 {
|
||||
if random_choice <= 2 {
|
||||
editor.set_block(COBBLESTONE, x, 0, z, None, None);
|
||||
} else if random_choice < 6 {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
} else {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"scrub" => {
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if random_choice < 40 {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
if random_choice < 15 {
|
||||
editor.set_block(OAK_LEAVES, x, 2, z, None, None);
|
||||
}
|
||||
} else if random_choice < 300 {
|
||||
if random_choice < 250 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
} else {
|
||||
editor.set_block(TALL_GRASS_BOTTOM, x, 1, z, None, None);
|
||||
editor.set_block(TALL_GRASS_TOP, x, 2, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"tree_row" | "wood" => {
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if random_choice <= 12 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"sand" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[SAND]))
|
||||
&& rng.gen_range(0..100) == 1
|
||||
{
|
||||
editor.set_block(DEAD_BUSH, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"shoal" => {
|
||||
if rng.gen_bool(0.05) {
|
||||
editor.set_block(WATER, x, 0, z, Some(&[SAND, GRAVEL]), None);
|
||||
}
|
||||
}
|
||||
"wetland" => {
|
||||
if let Some(wetland_type) = element.tags().get("wetland") {
|
||||
// Wetland without water blocks
|
||||
if matches!(wetland_type.as_str(), "wet_meadow" | "fen") {
|
||||
if rng.gen_bool(0.3) {
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, Some(&[MUD]), None);
|
||||
}
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
continue;
|
||||
}
|
||||
// All the other types of wetland
|
||||
if rng.gen_bool(0.3) {
|
||||
editor.set_block(
|
||||
WATER,
|
||||
x,
|
||||
0,
|
||||
z,
|
||||
Some(&[MUD, GRASS_BLOCK]),
|
||||
None,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if !editor.check_for_block(x, 0, z, Some(&[MUD, MOSS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
match wetland_type.as_str() {
|
||||
"reedbed" => {
|
||||
editor.set_block(TALL_GRASS_BOTTOM, x, 1, z, None, None);
|
||||
editor.set_block(TALL_GRASS_TOP, x, 2, z, None, None);
|
||||
}
|
||||
"swamp" | "mangrove" => {
|
||||
// TODO implement mangrove
|
||||
let random_choice: i32 = rng.gen_range(0..40);
|
||||
if random_choice == 0 {
|
||||
Tree::create(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if random_choice < 35 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"bog" => {
|
||||
if rng.gen_bool(0.2) {
|
||||
editor.set_block(
|
||||
MOSS_BLOCK,
|
||||
x,
|
||||
0,
|
||||
z,
|
||||
Some(&[MUD]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
if rng.gen_bool(0.15) {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"tidalflat" => {
|
||||
continue; // No vegetation here
|
||||
}
|
||||
_ => {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generic natural=wetland without wetland=... tag
|
||||
if rng.gen_bool(0.3) {
|
||||
editor.set_block(WATER, x, 0, z, Some(&[MUD]), None);
|
||||
continue;
|
||||
}
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"mountain_range" => {
|
||||
// Create block clusters instead of random placement
|
||||
let cluster_chance = rng.gen_range(0..1000);
|
||||
|
||||
if cluster_chance < 50 {
|
||||
// 5% chance to start a new cluster
|
||||
let cluster_block = match rng.gen_range(0..7) {
|
||||
0 => DIRT,
|
||||
1 => STONE,
|
||||
2 => GRAVEL,
|
||||
3 => GRANITE,
|
||||
4 => DIORITE,
|
||||
5 => ANDESITE,
|
||||
_ => GRASS_BLOCK,
|
||||
};
|
||||
|
||||
// Generate cluster size (5-10 blocks radius)
|
||||
let cluster_size = rng.gen_range(5..=10);
|
||||
|
||||
// Create cluster around current position
|
||||
for dx in -(cluster_size as i32)..=(cluster_size as i32) {
|
||||
for dz in -(cluster_size as i32)..=(cluster_size as i32) {
|
||||
let cluster_x = x + dx;
|
||||
let cluster_z = z + dz;
|
||||
|
||||
// Use distance to create more natural cluster shape
|
||||
let distance = ((dx * dx + dz * dz) as f32).sqrt();
|
||||
if distance <= cluster_size as f32 {
|
||||
// Probability decreases with distance from center
|
||||
let place_prob = 1.0 - (distance / cluster_size as f32);
|
||||
if rng.gen::<f32>() < place_prob {
|
||||
editor.set_block(
|
||||
cluster_block,
|
||||
cluster_x,
|
||||
0,
|
||||
cluster_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Add vegetation on grass blocks
|
||||
if cluster_block == GRASS_BLOCK {
|
||||
let vegetation_chance = rng.gen_range(0..100);
|
||||
if vegetation_chance == 0 {
|
||||
// 1% chance for rare trees
|
||||
Tree::create(
|
||||
editor,
|
||||
(cluster_x, 1, cluster_z),
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if vegetation_chance < 15 {
|
||||
// 15% chance for grass
|
||||
editor.set_block(
|
||||
GRASS, cluster_x, 1, cluster_z, None,
|
||||
None,
|
||||
);
|
||||
} else if vegetation_chance < 25 {
|
||||
// 10% chance for oak leaves
|
||||
editor.set_block(
|
||||
OAK_LEAVES, cluster_x, 1, cluster_z,
|
||||
None, None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"saddle" => {
|
||||
// Saddle areas - lowest point between peaks, mix of stone and grass
|
||||
let terrain_chance = rng.gen_range(0..100);
|
||||
if terrain_chance < 30 {
|
||||
// 30% chance for exposed stone
|
||||
editor.set_block(STONE, x, 0, z, None, None);
|
||||
} else if terrain_chance < 50 {
|
||||
// 20% chance for gravel/rocky terrain
|
||||
editor.set_block(GRAVEL, x, 0, z, None, None);
|
||||
} else {
|
||||
// 50% chance for grass
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, None, None);
|
||||
if rng.gen_bool(0.4) {
|
||||
// 40% chance for grass on top
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"ridge" => {
|
||||
// Ridge areas - elevated crest, mostly rocky with some vegetation
|
||||
let ridge_chance = rng.gen_range(0..100);
|
||||
if ridge_chance < 60 {
|
||||
// 60% chance for stone/rocky terrain
|
||||
let rock_type = match rng.gen_range(0..4) {
|
||||
0 => STONE,
|
||||
1 => COBBLESTONE,
|
||||
2 => GRANITE,
|
||||
_ => ANDESITE,
|
||||
};
|
||||
editor.set_block(rock_type, x, 0, z, None, None);
|
||||
} else {
|
||||
// 40% chance for grass with sparse vegetation
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, None, None);
|
||||
let vegetation_chance = rng.gen_range(0..100);
|
||||
if vegetation_chance < 20 {
|
||||
// 20% chance for grass
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
} else if vegetation_chance < 25 {
|
||||
// 5% chance for small shrubs
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"shrubbery" => {
|
||||
// Manicured shrubs and decorative vegetation
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
editor.set_block(OAK_LEAVES, x, 2, z, None, None);
|
||||
}
|
||||
"tundra" => {
|
||||
// Treeless habitat with low vegetation, mosses, lichens
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let tundra_chance = rng.gen_range(0..100);
|
||||
if tundra_chance < 40 {
|
||||
// 40% chance for grass (sedges, grasses)
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
} else if tundra_chance < 60 {
|
||||
// 20% chance for moss
|
||||
editor.set_block(MOSS_BLOCK, x, 0, z, Some(&[GRASS_BLOCK]), None);
|
||||
} else if tundra_chance < 70 {
|
||||
// 10% chance for dead bush (lichens)
|
||||
editor.set_block(DEAD_BUSH, x, 1, z, None, None);
|
||||
}
|
||||
// 30% chance for bare ground (no surface block)
|
||||
}
|
||||
"cliff" => {
|
||||
// Cliff areas - predominantly stone with minimal vegetation
|
||||
let cliff_chance = rng.gen_range(0..100);
|
||||
if cliff_chance < 90 {
|
||||
// 90% chance for stone variants
|
||||
let stone_type = match rng.gen_range(0..4) {
|
||||
0 => STONE,
|
||||
1 => COBBLESTONE,
|
||||
2 => ANDESITE,
|
||||
_ => DIORITE,
|
||||
};
|
||||
editor.set_block(stone_type, x, 0, z, None, None);
|
||||
} else {
|
||||
// 10% chance for gravel/loose rock
|
||||
editor.set_block(GRAVEL, x, 0, z, None, None);
|
||||
}
|
||||
}
|
||||
"hill" => {
|
||||
// Hill areas - elevated terrain with sparse trees and mostly grass
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let hill_chance = rng.gen_range(0..1000);
|
||||
if hill_chance == 0 {
|
||||
// 0.1% chance for rare trees
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if hill_chance < 50 {
|
||||
// 5% chance for flowers
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if hill_chance < 600 {
|
||||
// 55% chance for grass
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
} else if hill_chance < 650 {
|
||||
// 5% chance for tall grass
|
||||
editor.set_block(TALL_GRASS_BOTTOM, x, 1, z, None, None);
|
||||
editor.set_block(TALL_GRASS_TOP, x, 2, z, None, None);
|
||||
}
|
||||
// 35% chance for bare grass block
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_natural_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("natural") {
|
||||
// Generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_natural(
|
||||
editor,
|
||||
&ProcessedElement::Way((*member.way).clone()),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all outer ways into one with relation tags
|
||||
let mut combined_nodes = Vec::new();
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
combined_nodes.extend(member.way.nodes.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Only process if we have nodes
|
||||
if !combined_nodes.is_empty() {
|
||||
// Create combined way with relation tags
|
||||
let combined_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: combined_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate natural area from combined way
|
||||
generate_natural(
|
||||
editor,
|
||||
&ProcessedElement::Way(combined_way),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,18 @@ use crate::bresenham::bresenham_line;
|
||||
use crate::osm_parser::ProcessedWay;
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
pub fn generate_railways(editor: &mut WorldEditor, element: &ProcessedWay, ground_level: i32) {
|
||||
pub fn generate_railways(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
if let Some(railway_type) = element.tags.get("railway") {
|
||||
if ["proposed", "abandoned", "subway", "construction"].contains(&railway_type.as_str()) {
|
||||
if [
|
||||
"proposed",
|
||||
"abandoned",
|
||||
"subway",
|
||||
"construction",
|
||||
"razed",
|
||||
"turntable",
|
||||
]
|
||||
.contains(&railway_type.as_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,25 +31,212 @@ pub fn generate_railways(editor: &mut WorldEditor, element: &ProcessedWay, groun
|
||||
}
|
||||
|
||||
for i in 1..element.nodes.len() {
|
||||
let prev: &crate::osm_parser::ProcessedNode = &element.nodes[i - 1];
|
||||
let x1: i32 = prev.x;
|
||||
let z1: i32 = prev.z;
|
||||
let prev_node = element.nodes[i - 1].xz();
|
||||
let cur_node = element.nodes[i].xz();
|
||||
|
||||
let cur: &crate::osm_parser::ProcessedNode = &element.nodes[i];
|
||||
let x2: i32 = cur.x;
|
||||
let z2: i32 = cur.z;
|
||||
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);
|
||||
|
||||
// Generate the line of coordinates between the two nodes
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(x1, ground_level, z1, x2, ground_level, z2);
|
||||
for j in 0..smoothed_points.len() {
|
||||
let (bx, _, bz) = smoothed_points[j];
|
||||
|
||||
for (bx, _, bz) in bresenham_points {
|
||||
// TODO: Set direction of rail
|
||||
editor.set_block(IRON_BLOCK, bx, ground_level, bz, None, None);
|
||||
editor.set_block(RAIL, bx, ground_level + 1, bz, None, None);
|
||||
editor.set_block(GRAVEL, bx, 0, 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)),
|
||||
);
|
||||
|
||||
editor.set_block(rail_block, bx, 1, bz, None, None);
|
||||
|
||||
if bx % 4 == 0 {
|
||||
editor.set_block(OAK_LOG, bx, ground_level, bz, None, None);
|
||||
editor.set_block(OAK_LOG, bx, 0, bz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn smooth_diagonal_rails(points: &[(i32, i32, i32)]) -> Vec<(i32, i32, i32)> {
|
||||
let mut smoothed = Vec::new();
|
||||
|
||||
for i in 0..points.len() {
|
||||
let current = points[i];
|
||||
smoothed.push(current);
|
||||
|
||||
if i + 1 >= points.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let next = points[i + 1];
|
||||
let (x1, y1, z1) = current;
|
||||
let (x2, _, z2) = next;
|
||||
|
||||
// If points are diagonally adjacent
|
||||
if (x2 - x1).abs() == 1 && (z2 - z1).abs() == 1 {
|
||||
// Look ahead to determine best intermediate point
|
||||
let look_ahead = if i + 2 < points.len() {
|
||||
Some(points[i + 2])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Look behind
|
||||
let look_behind = if i > 0 { Some(points[i - 1]) } else { None };
|
||||
|
||||
// Choose intermediate point based on the overall curve direction
|
||||
let intermediate = if let Some((prev_x, _, _prev_z)) = look_behind {
|
||||
if prev_x == x1 {
|
||||
// Coming from vertical, keep x constant
|
||||
(x1, y1, z2)
|
||||
} else {
|
||||
// Coming from horizontal, keep z constant
|
||||
(x2, y1, z1)
|
||||
}
|
||||
} else if let Some((next_x, _, _next_z)) = look_ahead {
|
||||
if next_x == x2 {
|
||||
// Going to vertical, keep x constant
|
||||
(x2, y1, z1)
|
||||
} else {
|
||||
// Going to horizontal, keep z constant
|
||||
(x1, y1, z2)
|
||||
}
|
||||
} else {
|
||||
// Default to horizontal first if no context
|
||||
(x2, y1, z1)
|
||||
};
|
||||
|
||||
smoothed.push(intermediate);
|
||||
}
|
||||
}
|
||||
|
||||
smoothed
|
||||
}
|
||||
|
||||
fn determine_rail_direction(
|
||||
current: (i32, i32),
|
||||
prev: Option<(i32, i32)>,
|
||||
next: Option<(i32, i32)>,
|
||||
) -> Block {
|
||||
let (x, z) = current;
|
||||
|
||||
match (prev, next) {
|
||||
(Some((px, pz)), Some((nx, nz))) => {
|
||||
if px == nx {
|
||||
RAIL_NORTH_SOUTH
|
||||
} else if pz == nz {
|
||||
RAIL_EAST_WEST
|
||||
} else {
|
||||
// Calculate relative movements
|
||||
let from_prev = (px - x, pz - z);
|
||||
let to_next = (nx - x, nz - z);
|
||||
|
||||
match (from_prev, to_next) {
|
||||
// East to North or North to East
|
||||
((-1, 0), (0, -1)) | ((0, -1), (-1, 0)) => RAIL_NORTH_WEST,
|
||||
// West to North or North to West
|
||||
((1, 0), (0, -1)) | ((0, -1), (1, 0)) => RAIL_NORTH_EAST,
|
||||
// East to South or South to East
|
||||
((-1, 0), (0, 1)) | ((0, 1), (-1, 0)) => RAIL_SOUTH_WEST,
|
||||
// West to South or South to West
|
||||
((1, 0), (0, 1)) | ((0, 1), (1, 0)) => RAIL_SOUTH_EAST,
|
||||
_ => {
|
||||
if (px - x).abs() > (pz - z).abs() {
|
||||
RAIL_EAST_WEST
|
||||
} else {
|
||||
RAIL_NORTH_SOUTH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some((px, pz)), None) | (None, Some((px, pz))) => {
|
||||
if px == x {
|
||||
RAIL_NORTH_SOUTH
|
||||
} else if pz == z {
|
||||
RAIL_EAST_WEST
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
303
src/element_processing/subprocessor/buildings_interior.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
use crate::block_definitions::*;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Interior layout for building ground floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const INTERIOR1_LAYER1: [[char; 23]; 23] = [
|
||||
['1', 'U', ' ', 'W', 'C', ' ', ' ', ' ', 'S', 'S', 'W', 'B', 'T', 'T', 'B', 'W', '7', '8', ' ', ' ', ' ', ' ', 'W',],
|
||||
['2', ' ', ' ', 'W', 'F', ' ', ' ', ' ', 'U', 'U', 'W', 'B', 'T', 'T', 'B', 'W', '7', '8', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', 'F', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'T', 'T', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'A', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', 'J', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', 'T', 'S', 'S', 'T', ' ', ' ', 'W', 'S', 'S', ' ', 'B', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'T', 'T', 'T', 'T', ' ', ' ', 'W', 'U', 'U', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', 'T', 'T', 'T', 'T', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
|
||||
['L', ' ', 'A', 'L', 'W', 'W', ' ', ' ', 'W', 'J', 'U', 'U', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', '6', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['U', '5', ' ', 'W', ' ', ' ', 'W', 'C', 'F', 'F', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', 'W', 'A', ' ', 'B', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', 'J', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'U', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
['J', ' ', ' ', 'C', 'B', 'B', 'W', 'L', 'F', ' ', 'W', 'F', ' ', 'W', 'L', 'W', '7', '8', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', 'A', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'C', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'C', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', 'C', ' ', ' ', ' ', 'W', 'W', 'W', 'B', 'T', 'T', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building ground floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||
[' ', 'P', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'P', 'P', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', 'P', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', ' ', 'P', 'P', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['P', ' ', ' ', 'W', ' ', ' ', 'W', 'N', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'C', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'P', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'B', 'B', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'P', 'W', ' ', ' ', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'N', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (1nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['U', ' ', ' ', ' ', ' ', ' ', 'C', 'W', 'L', ' ', ' ', 'L', 'W', 'A', 'A', 'W', ' ', ' ', ' ', ' ', ' ', 'L', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'S', 'S', 'S', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'J', ' ', 'U', 'U', 'U', ' ', 'D',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', 'T', 'T', 'W', ' ', ' ', ' ', ' ', ' ', 'U', 'W', ' ', 'L', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'T', 'J', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', 'L', ' ', 'W',],
|
||||
['J', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', 'A', 'B', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'B', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'U', ' ', ' ', ' ', 'D', ' ', ' ', 'F', 'F', 'W', 'A', 'A', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'U', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', 'C', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['C', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'L', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'U', 'U', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'U', 'U', ' ', 'W', 'B', ' ', 'U', 'U', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['S', 'S', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', 'W',],
|
||||
['U', 'U', ' ', ' ', ' ', 'L', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', 'D', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['P', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'E', ' ', ' ', 'E', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'E', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', 'P', 'P', ' ', 'D',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'P', 'W', ' ', 'P', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'E', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', 'B', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'B', 'W', 'E', ' ', ' ', ' ', ' ', 'W', 'E', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'P', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'E', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['E', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'E', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'P', 'P', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'P', 'P', ' ', 'W', 'B', ' ', 'P', 'P', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', 'W',],
|
||||
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
|
||||
];
|
||||
|
||||
/// Maps interior layout characters to actual block types for different floor layers
|
||||
#[inline(always)]
|
||||
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
|
||||
match c {
|
||||
' ' => None, // Nothing
|
||||
'W' => Some(wall_block), // Use the building's wall block for interior walls
|
||||
'U' => Some(OAK_FENCE), // Oak Fence
|
||||
'S' => Some(OAK_STAIRS), // Oak Stairs
|
||||
'B' => Some(BOOKSHELF), // Bookshelf
|
||||
'C' => Some(CRAFTING_TABLE), // Crafting Table
|
||||
'F' => Some(FURNACE), // Furnace
|
||||
'1' => Some(RED_BED_NORTH_HEAD), // Bed North Head
|
||||
'2' => Some(RED_BED_NORTH_FOOT), // Bed North Foot
|
||||
'3' => Some(RED_BED_EAST_HEAD), // Bed East Head
|
||||
'4' => Some(RED_BED_EAST_FOOT), // Bed East Foot
|
||||
'5' => Some(RED_BED_SOUTH_HEAD), // Bed South Head
|
||||
'6' => Some(RED_BED_SOUTH_FOOT), // Bed South Foot
|
||||
'7' => Some(RED_BED_WEST_HEAD), // Bed West Head
|
||||
'8' => Some(RED_BED_WEST_FOOT), // Bed West Foot
|
||||
// 'H' => Some(CHEST), // Chest
|
||||
'L' => Some(CAULDRON), // Cauldron
|
||||
'A' => Some(ANVIL), // Anvil
|
||||
'P' => Some(OAK_PRESSURE_PLATE), // Pressure Plate
|
||||
'D' => {
|
||||
// Use different door types for different layers
|
||||
if is_layer2 {
|
||||
Some(DARK_OAK_DOOR_UPPER)
|
||||
} else {
|
||||
Some(DARK_OAK_DOOR_LOWER)
|
||||
}
|
||||
}
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||
_ => None, // Default case for unknown characters
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates interior layouts inside buildings at each floor level
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn generate_building_interior(
|
||||
editor: &mut WorldEditor,
|
||||
floor_area: &[(i32, i32)],
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
max_x: i32,
|
||||
max_z: i32,
|
||||
start_y_offset: i32,
|
||||
building_height: i32,
|
||||
wall_block: Block,
|
||||
floor_levels: &[i32],
|
||||
args: &crate::args::Args,
|
||||
element: &crate::osm_parser::ProcessedWay,
|
||||
abs_terrain_offset: i32,
|
||||
) {
|
||||
// Skip interior generation for very small buildings
|
||||
let width = max_x - min_x + 1;
|
||||
let depth = max_z - min_z + 1;
|
||||
|
||||
if width < 8 || depth < 8 {
|
||||
return; // Building too small for interior
|
||||
}
|
||||
|
||||
// For efficiency, create a HashSet of floor area coordinates
|
||||
let floor_area_set: HashSet<(i32, i32)> = floor_area.iter().cloned().collect();
|
||||
|
||||
// Add buffer around edges to avoid placing furniture too close to walls
|
||||
let buffer = 2;
|
||||
let interior_min_x = min_x + buffer;
|
||||
let interior_min_z = min_z + buffer;
|
||||
let interior_max_x = max_x - buffer;
|
||||
let interior_max_z = max_z - buffer;
|
||||
|
||||
// Generate interiors for each floor
|
||||
for (floor_index, &floor_y) in floor_levels.iter().enumerate() {
|
||||
// Store wall and door positions for this floor to extend them to the ceiling
|
||||
let mut wall_positions = Vec::new();
|
||||
let mut door_positions = Vec::new();
|
||||
|
||||
// Determine the floor extension height (ceiling) - either next floor or roof
|
||||
let current_floor_ceiling = if floor_index < floor_levels.len() - 1 {
|
||||
// For intermediate floors, extend walls up to just below the next floor
|
||||
floor_levels[floor_index + 1] - 1
|
||||
} else {
|
||||
// Last floor ceiling depends on roof generation
|
||||
if args.roof
|
||||
&& element.tags.contains_key("roof:shape")
|
||||
&& element.tags.get("roof:shape").unwrap() != "flat"
|
||||
{
|
||||
// When roof generation is enabled with non-flat roofs, stop at building height (no extra ceiling)
|
||||
start_y_offset + building_height
|
||||
} else {
|
||||
// When roof generation is disabled or flat roof, extend to building top + 1 (includes ceiling)
|
||||
start_y_offset + building_height + 1
|
||||
}
|
||||
};
|
||||
|
||||
// Choose the appropriate interior pattern based on floor number
|
||||
let (layer1, layer2) = if floor_index == 0 {
|
||||
// Ground floor uses INTERIOR1 patterns
|
||||
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
||||
} else {
|
||||
// Upper floors use INTERIOR2 patterns
|
||||
(&INTERIOR2_LAYER1, &INTERIOR2_LAYER2)
|
||||
};
|
||||
|
||||
// Get dimensions for the selected pattern
|
||||
let pattern_height = layer1.len() as i32;
|
||||
let pattern_width = layer1[0].len() as i32;
|
||||
|
||||
// Calculate Y offset - place interior 1 block above floor level consistently
|
||||
let y_offset = 1;
|
||||
|
||||
// Create a seamless repeating pattern across the interior of this floor
|
||||
for z in interior_min_z..=interior_max_z {
|
||||
for x in interior_min_x..=interior_max_x {
|
||||
// Skip if outside the building's floor area
|
||||
if !floor_area_set.contains(&(x, z)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Map the world coordinates to pattern coordinates using modulo
|
||||
// This creates a seamless tiling effect across the entire building
|
||||
// Add floor_index offset to create variation between floors
|
||||
let pattern_x = ((x - interior_min_x + floor_index as i32) % pattern_width
|
||||
+ pattern_width)
|
||||
% pattern_width;
|
||||
let pattern_z = ((z - interior_min_z + floor_index as i32) % pattern_height
|
||||
+ pattern_height)
|
||||
% pattern_height;
|
||||
|
||||
// Access the pattern arrays safely
|
||||
let cell1 = layer1[pattern_z as usize][pattern_x as usize];
|
||||
let cell2 = layer2[pattern_z as usize][pattern_x as usize];
|
||||
|
||||
// Place first layer blocks
|
||||
if let Some(block) = get_interior_block(cell1, false, wall_block) {
|
||||
editor.set_block_absolute(
|
||||
block,
|
||||
x,
|
||||
floor_y + y_offset + abs_terrain_offset,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// If this is a wall in layer 1, add to wall positions to extend later
|
||||
if cell1 == 'W' {
|
||||
wall_positions.push((x, z));
|
||||
}
|
||||
// If this is a door in layer 1, add to door positions to add wall above later
|
||||
else if cell1 == 'D' {
|
||||
door_positions.push((x, z));
|
||||
}
|
||||
}
|
||||
|
||||
// Place second layer blocks
|
||||
if let Some(block) = get_interior_block(cell2, true, wall_block) {
|
||||
editor.set_block_absolute(
|
||||
block,
|
||||
x,
|
||||
floor_y + y_offset + abs_terrain_offset + 1,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extend walls all the way to the next floor ceiling or roof
|
||||
for (x, z) in &wall_positions {
|
||||
for y in (floor_y + y_offset + 2)..=current_floor_ceiling {
|
||||
editor.set_block_absolute(wall_block, *x, y + abs_terrain_offset, *z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Add wall blocks above doors all the way to the ceiling/next floor
|
||||
for (x, z) in &door_positions {
|
||||
for y in (floor_y + y_offset + 2)..=current_floor_ceiling {
|
||||
editor.set_block_absolute(wall_block, *x, y + abs_terrain_offset, *z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/element_processing/subprocessor/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod buildings_interior;
|
||||
@@ -2,7 +2,7 @@ use crate::block_definitions::*;
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
pub fn generate_tourisms(editor: &mut WorldEditor, element: &ProcessedNode, ground_level: i32) {
|
||||
pub fn generate_tourisms(editor: &mut WorldEditor, element: &ProcessedNode) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags.get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
@@ -21,9 +21,13 @@ pub fn generate_tourisms(editor: &mut WorldEditor, element: &ProcessedNode, grou
|
||||
let z: i32 = element.z;
|
||||
|
||||
if tourism_type == "information" {
|
||||
if let Some("board") = element.tags.get("information").map(|x: &String| x.as_str()) {
|
||||
// TODO draw a sign
|
||||
editor.set_block(OAK_PLANKS, x, ground_level + 1, z, None, None);
|
||||
if let Some(info_type) = element.tags.get("information").map(|x: &String| x.as_str()) {
|
||||
if info_type != "office" && info_type != "visitor_centre" {
|
||||
// Draw an information board
|
||||
// TODO draw a sign with text if provided
|
||||
editor.set_block(COBBLESTONE_WALL, x, 1, z, None, None);
|
||||
editor.set_block(OAK_PLANKS, x, 2, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +1,364 @@
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::coord_rng;
|
||||
use crate::floodfill_cache::BuildingFootprintBitmap;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
/// Helper function to set blocks in a circular pattern around a central point.
|
||||
fn round1(editor: &mut WorldEditor, material: Block, x: i32, y: i32, z: i32) {
|
||||
editor.set_block(material, x - 2, y, z, None, None);
|
||||
editor.set_block(material, x + 2, y, z, None, None);
|
||||
editor.set_block(material, x, y, z - 2, None, None);
|
||||
editor.set_block(material, x, y, z + 2, None, None);
|
||||
editor.set_block(material, x - 1, y, z - 1, None, None);
|
||||
editor.set_block(material, x + 1, y, z + 1, None, None);
|
||||
editor.set_block(material, x + 1, y, z - 1, None, None);
|
||||
editor.set_block(material, x - 1, y, z + 1, None, None);
|
||||
}
|
||||
type Coord = (i32, i32, i32);
|
||||
|
||||
/// Helper function to set blocks in a wider circular pattern.
|
||||
fn round2(editor: &mut WorldEditor, material: Block, x: i32, y: i32, z: i32) {
|
||||
editor.set_block(material, x + 3, y, z, None, None);
|
||||
editor.set_block(material, x + 2, y, z - 1, None, None);
|
||||
editor.set_block(material, x + 2, y, z + 1, None, None);
|
||||
editor.set_block(material, x + 1, y, z - 2, None, None);
|
||||
editor.set_block(material, x + 1, y, z + 2, None, None);
|
||||
editor.set_block(material, x - 3, y, z, None, None);
|
||||
editor.set_block(material, x - 2, y, z - 1, None, None);
|
||||
editor.set_block(material, x - 2, y, z + 1, None, None);
|
||||
editor.set_block(material, x - 1, y, z + 2, None, None);
|
||||
editor.set_block(material, x - 1, y, z - 2, None, None);
|
||||
editor.set_block(material, x, y, z - 3, None, None);
|
||||
editor.set_block(material, x, y, z + 3, None, None);
|
||||
}
|
||||
// TODO all this data would probably be better suited in a TOML file or something.
|
||||
|
||||
/// Helper function to set blocks in a more scattered circular pattern.
|
||||
fn round3(editor: &mut WorldEditor, material: Block, x: i32, y: i32, z: i32) {
|
||||
editor.set_block(material, x + 3, y, z - 1, None, None);
|
||||
editor.set_block(material, x + 3, y, z + 1, None, None);
|
||||
editor.set_block(material, x + 2, y, z - 2, None, None);
|
||||
editor.set_block(material, x + 2, y, z + 2, None, None);
|
||||
editor.set_block(material, x + 1, y, z - 3, None, None);
|
||||
editor.set_block(material, x + 1, y, z + 3, None, None);
|
||||
editor.set_block(material, x - 3, y, z - 1, None, None);
|
||||
editor.set_block(material, x - 3, y, z + 1, None, None);
|
||||
editor.set_block(material, x - 2, y, z - 2, None, None);
|
||||
editor.set_block(material, x - 2, y, z + 2, None, None);
|
||||
editor.set_block(material, x - 1, y, z + 3, None, None);
|
||||
editor.set_block(material, x - 1, y, z - 3, None, None);
|
||||
}
|
||||
/// A circular pattern around a central point.
|
||||
#[rustfmt::skip]
|
||||
const ROUND1_PATTERN: [Coord; 8] = [
|
||||
(-2, 0, 0),
|
||||
(2, 0, 0),
|
||||
(0, 0, -2),
|
||||
(0, 0, 2),
|
||||
(-1, 0, -1),
|
||||
(1, 0, 1),
|
||||
(1, 0, -1),
|
||||
(-1, 0, 1),
|
||||
];
|
||||
|
||||
/// Function to create different types of trees.
|
||||
pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u8, snow: bool) {
|
||||
let mut blacklist: Vec<Block> = Vec::new();
|
||||
blacklist.extend(building_corner_variations());
|
||||
blacklist.extend(building_wall_variations());
|
||||
blacklist.extend(building_floor_variations());
|
||||
blacklist.push(WATER);
|
||||
/// A wider circular pattern.
|
||||
const ROUND2_PATTERN: [Coord; 12] = [
|
||||
(3, 0, 0),
|
||||
(2, 0, -1),
|
||||
(2, 0, 1),
|
||||
(1, 0, -2),
|
||||
(1, 0, 2),
|
||||
(-3, 0, 0),
|
||||
(-2, 0, -1),
|
||||
(-2, 0, 1),
|
||||
(-1, 0, 2),
|
||||
(-1, 0, -2),
|
||||
(0, 0, -3),
|
||||
(0, 0, 3),
|
||||
];
|
||||
|
||||
if editor.check_for_block(x, y - 1, z, None, Some(&blacklist)) {
|
||||
return;
|
||||
}
|
||||
/// A more scattered circular pattern.
|
||||
const ROUND3_PATTERN: [Coord; 12] = [
|
||||
(3, 0, -1),
|
||||
(3, 0, 1),
|
||||
(2, 0, -2),
|
||||
(2, 0, 2),
|
||||
(1, 0, -3),
|
||||
(1, 0, 3),
|
||||
(-3, 0, -1),
|
||||
(-3, 0, 1),
|
||||
(-2, 0, -2),
|
||||
(-2, 0, 2),
|
||||
(-1, 0, 3),
|
||||
(-1, 0, -3),
|
||||
];
|
||||
|
||||
match typetree {
|
||||
1 => {
|
||||
// Oak tree
|
||||
editor.fill_blocks(OAK_LOG, x, y, z, x, y + 8, z, None, None);
|
||||
editor.fill_blocks(OAK_LEAVES, x - 1, y + 3, z, x - 1, y + 9, z, None, None);
|
||||
editor.fill_blocks(OAK_LEAVES, x + 1, y + 3, z, x + 1, y + 9, z, None, None);
|
||||
editor.fill_blocks(OAK_LEAVES, x, y + 3, z - 1, x, y + 9, z - 1, None, None);
|
||||
editor.fill_blocks(OAK_LEAVES, x, y + 3, z + 1, x, y + 9, z + 1, None, None);
|
||||
editor.fill_blocks(OAK_LEAVES, x, y + 9, z, x, y + 10, z, None, None);
|
||||
round1(editor, OAK_LEAVES, x, y + 8, z);
|
||||
round1(editor, OAK_LEAVES, x, y + 7, z);
|
||||
round1(editor, OAK_LEAVES, x, y + 6, z);
|
||||
round1(editor, OAK_LEAVES, x, y + 5, z);
|
||||
round1(editor, OAK_LEAVES, x, y + 4, z);
|
||||
round1(editor, OAK_LEAVES, x, y + 3, z);
|
||||
round2(editor, OAK_LEAVES, x, y + 7, z);
|
||||
round2(editor, OAK_LEAVES, x, y + 6, z);
|
||||
round2(editor, OAK_LEAVES, x, y + 5, z);
|
||||
round2(editor, OAK_LEAVES, x, y + 4, z);
|
||||
round3(editor, OAK_LEAVES, x, y + 6, z);
|
||||
round3(editor, OAK_LEAVES, x, y + 5, z);
|
||||
/// Used for iterating over each of the round patterns
|
||||
const ROUND_PATTERNS: [&[Coord]; 3] = [&ROUND1_PATTERN, &ROUND2_PATTERN, &ROUND3_PATTERN];
|
||||
|
||||
if snow {
|
||||
editor.set_block(SNOW_LAYER, x, y + 11, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x + 1, y + 10, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x - 1, y + 10, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x, y + 10, z - 1, None, None);
|
||||
editor.set_block(SNOW_LAYER, x, y + 10, z + 1, None, None);
|
||||
round1(editor, SNOW_LAYER, x, y + 9, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 8, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 7, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 6, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 8, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 7, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 6, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 5, z);
|
||||
round3(editor, SNOW_LAYER, x, y + 7, z);
|
||||
round3(editor, SNOW_LAYER, x, y + 6, z);
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// Spruce tree
|
||||
editor.fill_blocks(SPRUCE_LOG, x, y, z, x, y + 9, z, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x - 1, y + 3, z, x - 1, y + 10, z, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x + 1, y + 3, z, x + 1, y + 10, z, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x, y + 3, z - 1, x, y + 10, z - 1, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x, y + 3, z + 1, x, y + 10, z + 1, None, None);
|
||||
editor.set_block(BIRCH_LEAVES, x, y + 10, z, None, None);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 9, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 7, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 6, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 4, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 3, z);
|
||||
round2(editor, BIRCH_LEAVES, x, y + 6, z);
|
||||
round2(editor, BIRCH_LEAVES, x, y + 3, z);
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
if snow {
|
||||
editor.set_block(SNOW_LAYER, x, y + 11, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x + 1, y + 11, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x - 1, y + 11, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x, y + 11, z - 1, None, None);
|
||||
editor.set_block(SNOW_LAYER, x, y + 11, z + 1, None, None);
|
||||
round1(editor, SNOW_LAYER, x, y + 10, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 8, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 7, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 5, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 4, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 7, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 4, z);
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
// Birch tree
|
||||
editor.fill_blocks(BIRCH_LOG, x, y, z, x, y + 6, z, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x - 1, y + 2, z, x - 1, y + 7, z, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x + 1, y + 2, z, x + 1, y + 7, z, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x, y + 2, z - 1, x, y + 7, z - 1, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x, y + 2, z + 1, x, y + 7, z + 1, None, None);
|
||||
editor.fill_blocks(BIRCH_LEAVES, x, y + 7, z, x, y + 8, z, None, None);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 6, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 5, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 4, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 3, z);
|
||||
round1(editor, BIRCH_LEAVES, x, y + 2, z);
|
||||
round2(editor, BIRCH_LEAVES, x, y + 2, z);
|
||||
round2(editor, BIRCH_LEAVES, x, y + 3, z);
|
||||
round2(editor, BIRCH_LEAVES, x, y + 4, z);
|
||||
const OAK_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||
((-1, 3, 0), (-1, 9, 0)),
|
||||
((1, 3, 0), (1, 9, 0)),
|
||||
((0, 3, -1), (0, 9, -1)),
|
||||
((0, 3, 1), (0, 9, 1)),
|
||||
((0, 9, 0), (0, 10, 0)),
|
||||
];
|
||||
|
||||
if snow {
|
||||
editor.set_block(SNOW_LAYER, x, y + 9, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x + 1, y + 8, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x - 1, y + 8, z, None, None);
|
||||
editor.set_block(SNOW_LAYER, x, y + 8, z - 1, None, None);
|
||||
editor.set_block(SNOW_LAYER, x, y + 8, z + 1, None, None);
|
||||
round1(editor, SNOW_LAYER, x, y + 7, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 6, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 5, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 4, z);
|
||||
round1(editor, SNOW_LAYER, x, y + 3, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 3, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 4, z);
|
||||
round2(editor, SNOW_LAYER, x, y + 5, z);
|
||||
}
|
||||
}
|
||||
_ => {} // Do nothing if typetree is not recognized
|
||||
const SPRUCE_LEAVES_FILL: [(Coord, Coord); 6] = [
|
||||
((-1, 3, 0), (-1, 10, 0)),
|
||||
((0, 3, -1), (0, 10, -1)),
|
||||
((1, 3, 0), (1, 10, 0)),
|
||||
((0, 3, -1), (0, 10, -1)),
|
||||
((0, 3, 1), (0, 10, 1)),
|
||||
((0, 11, 0), (0, 11, 0)),
|
||||
];
|
||||
|
||||
const BIRCH_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||
((-1, 2, 0), (-1, 7, 0)),
|
||||
((1, 2, 0), (1, 7, 0)),
|
||||
((0, 2, -1), (0, 7, -1)),
|
||||
((0, 2, 1), (0, 7, 1)),
|
||||
((0, 7, 0), (0, 8, 0)),
|
||||
];
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
/// Helper function to set blocks in various patterns.
|
||||
fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_pattern: &[Coord]) {
|
||||
for (i, j, k) in block_pattern {
|
||||
editor.set_block(material, x + i, y + j, z + k, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TreeType {
|
||||
Oak,
|
||||
Spruce,
|
||||
Birch,
|
||||
}
|
||||
|
||||
// TODO what should be moved in, and what should be referenced?
|
||||
pub struct Tree<'a> {
|
||||
// kind: TreeType, // NOTE: Not actually necessary to store!
|
||||
log_block: Block,
|
||||
log_height: i32,
|
||||
leaves_block: Block,
|
||||
leaves_fill: &'a [(Coord, Coord)],
|
||||
round_ranges: [Vec<i32>; 3],
|
||||
}
|
||||
|
||||
impl Tree<'_> {
|
||||
/// 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();
|
||||
blacklist.extend(Self::get_building_wall_blocks());
|
||||
blacklist.extend(Self::get_building_floor_blocks());
|
||||
blacklist.extend(Self::get_structural_blocks());
|
||||
blacklist.extend(Self::get_functional_blocks());
|
||||
blacklist.push(WATER);
|
||||
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
||||
1 => TreeType::Oak,
|
||||
2 => TreeType::Spruce,
|
||||
3 => TreeType::Birch,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
|
||||
// Build the logs
|
||||
editor.fill_blocks(
|
||||
tree.log_block,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
x,
|
||||
y + tree.log_height,
|
||||
z,
|
||||
None,
|
||||
Some(&blacklist),
|
||||
);
|
||||
|
||||
// Fill in the leaves
|
||||
for ((i1, j1, k1), (i2, j2, k2)) in tree.leaves_fill {
|
||||
editor.fill_blocks(
|
||||
tree.leaves_block,
|
||||
x + i1,
|
||||
y + j1,
|
||||
z + k1,
|
||||
x + i2,
|
||||
y + j2,
|
||||
z + k2,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Do the three rounds
|
||||
for (round_range, round_pattern) in tree.round_ranges.iter().zip(ROUND_PATTERNS) {
|
||||
for offset in round_range {
|
||||
round(editor, tree.leaves_block, (x, y + offset, z), round_pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tree(kind: TreeType) -> Self {
|
||||
match kind {
|
||||
TreeType::Oak => Self {
|
||||
// kind,
|
||||
log_block: OAK_LOG,
|
||||
log_height: 8,
|
||||
leaves_block: OAK_LEAVES,
|
||||
leaves_fill: &OAK_LEAVES_FILL,
|
||||
round_ranges: [
|
||||
(3..=8).rev().collect(),
|
||||
(4..=7).rev().collect(),
|
||||
(5..=6).rev().collect(),
|
||||
],
|
||||
},
|
||||
|
||||
TreeType::Spruce => Self {
|
||||
// kind,
|
||||
log_block: SPRUCE_LOG,
|
||||
log_height: 9,
|
||||
leaves_block: BIRCH_LEAVES, // TODO Is this correct?
|
||||
leaves_fill: &SPRUCE_LEAVES_FILL,
|
||||
// TODO can I omit the third empty vec? May cause issues with iter zip
|
||||
round_ranges: [vec![9, 7, 6, 4, 3], vec![6, 3], vec![]],
|
||||
},
|
||||
|
||||
TreeType::Birch => Self {
|
||||
// kind,
|
||||
log_block: BIRCH_LOG,
|
||||
log_height: 6,
|
||||
leaves_block: BIRCH_LEAVES,
|
||||
leaves_fill: &BIRCH_LEAVES_FILL,
|
||||
round_ranges: [(2..=6).rev().collect(), (2..=4).collect(), vec![]],
|
||||
},
|
||||
} // match
|
||||
} // fn get_tree
|
||||
|
||||
/// Get all possible building wall blocks
|
||||
fn get_building_wall_blocks() -> Vec<Block> {
|
||||
vec![
|
||||
BLACKSTONE,
|
||||
BLACK_TERRACOTTA,
|
||||
BRICK,
|
||||
BROWN_CONCRETE,
|
||||
BROWN_TERRACOTTA,
|
||||
DEEPSLATE_BRICKS,
|
||||
END_STONE_BRICKS,
|
||||
GRAY_CONCRETE,
|
||||
GRAY_TERRACOTTA,
|
||||
LIGHT_BLUE_TERRACOTTA,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
MUD_BRICKS,
|
||||
NETHER_BRICK,
|
||||
NETHERITE_BLOCK,
|
||||
POLISHED_ANDESITE,
|
||||
POLISHED_BLACKSTONE,
|
||||
POLISHED_BLACKSTONE_BRICKS,
|
||||
POLISHED_DEEPSLATE,
|
||||
POLISHED_GRANITE,
|
||||
QUARTZ_BLOCK,
|
||||
QUARTZ_BRICKS,
|
||||
SANDSTONE,
|
||||
SMOOTH_SANDSTONE,
|
||||
SMOOTH_STONE,
|
||||
STONE_BRICKS,
|
||||
WHITE_CONCRETE,
|
||||
WHITE_TERRACOTTA,
|
||||
ORANGE_TERRACOTTA,
|
||||
GREEN_STAINED_HARDENED_CLAY,
|
||||
BLUE_TERRACOTTA,
|
||||
YELLOW_TERRACOTTA,
|
||||
BLACK_CONCRETE,
|
||||
WHITE_CONCRETE,
|
||||
GRAY_CONCRETE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
BROWN_CONCRETE,
|
||||
RED_CONCRETE,
|
||||
ORANGE_TERRACOTTA,
|
||||
YELLOW_CONCRETE,
|
||||
LIME_CONCRETE,
|
||||
GREEN_STAINED_HARDENED_CLAY,
|
||||
CYAN_CONCRETE,
|
||||
LIGHT_BLUE_CONCRETE,
|
||||
BLUE_CONCRETE,
|
||||
PURPLE_CONCRETE,
|
||||
MAGENTA_CONCRETE,
|
||||
RED_TERRACOTTA,
|
||||
]
|
||||
}
|
||||
|
||||
/// Get all possible building floor blocks
|
||||
fn get_building_floor_blocks() -> Vec<Block> {
|
||||
vec![
|
||||
GRAY_CONCRETE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
WHITE_CONCRETE,
|
||||
SMOOTH_STONE,
|
||||
POLISHED_ANDESITE,
|
||||
STONE_BRICKS,
|
||||
]
|
||||
}
|
||||
|
||||
/// Get structural blocks (fences, walls, stairs, slabs, rails, etc.)
|
||||
fn get_structural_blocks() -> Vec<Block> {
|
||||
vec![
|
||||
// Fences
|
||||
OAK_FENCE,
|
||||
// Walls
|
||||
COBBLESTONE_WALL,
|
||||
ANDESITE_WALL,
|
||||
STONE_BRICK_WALL,
|
||||
// Stairs
|
||||
OAK_STAIRS,
|
||||
// Slabs
|
||||
OAK_SLAB,
|
||||
STONE_BLOCK_SLAB,
|
||||
STONE_BRICK_SLAB,
|
||||
// Rails
|
||||
RAIL,
|
||||
RAIL_NORTH_SOUTH,
|
||||
RAIL_EAST_WEST,
|
||||
RAIL_ASCENDING_EAST,
|
||||
RAIL_ASCENDING_WEST,
|
||||
RAIL_ASCENDING_NORTH,
|
||||
RAIL_ASCENDING_SOUTH,
|
||||
RAIL_NORTH_EAST,
|
||||
RAIL_NORTH_WEST,
|
||||
RAIL_SOUTH_EAST,
|
||||
RAIL_SOUTH_WEST,
|
||||
// Doors and trapdoors
|
||||
OAK_DOOR,
|
||||
DARK_OAK_DOOR_LOWER,
|
||||
DARK_OAK_DOOR_UPPER,
|
||||
OAK_TRAPDOOR,
|
||||
// Ladders
|
||||
LADDER,
|
||||
]
|
||||
}
|
||||
|
||||
/// Get functional blocks (furniture, decorative items, etc.)
|
||||
fn get_functional_blocks() -> Vec<Block> {
|
||||
vec![
|
||||
// Furniture and functional blocks
|
||||
CHEST,
|
||||
CRAFTING_TABLE,
|
||||
FURNACE,
|
||||
ANVIL,
|
||||
BREWING_STAND,
|
||||
NOTE_BLOCK,
|
||||
BOOKSHELF,
|
||||
CAULDRON,
|
||||
// Beds
|
||||
RED_BED_NORTH_HEAD,
|
||||
RED_BED_NORTH_FOOT,
|
||||
RED_BED_EAST_HEAD,
|
||||
RED_BED_EAST_FOOT,
|
||||
RED_BED_SOUTH_HEAD,
|
||||
RED_BED_SOUTH_FOOT,
|
||||
RED_BED_WEST_HEAD,
|
||||
RED_BED_WEST_FOOT,
|
||||
// Pressure plates and signs
|
||||
OAK_PRESSURE_PLATE,
|
||||
SIGN,
|
||||
// Glass blocks (windows)
|
||||
GLASS,
|
||||
WHITE_STAINED_GLASS,
|
||||
GRAY_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
TINTED_GLASS,
|
||||
// Carpets
|
||||
WHITE_CARPET,
|
||||
RED_CARPET,
|
||||
// Other structural/building blocks
|
||||
IRON_BARS,
|
||||
IRON_BLOCK,
|
||||
SCAFFOLDING,
|
||||
BEDROCK,
|
||||
]
|
||||
}
|
||||
} // impl Tree
|
||||
|
||||