├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── py ├── build_night.py ├── cleanup.py ├── convert_biomes_amidst.py ├── convert_block_naturality.py ├── cut_tiles.py ├── extract_regions.py ├── image_from_tiles.py ├── jm-to-vm-tiles.py ├── merge_all.py ├── merge_tile_images.py ├── rezip_cache.py └── zoom.py ├── src ├── bin │ ├── blockcount.rs │ ├── merge_caches.rs │ ├── palette.rs │ ├── render.rs │ └── replay.rs ├── biomes.rs ├── buf_rw.rs ├── ccnatural.rs ├── colorizer.rs ├── lib.rs ├── mc │ ├── blocks.rs │ ├── mod.rs │ └── packet.rs ├── replay.rs └── tile.rs ├── voxelmap-cache-format.md └── xaero-format.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | *.png 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "adler32" 5 | version = "1.0.4" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" 8 | 9 | [[package]] 10 | name = "aho-corasick" 11 | version = "0.7.6" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" 14 | dependencies = [ 15 | "memchr", 16 | ] 17 | 18 | [[package]] 19 | name = "backtrace" 20 | version = "0.3.41" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "a4ed64ae6d9ebfd9893193c4b2532b1292ec97bd8271c9d7d0fa90cd78a34cba" 23 | dependencies = [ 24 | "backtrace-sys", 25 | "cfg-if 0.1.10", 26 | "libc", 27 | "rustc-demangle", 28 | ] 29 | 30 | [[package]] 31 | name = "backtrace-sys" 32 | version = "0.1.32" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" 35 | dependencies = [ 36 | "cc", 37 | "libc", 38 | ] 39 | 40 | [[package]] 41 | name = "bitflags" 42 | version = "1.2.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 45 | 46 | [[package]] 47 | name = "byteorder" 48 | version = "0.4.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "96c8b41881888cc08af32d47ac4edd52bc7fa27fef774be47a92443756451304" 51 | 52 | [[package]] 53 | name = "byteorder" 54 | version = "1.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" 57 | 58 | [[package]] 59 | name = "bzip2" 60 | version = "0.3.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" 63 | dependencies = [ 64 | "bzip2-sys", 65 | "libc", 66 | ] 67 | 68 | [[package]] 69 | name = "bzip2-sys" 70 | version = "0.1.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "6584aa36f5ad4c9247f5323b0a42f37802b37a836f0ad87084d7a33961abe25f" 73 | dependencies = [ 74 | "cc", 75 | "libc", 76 | ] 77 | 78 | [[package]] 79 | name = "cc" 80 | version = "1.0.50" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 83 | 84 | [[package]] 85 | name = "cfg-if" 86 | version = "0.1.10" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 89 | 90 | [[package]] 91 | name = "cfg-if" 92 | version = "1.0.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 95 | 96 | [[package]] 97 | name = "crc32fast" 98 | version = "1.2.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" 101 | dependencies = [ 102 | "cfg-if 0.1.10", 103 | ] 104 | 105 | [[package]] 106 | name = "docopt" 107 | version = "1.1.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969" 110 | dependencies = [ 111 | "lazy_static", 112 | "regex", 113 | "serde", 114 | "strsim", 115 | ] 116 | 117 | [[package]] 118 | name = "error-chain" 119 | version = "0.12.1" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" 122 | dependencies = [ 123 | "backtrace", 124 | "version_check", 125 | ] 126 | 127 | [[package]] 128 | name = "filetime" 129 | version = "0.2.14" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" 132 | dependencies = [ 133 | "cfg-if 1.0.0", 134 | "libc", 135 | "redox_syscall 0.2.5", 136 | "winapi", 137 | ] 138 | 139 | [[package]] 140 | name = "flate2" 141 | version = "0.2.20" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "e6234dd4468ae5d1e2dbb06fe2b058696fdc50a339c68a393aefbf00bc81e423" 144 | dependencies = [ 145 | "libc", 146 | "miniz-sys", 147 | ] 148 | 149 | [[package]] 150 | name = "flate2" 151 | version = "1.0.13" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f" 154 | dependencies = [ 155 | "cfg-if 0.1.10", 156 | "crc32fast", 157 | "libc", 158 | "miniz_oxide", 159 | ] 160 | 161 | [[package]] 162 | name = "glob" 163 | version = "0.3.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 166 | 167 | [[package]] 168 | name = "hermit-abi" 169 | version = "0.1.6" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" 172 | dependencies = [ 173 | "libc", 174 | ] 175 | 176 | [[package]] 177 | name = "itoa" 178 | version = "0.4.4" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" 181 | 182 | [[package]] 183 | name = "lazy_static" 184 | version = "1.4.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 187 | 188 | [[package]] 189 | name = "libc" 190 | version = "0.2.66" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 193 | 194 | [[package]] 195 | name = "lodepng" 196 | version = "2.5.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "40ed9859b15e009b494528f32ad054c5baf67afabf3c05d27973ea151563d430" 199 | dependencies = [ 200 | "libc", 201 | "rgb", 202 | ] 203 | 204 | [[package]] 205 | name = "memchr" 206 | version = "2.3.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" 209 | 210 | [[package]] 211 | name = "miniz-sys" 212 | version = "0.1.12" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "1e9e3ae51cea1576ceba0dde3d484d30e6e5b86dee0b2d412fe3a16a15c98202" 215 | dependencies = [ 216 | "cc", 217 | "libc", 218 | ] 219 | 220 | [[package]] 221 | name = "miniz_oxide" 222 | version = "0.3.5" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "6f3f74f726ae935c3f514300cc6773a0c9492abc5e972d42ba0c0ebb88757625" 225 | dependencies = [ 226 | "adler32", 227 | ] 228 | 229 | [[package]] 230 | name = "nbtrs" 231 | version = "0.1.1" 232 | source = "git+https://github.com/overviewer/nbtrs#91eb2d99982b15079d080bee8aa6fd349610d03e" 233 | dependencies = [ 234 | "byteorder 0.4.2", 235 | "flate2 0.2.20", 236 | ] 237 | 238 | [[package]] 239 | name = "num_cpus" 240 | version = "1.11.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72" 243 | dependencies = [ 244 | "hermit-abi", 245 | "libc", 246 | ] 247 | 248 | [[package]] 249 | name = "podio" 250 | version = "0.1.6" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd" 253 | 254 | [[package]] 255 | name = "proc-macro2" 256 | version = "1.0.7" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" 259 | dependencies = [ 260 | "unicode-xid", 261 | ] 262 | 263 | [[package]] 264 | name = "quote" 265 | version = "1.0.2" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 268 | dependencies = [ 269 | "proc-macro2", 270 | ] 271 | 272 | [[package]] 273 | name = "redox_syscall" 274 | version = "0.1.56" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 277 | 278 | [[package]] 279 | name = "redox_syscall" 280 | version = "0.2.5" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" 283 | dependencies = [ 284 | "bitflags", 285 | ] 286 | 287 | [[package]] 288 | name = "regex" 289 | version = "1.3.3" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" 292 | dependencies = [ 293 | "aho-corasick", 294 | "memchr", 295 | "regex-syntax", 296 | "thread_local", 297 | ] 298 | 299 | [[package]] 300 | name = "regex-syntax" 301 | version = "0.6.13" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" 304 | 305 | [[package]] 306 | name = "rgb" 307 | version = "0.8.16" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "5ec4ab2cf0b27e111e266e161cf7f9efd20125a161190da1c0945c4a4408fef3" 310 | 311 | [[package]] 312 | name = "rustc-demangle" 313 | version = "0.1.16" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" 316 | 317 | [[package]] 318 | name = "rustc-serialize" 319 | version = "0.3.24" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 322 | 323 | [[package]] 324 | name = "ryu" 325 | version = "1.0.2" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 328 | 329 | [[package]] 330 | name = "serde" 331 | version = "1.0.104" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" 334 | dependencies = [ 335 | "serde_derive", 336 | ] 337 | 338 | [[package]] 339 | name = "serde_derive" 340 | version = "1.0.104" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" 343 | dependencies = [ 344 | "proc-macro2", 345 | "quote", 346 | "syn", 347 | ] 348 | 349 | [[package]] 350 | name = "serde_json" 351 | version = "1.0.44" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7" 354 | dependencies = [ 355 | "itoa", 356 | "ryu", 357 | "serde", 358 | ] 359 | 360 | [[package]] 361 | name = "strsim" 362 | version = "0.9.3" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" 365 | 366 | [[package]] 367 | name = "syn" 368 | version = "1.0.13" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8" 371 | dependencies = [ 372 | "proc-macro2", 373 | "quote", 374 | "unicode-xid", 375 | ] 376 | 377 | [[package]] 378 | name = "thread_local" 379 | version = "1.0.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 382 | dependencies = [ 383 | "lazy_static", 384 | ] 385 | 386 | [[package]] 387 | name = "threadpool" 388 | version = "1.7.1" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865" 391 | dependencies = [ 392 | "num_cpus", 393 | ] 394 | 395 | [[package]] 396 | name = "time" 397 | version = "0.1.42" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" 400 | dependencies = [ 401 | "libc", 402 | "redox_syscall 0.1.56", 403 | "winapi", 404 | ] 405 | 406 | [[package]] 407 | name = "unicode-xid" 408 | version = "0.2.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 411 | 412 | [[package]] 413 | name = "version_check" 414 | version = "0.1.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 417 | 418 | [[package]] 419 | name = "voxelmap_cache" 420 | version = "0.2.0" 421 | dependencies = [ 422 | "byteorder 1.3.2", 423 | "docopt", 424 | "error-chain", 425 | "filetime", 426 | "glob", 427 | "lazy_static", 428 | "lodepng", 429 | "nbtrs", 430 | "rustc-serialize", 431 | "serde", 432 | "serde_json", 433 | "threadpool", 434 | "zip", 435 | ] 436 | 437 | [[package]] 438 | name = "winapi" 439 | version = "0.3.8" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 442 | dependencies = [ 443 | "winapi-i686-pc-windows-gnu", 444 | "winapi-x86_64-pc-windows-gnu", 445 | ] 446 | 447 | [[package]] 448 | name = "winapi-i686-pc-windows-gnu" 449 | version = "0.4.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 452 | 453 | [[package]] 454 | name = "winapi-x86_64-pc-windows-gnu" 455 | version = "0.4.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 458 | 459 | [[package]] 460 | name = "zip" 461 | version = "0.5.4" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "e41ff37ba788e2169b19fa70253b70cb53d9f2db9fb9aea9bcfc5047e02c3bae" 464 | dependencies = [ 465 | "bzip2", 466 | "crc32fast", 467 | "flate2 1.0.13", 468 | "podio", 469 | "time", 470 | ] 471 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "voxelmap_cache" 3 | version = "0.2.0" 4 | authors = ["Gjum "] 5 | 6 | [dependencies] 7 | byteorder = "~1.3" 8 | docopt = "~1.1" 9 | error-chain = "~0.12" 10 | filetime = "0.2.13" 11 | glob = "~0.3" 12 | lazy_static = "~1.4" 13 | lodepng = "~2.5" 14 | nbtrs = { git = "https://github.com/overviewer/nbtrs" } 15 | rustc-serialize = "0.3" 16 | serde = "~1.0" 17 | serde_json = "~1.0" 18 | threadpool = "~1.7" 19 | zip = "~0.5" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # voxelmap-cache 2 | 3 | Utilities for [VoxelMap](https://www.planetminecraft.com/mod/zans-minimap/) 4 | and [CivMap](https://github.com/Gjum/CivMap/)/[CCMap](https://ccmap.github.io/) 5 | to merge map caches and render image tiles 6 | 7 | ## Installation 8 | 9 | You will need [Rust](https://www.rust-lang.org/) Nightly, 10 | for example using [rustup](https://rustup.rs/), 11 | and [Python 3](https://www.python.org/downloads/) 12 | with the [Pillow](https://pypi.org/project/Pillow/) package. 13 | 14 | The following command line examples are written for Mac/Linux. 15 | If you are using Windows, you can install [Git Bash](https://gitforwindows.org/#bash) 16 | or use [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10). 17 | 18 | ## Updating the map with new data 19 | 20 | Here's an overview. Each step is detailed below. 21 | 22 | - get region caches from extracted archives using `extract_regions.py` 23 | - merge all caches using `merge_caches` 24 | - render terrain tiles using VoxelMap 25 | - clean up any player-loaded chunks from the cache and terrain tiles using `cleanup.py` 26 | - render other map tiles (simple/light/biome/height) using the custom CivMap renderer 27 | - create zoomed-out tiles using `zoom.py` in each tileset directory 28 | - optional: create a single image from all the tiles of one tileset 29 | 30 | ### Example Directory Structure 31 | 32 | ``` 33 | ~/civmap-cc/ 34 | |- contrib/ 35 | | |- player_2018-08-04.zip 36 | | |- player_2020-01-13.7z 37 | | ... 38 | |- extracted/ 39 | | |- -27,11,player_2020-01-13_overworld.zip 40 | | |- -27,11,player_2018-08-04_overworld.zip 41 | | ... 42 | |-merged/ 43 | | |- current -> 2020-01-13/ 44 | | |- 2020-01-13/ 45 | | | |- -27,11.zip 46 | | ... 47 | '- tiles/ 48 | |- terrain/ 49 | | |- z0/ 50 | | | |- -27,11.png 51 | | | ... 52 | | |- z-1/ 53 | | ... 54 | |- height/ 55 | ... 56 | ``` 57 | 58 | ### extract_regions.py 59 | 60 | Usage: 61 | 62 | python3 py/extract_regions.py [-v] [new_cache ...] 63 | 64 | Takes a source directory containing voxelmap region caches (`,.zip`) 65 | and creates hardlinks of them in the target directory, 66 | tagged with the source directory's name. 67 | 68 | Example: 69 | 70 | ```bash 71 | mkdir contrib/player_2018-08-04/ 72 | cd contrib/player_2018-08-04/ 73 | unzip ../player_2018-08-04.zip 74 | # ... do the same for all other contributions 75 | 76 | python3 py/extract_regions.py extracted/ contrib/player_2018-08-04/ contrib/player_2020-01-13/ # ... 77 | ``` 78 | 79 | ### Merge Caches 80 | 81 | Compile with: 82 | 83 | cargo build --release --bin merge_caches 84 | 85 | Usage: 86 | 87 | target/release/merge_caches [-q] [-t threads] [--between=] ... 88 | 89 | `cache-path` contains voxelmap caches in the format `,,.zip`. 90 | 91 | `output-path` should be an *empty* directory and will contain the merged cache. 92 | 93 | Options: 94 | 95 | -q, --quiet Do not output info messages. 96 | -t, --threads Number of threads to use for parallel processing 97 | --between= Only merge tiles at least partially within this bounding box, 98 | format: w,n,e,s [default: -99999,-99999,99999,99999] 99 | 100 | Example: 101 | 102 | ```bash 103 | # prepare empty directory 104 | mkdir -p merged/2020-01-13/ 105 | rm merged/current # delete old symlink 106 | ln -rs merged/2020-01-13/ merged/current 107 | 108 | # merge ALL extracted contributions 109 | # using `cargo run` compiles the program and then runs it, `--release` makes it run fast, `--bin merge_caches` selects this program only out of all the ones available 110 | cargo run --release --bin merge_caches merged/current/ extracted/ 111 | ``` 112 | 113 | ### Rendering tiles using VoxelMap 114 | 115 | - see also: [instructions at old VoxelMap-related project](https://github.com/MamiyaOtaru/anvilmapper/blob/0b1d5ff6bc4062c048645202f5b266f5f1288c2f/README.md#voxelmap-output-image-processor) 116 | - create a singleplayer world (`WORLDNAME`), 117 | move outside of the world border (13000 blocks for CivClassic 2.0) plus render distance, 118 | so you don't overwrite the cache when you render it later 119 | - copy/symlink the merged cache to `.minecraft/mods/VoxelMods/voxelMap/cache/WORLDNAME/overworld (dimension 0)/` 120 | - stop Mincraft, put `Output Images:true` in `.minecraft/mods/VoxelMods/voxelmap.properties`, start Minecraft 121 | (note that this line gets deleted when you start Minecraft, it only applies to the running game instance, 122 | so if you are repeating this step you will have to edit it again each time) 123 | - ensure you have your chosen resource pack loaded, and it's daytime 124 | (`/gamerule doDaylightCycle false`, `/time set 6000`); 125 | you may want to enable GammaBright mod to show under water terrain 126 | - load the singleplayer world, open VoxelMap, pan around until all of the map is rendered 127 | - images will be created in `.minecraft/mods/VoxelMods/voxelMap/cache/WORLDNAME/overworld (dimension 0)/images/z1/,.png` 128 | - copy them to `tiles/terrain/z0/` (CivMap uses different zoom numbers) 129 | - run `cleanup.py` on them to remove the area you were standing in during rendering 130 | 131 | ### cleanup.py 132 | 133 | Usage: 134 | 135 | python3 py/cleanup.py [-f] 136 | 137 | Removes region files (cache, tile, chunk times) that are 138 | outside the 13000 blocks world border of CivClassic 2.0. 139 | (Edit the source code to change this radius.) 140 | 141 | Without `-f`, only lists the files being removed. 142 | With `-f`, removes them. 143 | 144 | This can be used after in-game rendering (using VoxelMap) 145 | to remove the tiles and cache files around the player, if it is 146 | standing outside render distance of the world border. 147 | 148 | Example: 149 | 150 | ```bash 151 | # remove extraneous tiles before zooming out 152 | # 1. list files to be deleted ("dry run") 153 | python3 py/cleanup.py tiles/terrain/z0/ 154 | # 2. add -f flag after you've confirmed it won't eat your precious files 155 | python3 py/cleanup.py tiles/terrain/z0/ -f 156 | # also clean the cache for custom renderer 157 | python3 py/cleanup.py merged/current/ 158 | ``` 159 | 160 | ### Custom Renderer 161 | 162 | Turns cache tiles (`,.zip`) into tile images (`,.png`). 163 | 164 | Available modes: 165 | 166 | - simple: blue-gray water-land map 167 | - light: grayscale block light values, used to generate the nightmap 168 | - biome: color coded biomes, using [AMIDST color map][amidst-biomecolors] 169 | - height: color coded block heights and water depths 170 | 171 | Compile with: 172 | 173 | cargo build --release --bin render 174 | 175 | Usage: 176 | 177 | target/release/render [-q] [-t threads] (simple | light | biome | height) 178 | 179 | `cache-path` contains voxelmap caches in the format `,.zip`, 180 | for example the result of `merge_caches`. 181 | 182 | `output-path` is a directory that will contain the rendered tiles. 183 | 184 | Options: 185 | 186 | -q, --quiet Do not output info messages. 187 | -t, --threads Number of threads to use for parallel processing 188 | --between= Only render tiles at least partially within this bounding box, 189 | format: w,n,e,s [default: -99999,-99999,99999,99999] 190 | 191 | Example: 192 | 193 | ```bash 194 | cargo run --release --bin render merged/current tiles/height/z0 height 195 | ``` 196 | 197 | ### build_night.py 198 | 199 | python3 py/build_night.py tiles/night/z0 /tiles/terrain/z0 /tiles/light/z0 200 | 201 | or, when using bash: 202 | 203 | python3 py/build_night.py tiles/{night,terrain,light}/z0 204 | 205 | Combine `terrain` and `light` into `night` tiles. 206 | 207 | ### zoom.py 208 | 209 | python3 zoom.py [minimum zoom level = -1] 210 | 211 | Zoom out a tileset, combining 4 tiles into one, and shrinking it to the original tile size. 212 | The tileset root must contain a directory named `z0`. 213 | If given, the minimum zoom level must be negative (n < 0), default is -1. 214 | This will create new directories `z-1`, `z-2`, ... next to `z0`, containing the zoomed-out tiles. 215 | 216 | Example: 217 | 218 | ```bash 219 | python3 py/zoom.py tiles/terrain/ -6 220 | python3 py/zoom.py tiles/height/ -6 221 | ``` 222 | 223 | ### image_from_tiles.py 224 | 225 | python3 image_from_tiles.py 226 | 227 | Combines all tiles in `` into a single image. 228 | 229 | Example: 230 | 231 | ```bash 232 | # using z-3 results in 8x zoom (2^3 = 8) 233 | python3 py/image_from_tiles.py terrain_8x_zoomed.png tiles/terrain/z-3 234 | # you can also create full-scale images (1:1 pixel:block), 235 | # but they may crash your system when opened in a typical image viewer. 236 | python3 py/image_from_tiles.py simple_full_scale.png tiles/simple/z0 237 | ``` 238 | 239 | ## Miscellaneous Utilities 240 | 241 | ### convert_biomes_amidst.py 242 | 243 | python py/convert_biomes_amidst.py --rs --download > src/biomes.rs 244 | python py/convert_biomes_amidst.py --rs < biomes_amidst.csv > src/biomes.rs 245 | 246 | Converts the color info into source code, sourcing it 247 | from the [crbednarz/AMIDST Github repo][amidst-biomecolors] (`--download` flag) 248 | or from a csv file, piped into stdin (format: `id,name,red,green,blue,type`). 249 | 250 | [amidst-biomecolors]: https://github.com/crbednarz/AMIDST/wiki/biomecolors 251 | 252 | If `--rs` is supplied, prints header and footer of the color array, 253 | so the output is a valid Rust file as required for `src/biomes.rs`. 254 | 255 | ### rezip_cache.py 256 | 257 | Sometimes Rust's zip reader can't open some region cache .zip files. 258 | This script re-zips every region in a way that Rust can read them, 259 | keeping their timestamps intact. 260 | -------------------------------------------------------------------------------- /py/build_night.py: -------------------------------------------------------------------------------- 1 | """ 2 | Combine terrain and light into night tiles. 3 | Example: python3 build_night.py /tiles/night/z0 /tiles/terrain/z0 /tiles/light/z0 4 | or, when using bash: python3 build_night.py /tiles/{night,terrain,light}/z0 5 | """ 6 | import os 7 | import sys 8 | import time 9 | from PIL import Image 10 | 11 | darkness = .2 # how dark the night areas are, 0 (black) to 1 (day) 12 | 13 | def make_night(day_path, light_path): 14 | day = Image.open(day_path).convert("RGBA") # make sure there's a black value 15 | light = Image.open(light_path) 16 | 17 | black = Image.new(day.mode, day.size, 'black') 18 | night = Image.blend(black, day, darkness) 19 | 20 | night.paste(day, (0, 0), light.convert('L')) 21 | 22 | return night 23 | 24 | def make_night_all(night_dir, day_dir, light_dir): 25 | os.makedirs(night_dir, exist_ok=True) 26 | 27 | tiles = [tuple(map(int, tile[:-4].split(','))) 28 | for tile in os.listdir(day_dir) 29 | if tile[-4:] == '.png'] 30 | 31 | print('converting', len(tiles), 'tiles to night ...') 32 | 33 | first_progress = last_progress = time.time() 34 | for tn, pos in enumerate(tiles): 35 | if last_progress + 3 < time.time(): 36 | last_progress += 3 37 | time_left = (time.time() - first_progress) / tn * (len(tiles) - tn) 38 | print('night: %i/%i tiles' % (tn, len(tiles)), 39 | '%i:%02i left' % (int(time_left / 60), int(time_left % 60))) 40 | 41 | night = make_night( 42 | day_dir + '/%s,%s.png' % pos, 43 | light_dir + '/%s,%s.png' % pos) 44 | 45 | night.save(night_dir + '/%s,%s.png' % pos, 'PNG') 46 | 47 | print('Done, night is at', night_dir) 48 | 49 | 50 | if __name__ == '__main__': 51 | try: 52 | night_dir, day_dir, light_dir = sys.argv[1:4] 53 | except ValueError: 54 | print('Args: ') 55 | else: 56 | make_night_all(night_dir, day_dir, light_dir) 57 | -------------------------------------------------------------------------------- /py/cleanup.py: -------------------------------------------------------------------------------- 1 | """ 2 | python3 cleanup.py [-f] 3 | 4 | Removes region files (cache, tile, chunk times) that are 5 | outside the 13000 blocks world border of CivClassic 2.0. 6 | 7 | Without -f, only lists the files being removed. 8 | With -f, removes them. 9 | """ 10 | import os, sys 11 | 12 | radius = 52 # 13312 blocks = 52 tiles * 256 blocks/tile 13 | 14 | to_remove = [] 15 | cleaned_dir = sys.argv[1] 16 | for f in os.listdir(cleaned_dir): 17 | if '.' not in f: continue 18 | region_pos, file_ending = f.rsplit('.', 1) 19 | if file_ending in ('zip', 'png'):#, 'gz'): 20 | x, z = map(int, region_pos.split(',', 1)) 21 | if not -radius <= x < radius or not -radius <= z < radius: 22 | to_remove.append(f) 23 | 24 | if len(sys.argv) > 2 and sys.argv[2] == '-f': 25 | print('removing:', len(to_remove)) 26 | for f in to_remove: 27 | os.remove(cleaned_dir + '/' + f) 28 | else: 29 | print(*to_remove, sep='\n') 30 | print('total:', len(to_remove)) 31 | -------------------------------------------------------------------------------- /py/convert_biomes_amidst.py: -------------------------------------------------------------------------------- 1 | """ 2 | python py/convert_biomes_amidst.py --rs --download > src/biomes.rs 3 | python py/convert_biomes_amidst.py --rs < biomes_amidst.csv > src/biomes.rs 4 | 5 | Gets the color info from the crbednarz/AMIDST Github repo (--download flag) 6 | or from a csv file, piped into stdin (format: id,name,red,green,blue,type). 7 | 8 | If --rs is supplied, prints header and footer of the color array, 9 | so the output is a valid Rust file as required for src/biomes.rs. 10 | """ 11 | import sys 12 | 13 | biome_colors = [0x88ff00ff for _ in range(256)] 14 | 15 | if '--download' in sys.argv[1:]: 16 | url = 'https://raw.githubusercontent.com/crbednarz/AMIDST/master/src/amidst/minecraft/Biome.java' 17 | print('// Colors taken from', url) 18 | 19 | import re 20 | from urllib import request 21 | 22 | # public static final Biome frozenRiverM = new Biome("Frozen River M", 139, Util.makeColor(160, 160, 255)); 23 | biome_re = re.compile(r'new Biome\("[^"]+",[ \t]*([0-9]+),[ \t]*Util.makeColor\(([0-9]+),[ \t]*([0-9]+),[ \t]*([0-9]+)\)') 24 | 25 | data = request.urlopen(url).read().decode() 26 | 27 | for match in biome_re.finditer(data): 28 | i_id, i_red, i_green, i_blue = map(int, match.groups()) 29 | color = 0xff000000 | i_blue << 16 | i_green << 8 | i_red 30 | biome_colors[i_id] = color 31 | 32 | else: # assume stdin is piped .csv 33 | # skip header: id,name,red,green,blue,type 34 | next(sys.stdin) 35 | 36 | for line in sys.stdin: 37 | s_id, s_name, s_red, s_green, s_blue, s_type = line.strip().split(',') 38 | i_id, i_red, i_green, i_blue = map(int, (s_id, s_red, s_green, s_blue)) 39 | color = 0xff000000 | i_blue << 16 | i_green << 8 | i_red 40 | biome_colors[i_id] = color 41 | 42 | if '--rs' in sys.argv[1:]: 43 | print('// Created using py/convert_biomes_amidst.py', *sys.argv[1:]) 44 | print('pub const BIOME_COLOR_TABLE: [u32; 256] = [') 45 | 46 | for color in biome_colors: 47 | print(hex(color)+',') 48 | 49 | if '--rs' in sys.argv[1:]: 50 | print('];') 51 | -------------------------------------------------------------------------------- /py/convert_block_naturality.py: -------------------------------------------------------------------------------- 1 | # Converts data/blockcounts-categories.tsv into 2 | # CCNATURAL_COLORS_BLOCK_BIOME and CCNATURAL_COLORS_BLOCK_DEFAULT 3 | 4 | import sys 5 | from collections import defaultdict 6 | 7 | # read tsv 8 | 9 | blocks = defaultdict(lambda: defaultdict(list)) # block -> category -> biomes 10 | 11 | sys.stdin.readline() # skip header 12 | 13 | for line in sys.stdin: 14 | line = line.strip() 15 | if not line: 16 | continue 17 | biomeid, biome, block, category, *_ = line.split('\t') 18 | if category == 'x': 19 | category = 'unknown' 20 | biomeid = int(biomeid) 21 | 22 | blocks[block][category].append(biomeid) 23 | 24 | # calculate primary category 25 | 26 | # block -> category (with most biomes) 27 | primcats = { 28 | block: max(cats.items(), key=lambda cb: len(cb[1]))[0] 29 | for block, cats in blocks.items() 30 | } 31 | 32 | # print primary category 33 | 34 | print(*sorted( 35 | f'("{bl}", Naturality::{cat.capitalize()}),' 36 | for (bl, cat) in primcats.items() 37 | ), sep='\n') 38 | 39 | print("""\ 40 | ].iter().map(|(n,c)| (*n, *c))); 41 | 42 | pub static ref CCNATURAL_COLORS_BLOCK_BIOME: HashMap<(&'static str, u8), Naturality> = HashMap::from_iter([\ 43 | """) 44 | 45 | # print non-primary block+biome to category mappings 46 | 47 | print(*sorted( 48 | f'(("{bl}", {bi}), Naturality::{cat.capitalize()}),' 49 | for (bl, cats) in blocks.items() 50 | for (cat, bis) in cats.items() 51 | for bi in bis 52 | if primcats[bl] != cat 53 | ), sep='\n') 54 | 55 | print("""\ 56 | ].iter().map(|((n,b),c)| ((*n, *b), *c))); 57 | }\ 58 | """) 59 | -------------------------------------------------------------------------------- /py/cut_tiles.py: -------------------------------------------------------------------------------- 1 | ##### settings 2 | 3 | ts = 256 # tile size (width and height) 4 | outfmt = '{x},{z}.png' # tiles output paths template 5 | x_off = -20 # x coord of left tile 6 | z_off = -20+5 # z coord of top tile 7 | 8 | ##### 9 | 10 | import math, sys 11 | from PIL import Image 12 | 13 | Image.MAX_IMAGE_PIXELS = None 14 | 15 | img = Image.open(sys.argv[1]) 16 | img_w, img_h = img.size 17 | 18 | for z in range(math.ceil(img_h / ts)): 19 | for x in range(math.ceil(img_w / ts)): 20 | tile = img.crop((x*ts, z*ts, (x+1)*ts, (z+1)*ts)) 21 | tile.save(outfmt.format(x=x+x_off, z=z+z_off)) 22 | -------------------------------------------------------------------------------- /py/extract_regions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Takes a source directory containing voxelmap region caches (,.zip) 3 | and creates hardlinks of them in the target directory, 4 | tagged with the source directory's name. 5 | 6 | Args: [-v]
[new cache ...] 7 | 8 | Input examples: 9 | 10 | contribs/ 11 | |- foo_2017-01-01/ 12 | | |- play.devotedmc.com/ 13 | | |- world/ 14 | | | |- Overworld (dimension 0)/ 15 | | | |- ,.zip <-- this is what we want! 16 | | |- Overworld (dimension 0)/ 17 | | | |- ,.zip <-- looks very similar, but is not what we want! 18 | | |- nether/ 19 | | |- Overworld (dimension 0)/ 20 | | |- ,.zip <-- also not what we want! 21 | |- bar_2017-01-01/ 22 | | |- world/ 23 | | |- Overworld (dimension 0)/ 24 | | |- ,.zip 25 | |- baz_2017-01-01_custom/ 26 | | |- Overworld (dimension 0)/ 27 | | |- ,.zip 28 | 29 | Command example: 30 | 31 | python3 extract_regions.py extracted/ contribs/foo_2017-01-01/ contribs/bar_2017-01-01/ contribs/baz_2017-01-01_custom/ 32 | 33 | Output format: 34 | 35 | extracted/ 36 | |- ,,foo_2017-01-01.zip 37 | |- ,,bar_2017-01-01.zip 38 | |- ,,baz_2017-01-01_custom.zip 39 | 40 | """ 41 | import os 42 | import re 43 | import sys 44 | 45 | 46 | region_regexp = re.compile('[-0-9]+,[-0-9]+.zip') 47 | 48 | 49 | def hardlink_cache(main_cache, contrib, src_path, verbose): 50 | coords = os.path.basename(src_path)[:-4] 51 | dest_path = '%s/%s,%s.zip' % (main_cache, coords, contrib) 52 | try: 53 | os.link(src_path, dest_path) 54 | mtime = os.path.getmtime(src_path) 55 | os.utime(dest_path, (mtime, mtime)) 56 | except FileExistsError: 57 | if verbose: print('! Skipping existing', src_path, dest_path) 58 | except Exception as e: 59 | print('# Error hardlinking', src_path, 'to', dest_path, e.__class__.__name__, e) 60 | 61 | 62 | def hardlink_dir(main_cache, contrib, contrib_dir, verbose=False, dry=False): 63 | regions = [r for r in os.listdir(contrib_dir) if r[-4:] == '.zip'] 64 | print('> Found', len(regions), 'regions in', contrib_dir) 65 | if dry: 66 | return 67 | for region in regions: 68 | hardlink_cache(main_cache, contrib, contrib_dir + '/' + region, verbose) 69 | 70 | 71 | def hardlink_contrib(main_cache, contrib, contrib_path, verbose=False, dry=False): 72 | os.makedirs(main_cache, exist_ok=True) 73 | if verbose: print('Opening', contrib_path) 74 | 75 | entries = [entry.name for entry in os.scandir(contrib_path)] 76 | if 'world' in entries: 77 | if verbose: print('? has world dir, skipping Overworld dir if present') 78 | hardlink_dir(main_cache, contrib, contrib_path + '/world/Overworld (dimension 0)', verbose, dry) 79 | elif 'Overworld (dimension 0)' in entries: 80 | if verbose: print('? has only Overworld dir') 81 | hardlink_dir(main_cache, contrib, contrib_path + '/Overworld (dimension 0)', verbose, dry) 82 | elif 'mc.civclassic.com' in entries: 83 | if verbose: print('? has hostname dir') 84 | if 'world' in [entry.name for entry in os.scandir(contrib_path + '/mc.civclassic.com')]: 85 | if verbose: print('? has hostname/world dir') 86 | hardlink_dir(main_cache, contrib+'_world', contrib_path + '/mc.civclassic.com/world/Overworld (dimension 0)', verbose, dry) 87 | hardlink_dir(main_cache, contrib, contrib_path + '/mc.civclassic.com/Overworld (dimension 0)', verbose, dry) 88 | elif 'play.devotedmc.com' in entries: 89 | if verbose: print('? has hostname dir') 90 | hardlink_dir(main_cache, contrib, contrib_path + '/play.devotedmc.com/world/Overworld (dimension 0)', verbose, dry) 91 | elif any(region_regexp.match(entry) for entry in entries): 92 | if verbose: print('? regions were placed directly in contribution') 93 | hardlink_dir(main_cache, contrib, contrib_path, verbose, dry) 94 | else: 95 | if verbose: print('! Skipping: nothing found in', contrib_path) 96 | 97 | if verbose: print('Closing', contrib_path, '\n') 98 | 99 | 100 | def hardlink_all_contribs(main_cache, *contribs, verbose=False): 101 | if verbose: print('Hardlinking', len(contribs), 'contributions') 102 | for contrib_path in contribs: 103 | contrib = os.path.basename(contrib_path) 104 | if not contrib: 105 | contrib = os.path.basename(contrib_path[:-1]) 106 | try: 107 | hardlink_contrib(main_cache, contrib, contrib_path, verbose) 108 | except NotADirectoryError: 109 | if verbose: print('! Skipping non-directory contrib', contrib_path) 110 | except Exception as e: 111 | print('# Error processing', contrib_path, e.__class__.__name__, e) 112 | 113 | 114 | def main(args): 115 | verbose = False 116 | if '-v' in args: 117 | args.remove('-v') 118 | verbose = True 119 | 120 | try: 121 | main_cache, *contribs = args 122 | except ValueError: 123 | print('Args: [-v]
[new cache ...]') 124 | return 1 125 | 126 | hardlink_all_contribs(main_cache, *contribs, verbose=verbose) 127 | 128 | 129 | if __name__ == '__main__': 130 | sys.exit(main(sys.argv[1:])) 131 | 132 | -------------------------------------------------------------------------------- /py/image_from_tiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | python3 image_from_tiles.py 3 | 4 | Combines all tiles in into a single image. 5 | """ 6 | import os 7 | import sys 8 | import time 9 | from PIL import Image 10 | 11 | def stitch_all(img_path, tiles_dir): 12 | tiles = [tuple(map(int, tile[:-4].split(','))) 13 | for tile in os.listdir(tiles_dir) 14 | if tile[-4:] == '.png'] 15 | 16 | tile_size = Image.open(tiles_dir + '/%i,%i.png' % tiles[0]).size[0] 17 | 18 | min_x = min(x for x,z in tiles) 19 | min_z = min(z for x,z in tiles) 20 | max_x = max(x for x,z in tiles) 21 | max_z = max(z for x,z in tiles) 22 | width = max_x - min_x + 1 23 | height = max_z - min_z + 1 24 | 25 | out = Image.new('RGBA', (width*tile_size, height*tile_size)) 26 | 27 | last_progress = time.time() 28 | for tn, tile in enumerate(tiles): 29 | if last_progress + 3 < time.time(): 30 | last_progress += 3 31 | print('%i/%i tiles' % (tn, len(tiles))) 32 | 33 | x, z = tile 34 | 35 | try: 36 | tile_img = Image.open(tiles_dir + '/%i,%i.png' % tile) 37 | out.paste(im=tile_img, 38 | box=((x - min_x) * tile_size, 39 | (z - min_z) * tile_size))#, 40 | # mask=tile_img if tile_img.hasAlphaChannel() else None) 41 | except: 42 | print('failed at', tile, tiles_dir + '/%i,%i.png' % tile) 43 | # continue 44 | raise 45 | 46 | print('saving image as', img_path) 47 | 48 | # out.thumbnail((256, 256))#, Image.NEAREST) 49 | out.save(img_path, 'PNG') 50 | 51 | if __name__ == '__main__': 52 | try: 53 | img_path, tiles_dir = sys.argv[1:3] 54 | except ValueError: 55 | print('Args: ') 56 | else: 57 | stitch_all(img_path, tiles_dir) 58 | -------------------------------------------------------------------------------- /py/jm-to-vm-tiles.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import sys 4 | from PIL import Image 5 | 6 | try: 7 | jm_dir = sys.argv[1] 8 | vm_dir = sys.argv[2] 9 | except IndexError: 10 | print('Args: ') 11 | sys.exit(1) 12 | 13 | os.makedirs(vm_dir, exist_ok=True) 14 | 15 | for jm_path in os.listdir(jm_dir): 16 | if jm_path[-4:] != '.png': 17 | continue 18 | jm_img = Image.open(jm_dir+'/'+jm_path) 19 | jm_x, jm_z = map(int, jm_path[:-4].split(',')) 20 | vm_x0, vm_z0 = jm_x * 2, jm_z * 2 21 | for dx, dz in ((0, 0), (1, 0), (0, 1), (1, 1)): 22 | vm_img = jm_img.crop((dx*256, dz*256, (dx+1)*256, (dz+1)*256)) 23 | vm_img.save(vm_dir + '/%i,%i.png' % (vm_x0+dx, vm_z0+dz)) 24 | -------------------------------------------------------------------------------- /py/merge_all.py: -------------------------------------------------------------------------------- 1 | """ 2 | NOTE: This file only serves as an example now. 3 | Do not use it, it is outdated and does not work correctly. 4 | Instead, use the `merge_all` Rust program. 5 | 6 | python3 merge_all.py 7 | Merges all tagged tile cache files from 8 | into a single cache in which then can be rendered using VoxelMap. 9 | 10 | The files in are in the format used by extract_tiles.py. 11 | """ 12 | import os 13 | import sys 14 | import time 15 | from collections import defaultdict 16 | from gzip import compress 17 | from zipfile import BadZipFile, ZipFile, ZIP_DEFLATED 18 | 19 | def serialize_key(key): 20 | s = b'' 21 | # the trailing \r\n might be (un)necessary, anyway it's in voxelmap's key files 22 | for v, k in key.items(): 23 | s += b'%i %s\r\n' % (k, v) 24 | return s 25 | 26 | def merge_all(out_dir, in_dir): 27 | os.makedirs(out_dir, exist_ok=True) 28 | 29 | named_tile_paths = defaultdict(list) 30 | num_total_tiles = 0 31 | for filename in os.listdir(in_dir): 32 | if filename[-4:] == '.zip': 33 | pos_split = filename[:-4].split(',', 2)[:2] 34 | rx, rz = map(int, pos_split) 35 | pos = ','.join(pos_split) 36 | tile_path = in_dir + '/' + filename 37 | mtime = os.path.getmtime(tile_path) 38 | named_tile_paths[pos].append((mtime, tile_path)) 39 | num_total_tiles += 1 40 | 41 | last_progress = first_progress = time.time() 42 | tiles_merged = -1 43 | skipped_tags = set() # XXX 44 | 45 | for tile_pos, tile_contribs in named_tile_paths.items(): 46 | unset_chunks = set(range(256)) 47 | chunk_mtimes = [-1 for _ in range(256)] # TODO read from file generated during previous merge 48 | 49 | out_data = bytearray(256*256*17) 50 | mv_out = memoryview(out_data) 51 | out_key = {} # string -> index (inverse of key file) 52 | next_key = 1 # voxelmap keys start at 1 53 | 54 | for mtime, tile_path in reversed(sorted(tile_contribs)): 55 | tiles_merged += 1 56 | if not unset_chunks: 57 | continue 58 | 59 | if last_progress + 3 < time.time(): 60 | last_progress += 3 61 | time_left = (time.time() - first_progress) / tiles_merged * (num_total_tiles - tiles_merged) 62 | print(' merge tagged: %i/%i tiles' % (tiles_merged, num_total_tiles), 63 | '%i:%02i left' % (int(time_left / 60), int(time_left % 60))) 64 | 65 | try: 66 | zip_file = ZipFile(tile_path) 67 | except BadZipFile: 68 | print('# bad zip file', tile_path) 69 | continue 70 | 71 | in_data = zip_file.open('data').read() 72 | mv_in = memoryview(in_data) 73 | 74 | if len(zip_file.filelist) > 1: 75 | key_file = zip_file.open('key') 76 | in_key = { int(k): v for k, v in (l.split() for l in key_file.read().split(b'\r\n') if l) } 77 | if not in_key: 78 | print('# skipping empty key', tile_path) 79 | continue 80 | else: 81 | # TODO convert keyless to keyed 82 | # in_key = ... 83 | 84 | tag = tile_path.split(',')[2][:-4] 85 | if tag not in skipped_tags: 86 | skipped_tags.add(tag) 87 | print('# skipping old unkeyed format', tag) 88 | 89 | continue 90 | 91 | # merge in_keys into out_keys, produce in->out key mapping 92 | current_key_map = [0] * (1 + max(k for k in in_key.keys())) 93 | for in_id, str_id in in_key: 94 | out_id = out_key.get(str_id) 95 | if out_id is None: 96 | out_id = out_key[str_id] = next_key 97 | next_key += 1 98 | current_key_map[in_id] = out_id 99 | 100 | for chunk_i in unset_chunks.copy(): 101 | chunk_x = chunk_i % 16 102 | chunk_z = chunk_i // 16 103 | chunk_off = 16 * (chunk_x + 256 * chunk_z) 104 | 105 | height_first_block = mv_in[chunk_off * 17] 106 | chunk_first_block = mv_in[chunk_off * 17 + 1] << 8 | mv_in[chunk_off * 17 + 2] 107 | if height_first_block == 0 and 'minecraft:air' == in_key[chunk_first_block]: 108 | continue # empty chunk 109 | 110 | # copy chunk, block by block 111 | for z in range(16): 112 | for x in range(16): 113 | start = 17 * (x + 256 * z + chunk_off) 114 | # copy everything, then change just the 4 blocks 115 | mv_out[start:start + 17] = mv_in[start:start + 17] 116 | for i in range(1, 1+16, 4): 117 | block = mv_in[i] << 8 | mv_in[i + 1] 118 | new_block = current_key_map[block] 119 | mv_out[i] = block >> 8 120 | mv_out[i+1] = block & 0xff 121 | 122 | unset_chunks.remove(chunk_i) 123 | chunk_mtimes[chunk_i] = mtime 124 | 125 | if out_key == {}: 126 | continue 127 | 128 | zf = ZipFile(out_dir + '/%s.zip' % tile_pos, 'w', compression=ZIP_DEFLATED) 129 | zf.writestr('data', out_data) 130 | zf.writestr('key', serialize_key(out_key)) 131 | zf.close() 132 | zip_file.close() 133 | 134 | #with open(out_dir + '/%s_chunk-times.gz' % tile_pos, 'wb') as f: 135 | # f.write(compress(','.join(map(str, chunk_mtimes)).encode())) 136 | 137 | 138 | if __name__ == '__main__': 139 | try: 140 | out_dir, in_dir = sys.argv[1:] 141 | except ValueError: 142 | print('Args: ') 143 | else: 144 | merge_all(out_dir, in_dir) 145 | -------------------------------------------------------------------------------- /py/merge_tile_images.py: -------------------------------------------------------------------------------- 1 | """ 2 | Combine two or more sets of terrain tiles, treating black pixels as transparent. 3 | Example: python3 merge_tile_images.py /tiles/out/ /tiles/bottom/ /tiles/middle/ /tiles/top/ 4 | """ 5 | import os 6 | import sys 7 | import time 8 | from PIL import Image 9 | import numpy as np 10 | 11 | 12 | # uses img.alpha_composite() 13 | def merge_one_pil(pos, out_dir, in_dirs): 14 | tile_filename = '/%s,%s.png' % pos 15 | out_img = Image.new("RGBA", (256,256), (0,0,0,0)) 16 | 17 | for in_dir in in_dirs: 18 | try: 19 | in_img = Image.open(in_dir + tile_filename).convert("RGBA") 20 | except FileNotFoundError: 21 | continue 22 | 23 | img_arr = np.array(in_img) 24 | 25 | black_areas = img_arr[:,:,0] | img_arr[:,:,1] | img_arr[:,:,2] == 0 26 | img_arr[black_areas] = [0,0,0, 0] 27 | transparent_img = Image.fromarray(img_arr) 28 | out_img.alpha_composite(transparent_img) 29 | 30 | out_img.save(out_dir + tile_filename, 'PNG') 31 | 32 | 33 | # uses numpy exclusively 34 | def merge_one_np(pos, out_dir, in_dirs): 35 | tile_filename = '/%s,%s.png' % pos 36 | out_arr = np.zeros((256,256,4), dtype='uint8') 37 | 38 | for in_dir in in_dirs: 39 | try: 40 | in_img = Image.open(in_dir + tile_filename).convert("RGBA") 41 | except FileNotFoundError: 42 | continue 43 | 44 | img_arr = np.array(in_img) 45 | 46 | present_areas = img_arr[:,:,0] | img_arr[:,:,1] | img_arr[:,:,2] != 0 47 | out_arr[present_areas] = img_arr[present_areas] 48 | 49 | out_img = Image.fromarray(out_arr) 50 | out_img.save(out_dir + tile_filename, 'PNG') 51 | 52 | 53 | def merge_all(out_dir, in_dirs): 54 | os.makedirs(out_dir, exist_ok=True) 55 | 56 | tiles = set( 57 | tuple(map(int, filename[:-4].split(','))) 58 | for in_dir in in_dirs 59 | for filename in os.listdir(in_dir) 60 | if filename[-4:] == '.png' and ',' in filename) 61 | 62 | print('total', len(tiles), 'tile locations') 63 | 64 | first_progress = last_progress = time.time() 65 | for tn, pos in enumerate(tiles): 66 | if last_progress + 3 < time.time(): 67 | last_progress = time.time() 68 | time_left = (time.time() - first_progress) / tn * (len(tiles) - tn) 69 | print('merge tile images: %i/%i tiles' % (tn, len(tiles)), 70 | '%i:%02i left' % (int(time_left / 60), int(time_left % 60))) 71 | 72 | # merge_one_pil(pos, out_dir, in_dirs) 73 | merge_one_np(pos, out_dir, in_dirs) 74 | 75 | print('Done, merged tile images are at ', out_dir) 76 | 77 | 78 | if __name__ == '__main__': 79 | try: 80 | out_dir, *in_dirs = sys.argv[1:] 81 | if len(in_dirs) < 2: raise ValueError() 82 | except ValueError: 83 | print('Args: [...] ') 84 | else: 85 | merge_all(out_dir, in_dirs) 86 | -------------------------------------------------------------------------------- /py/rezip_cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sometimes Rust's zip reader can't open some region cache .zip files. 3 | This script re-zips every region in a way that Rust can read them, 4 | keeping their timestamps intact. 5 | """ 6 | import os 7 | import sys 8 | import time 9 | from shutil import copystat, move 10 | import zipfile as zf 11 | from zipfile import ZipFile 12 | 13 | compressions = dict(zip( 14 | ('bzip2' , 'deflate' , 'lzma' , 'store' ), 15 | (zf.ZIP_BZIP2, zf.ZIP_DEFLATED, zf.ZIP_LZMA, zf.ZIP_STORED) 16 | )) 17 | 18 | def main(): 19 | try: 20 | compression_name, target, source = sys.argv[1:4] 21 | except ValueError: 22 | print('Args: ') 23 | print('Available compression methods: ' + ' '.join(compressions.keys())) 24 | else: 25 | compression = compressions[compression_name] 26 | if source[-4:] == '.zip': 27 | rezip_one(target, source, compression) 28 | else: rezip_all(target, source, compression) 29 | 30 | 31 | def rezip_all(target_dir, source_dir, compression=zf.ZIP_STORED): 32 | os.makedirs(target_dir, exist_ok=True) 33 | 34 | regions = [region for region in os.listdir(source_dir) if region[-4:] == '.zip'] 35 | 36 | print('re-zipping', len(regions), 'regions ...') 37 | 38 | first_progress = last_progress = time.time() 39 | for rn, region in enumerate(regions): 40 | if last_progress + 3 < time.time(): 41 | last_progress += 3 42 | time_left = (time.time() - first_progress) / rn * (len(regions) - rn) 43 | print('re-zip: %i/%i regions' % (rn, len(regions)), 44 | '%i:%02i left' % (int(time_left / 60), int(time_left % 60))) 45 | 46 | source_zip = source_dir + '/' + region 47 | target_zip = target_dir + '/' + region 48 | 49 | rezip_one(target_zip, source_zip, compression) 50 | 51 | print('Done, new cache is at', target_dir) 52 | 53 | 54 | def rezip_one(target_zip, source_zip, compression): 55 | try: 56 | with ZipFile(source_zip).open('data') as f: 57 | data = f.read() 58 | except Exception as e: 59 | print('skipping zip file', source_zip, 'for error', e) 60 | return 61 | 62 | # write to intermediate file in case source_dir == target_dir 63 | zf = ZipFile(target_zip+'.new', 'w', compression=compression) 64 | zf.writestr('data', data) 65 | zf.close() 66 | 67 | copystat(source_zip, target_zip+'.new') 68 | move(target_zip+'.new', target_zip) 69 | 70 | 71 | if __name__ == '__main__': 72 | main() 73 | -------------------------------------------------------------------------------- /py/zoom.py: -------------------------------------------------------------------------------- 1 | """ 2 | Zoom out a tileset, combining 4 tiles into one, and shrinking it to the original tile size. 3 | The tileset root must contain a directory named `z0`. 4 | If given, the minimum zoom level must be negative (n < 0), default is -1. 5 | This will create new directories z-1, z-2, ... next to z0, containing the zoomed-out tiles. 6 | """ 7 | import os 8 | import sys 9 | from PIL import Image 10 | 11 | 12 | def main(): 13 | try: 14 | tiles_root = sys.argv[1] 15 | except IndexError: 16 | print('Args: [minimum zoom level = -1]') 17 | sys.exit(1) 18 | 19 | try: 20 | min_zoom = int(sys.argv[2]) 21 | except ValueError: 22 | print('Args: [minimum zoom level = -1]') 23 | sys.exit(1) 24 | except IndexError: 25 | min_zoom = -1 26 | 27 | tiles_root += '/z%i' 28 | 29 | for current_zoom in range(-min_zoom): 30 | print('zooming', -current_zoom - 1,) 31 | stitch_all(tiles_root % (-current_zoom - 1), tiles_root % -current_zoom) 32 | 33 | 34 | def stitch_four(size, x, z, out_path, in_path): 35 | """ 36 | x,z are tile coords of the nw small tile 37 | size is the width of a small tile 38 | """ 39 | nw_path = in_path + '/%i,%i.png' % (x, z) 40 | sw_path = in_path + '/%i,%i.png' % (x, z+1) 41 | ne_path = in_path + '/%i,%i.png' % (x+1, z) 42 | se_path = in_path + '/%i,%i.png' % (x+1, z+1) 43 | 44 | out = Image.new('RGBA', (2*size, 2*size)) 45 | 46 | try: 47 | if os.path.isfile(nw_path): 48 | out.paste(im=Image.open(nw_path), box=(0, 0)) 49 | except Exception as e: 50 | print('Exception at', nw_path, e) 51 | try: 52 | if os.path.isfile(sw_path): 53 | out.paste(im=Image.open(sw_path), box=(0, size)) 54 | except Exception as e: 55 | print('Exception at', sw_path, e) 56 | try: 57 | if os.path.isfile(ne_path): 58 | out.paste(im=Image.open(ne_path), box=(size, 0)) 59 | except Exception as e: 60 | print('Exception at', ne_path, e) 61 | try: 62 | if os.path.isfile(se_path): 63 | out.paste(im=Image.open(se_path), box=(size, size)) 64 | except Exception as e: 65 | print('Exception at', se_path, e) 66 | 67 | out.thumbnail((size, size)) 68 | #out.thumbnail((256, 256), Image.NEAREST) 69 | out.save(out_path, 'PNG') 70 | 71 | def stitch_all(out_path, in_path): 72 | os.makedirs(out_path, exist_ok=True) 73 | 74 | tiles = [tuple(map(int, region[:-4].split(','))) 75 | for region in os.listdir(in_path) 76 | if region[-4:] == '.png'] 77 | 78 | size = Image.open(in_path + '/%i,%i.png' % tiles[0]).size[0] 79 | 80 | min_x = min(x for x,y in tiles) // 2 81 | min_z = min(z for x,z in tiles) // 2 82 | max_x = max(x for x,y in tiles) // 2 83 | max_z = max(z for x,z in tiles) // 2 84 | 85 | for x in range(min_x, max_x+1): 86 | for z in range(min_z, max_z+1): 87 | out_tile = out_path + '/%i,%i.png' % (x, z) 88 | try: 89 | stitch_four(size, 2*x, 2*z, out_tile, in_path) 90 | except Exception as e: 91 | print('Exception at', x, z, e.__class__.__name__) 92 | 93 | if __name__ == '__main__': 94 | main() 95 | -------------------------------------------------------------------------------- /src/bin/blockcount.rs: -------------------------------------------------------------------------------- 1 | extern crate docopt; 2 | extern crate serde; 3 | extern crate threadpool; 4 | extern crate voxelmap_cache; 5 | extern crate zip; 6 | 7 | use docopt::Docopt; 8 | use serde::Deserialize; 9 | use std::collections::HashMap; 10 | use std::path::PathBuf; 11 | use std::sync::mpsc::channel; 12 | use threadpool::ThreadPool; 13 | use voxelmap_cache::tile::{get_tile_paths_in_dirs, read_tile, Tile}; 14 | use voxelmap_cache::ProgressTracker; 15 | use voxelmap_cache::{biomes::BIOME_NAMES, tile::TILE_COLUMNS}; 16 | 17 | const USAGE: &'static str = " 18 | Usage: blockcount [-q] [-t threads] 19 | 20 | cache-path contains voxelmap caches in the format `,.zip` 21 | 22 | Options: 23 | -q, --quiet Do not output info messages. 24 | -t, --threads Number of threads to use for parallel processing 25 | "; 26 | 27 | #[derive(Debug, Deserialize)] 28 | struct Args { 29 | arg_cache_path: String, 30 | flag_quiet: bool, 31 | arg_threads: Option, 32 | } 33 | 34 | /// (biome, block) -> count 35 | type BiomeBlockCounts = HashMap<(u16, String), usize>; 36 | 37 | fn new_biome_block_counts() -> BiomeBlockCounts { 38 | HashMap::new() 39 | } 40 | fn merge_biome_block_counts_into(counts: &mut BiomeBlockCounts, other: &BiomeBlockCounts) { 41 | for (key, val) in other.iter() { 42 | *counts.entry(key.clone()).or_insert(0) += val; 43 | } 44 | } 45 | 46 | fn main() { 47 | let args: Args = Docopt::new(USAGE) 48 | .and_then(|d| d.deserialize()) 49 | .unwrap_or_else(|e| e.exit()); 50 | let verbose = !args.flag_quiet; 51 | 52 | let tile_paths = get_tile_paths_in_dirs(&vec![args.arg_cache_path.clone()], verbose) 53 | .unwrap_or_else(|e| { 54 | eprintln!("Error while listing cache directory: {:?}", e); 55 | std::process::exit(1); 56 | }); 57 | 58 | let tile_paths: Vec = tile_paths.into_iter().collect(); 59 | 60 | let total_work = tile_paths.len(); 61 | let mut progress = ProgressTracker::new(total_work); 62 | if verbose { 63 | eprintln!("Counting blocks in {:?} tiles", total_work) 64 | } 65 | 66 | let pool = ThreadPool::new(args.arg_threads.unwrap_or(4)); 67 | let (tx, rx) = channel(); 68 | 69 | // let global_keys_map = Arc::new(build_global_keys_map()); 70 | 71 | for tile_path in tile_paths.into_iter() { 72 | let tx = tx.clone(); 73 | // let global_keys_map = global_keys_map.clone(); 74 | pool.execute(move || { 75 | let result = count_tile(&tile_path); //, &global_keys_map); 76 | tx.send((tile_path, result)).expect("Sending result"); 77 | }); 78 | } 79 | 80 | let mut counts = new_biome_block_counts(); 81 | 82 | for work_done in 0..total_work { 83 | let result_with_path = rx.recv().expect("Receiving next result"); 84 | let (tile_path, result) = result_with_path; 85 | if let Err(msg) = result { 86 | eprintln!("Failed counting tile {:?} {}", tile_path, msg); 87 | return; 88 | } 89 | let tile_counts = result.unwrap(); 90 | 91 | merge_biome_block_counts_into(&mut counts, &tile_counts); 92 | 93 | progress.progress_to(work_done); 94 | if verbose { 95 | progress.print_progress(); 96 | } 97 | } 98 | 99 | let mut biome_counts = [0; 256]; 100 | for ((biome_id, _block_name), count) in counts.iter() { 101 | biome_counts[*biome_id as usize] += count; 102 | } 103 | 104 | // let counts_array = counts.iter().array(); 105 | // counts_array.sort_unstable_by_key(|((biome_id, block_name), count)| (biome_id, block_name, count)); 106 | // for (biome_id, block_name, count) in counts_array.iter() { 107 | for ((biome_id, block_name), count) in counts.iter() { 108 | let biome_name = BIOME_NAMES[*biome_id as usize]; 109 | let rel_count = *count as f32 / biome_counts[*biome_id as usize] as f32; 110 | // println!("{}\t{}\t{:10}\t{}", biome_name, biome_id, count, block_name); 111 | println!( 112 | "{}\t{}\t{}\t{}\t{}", 113 | rel_count, count, block_name, biome_name, biome_id 114 | ); 115 | } 116 | 117 | if verbose { 118 | let time_total = progress.elapsed(); 119 | let total_min = time_total.as_secs() / 60; 120 | let total_sec = time_total.as_secs() % 60; 121 | let time_per_work_item = time_total / total_work as u32; 122 | let tile_ms = time_per_work_item.as_secs() * 1_000 123 | + time_per_work_item.subsec_nanos() as u64 / 1_000_000; 124 | eprintln!( 125 | "Done counting. Took {}:{:02} for all {} tiles, {}ms per tile", 126 | total_min, total_sec, total_work, tile_ms, 127 | ); 128 | }; 129 | } 130 | 131 | // fn count_tile(tile_path: &PathBuf, global_keys_map: &KeysMap) -> Result { 132 | fn count_tile(tile_path: &PathBuf) -> Result { 133 | let tile = read_tile(tile_path).map_err(|e| e.to_string())?; 134 | 135 | let mut counts = new_biome_block_counts(); 136 | 137 | let steps_block_getters: Vec u16> = vec![ 138 | Tile::get_blockstate, 139 | Tile::get_ocean_floor_blockstate, 140 | Tile::get_transparent_blockstate, 141 | Tile::get_foliage_blockstate, 142 | ]; 143 | 144 | for column_nr in 0..TILE_COLUMNS { 145 | let biome = tile.get_biome_id(column_nr); 146 | for get_block_nr in &steps_block_getters { 147 | let block_nr = get_block_nr(&tile, column_nr) as usize; 148 | if block_nr != 0 { 149 | let block_name_full = tile.names.get(block_nr).unwrap().to_string(); 150 | let block_name_stem = block_name_full.split("[").next().unwrap().to_string(); 151 | 152 | *counts.entry((biome, block_name_stem)).or_insert(0) += 1; 153 | } 154 | } 155 | } 156 | 157 | Ok(counts) 158 | } 159 | -------------------------------------------------------------------------------- /src/bin/merge_caches.rs: -------------------------------------------------------------------------------- 1 | extern crate docopt; 2 | extern crate filetime; 3 | extern crate serde; 4 | extern crate threadpool; 5 | extern crate voxelmap_cache; 6 | extern crate zip; 7 | 8 | use docopt::Docopt; 9 | use filetime::{set_file_times, FileTime}; 10 | use serde::Deserialize; 11 | use std::fs; 12 | use std::path::PathBuf; 13 | use std::sync::mpsc::channel; 14 | use std::{ 15 | collections::HashMap, 16 | time::{Duration, SystemTime}, 17 | }; 18 | use threadpool::ThreadPool; 19 | use voxelmap_cache::tile::{ 20 | first_column_nr_of_chunk_nr, get_contrib_from_tile_path, get_tile_paths_in_dirs, 21 | get_xz_from_tile_path, is_tile_pos_in_bounds, read_tile, write_tile, KeysMap, Tile, TilePos, 22 | COLUMN_BYTES_MODERN, 23 | }; 24 | use voxelmap_cache::{ 25 | parse_bounds, ProgressTracker, CHUNK_HEIGHT, CHUNK_WIDTH, TILE_CHUNKS, TILE_COLUMNS, TILE_WIDTH, 26 | }; 27 | 28 | const USAGE: &'static str = " 29 | Usage: merge_caches [-q] [-t threads] [--between=] ... 30 | 31 | cache-path contains voxelmap caches in the format 32 | `,,.zip` or just `,.zip` 33 | 34 | Options: 35 | -q, --quiet Do not output info messages. 36 | -t, --threads Number of threads to use for parallel processing 37 | --between= Only merge tiles at least partially within this bounding box, 38 | format: w,n,e,s [default: -99999,-99999,99999,99999] 39 | "; 40 | 41 | #[derive(Debug, Deserialize)] 42 | struct Args { 43 | flag_quiet: bool, 44 | arg_threads: Option, 45 | flag_between: String, 46 | arg_output_path: String, 47 | arg_cache_path: Vec, 48 | } 49 | 50 | fn main() { 51 | let args: Args = Docopt::new(USAGE) 52 | .and_then(|d| d.deserialize()) 53 | .unwrap_or_else(|e| e.exit()); 54 | let verbose = !args.flag_quiet; 55 | 56 | let tile_paths = get_tile_paths_in_dirs(&args.arg_cache_path, verbose).unwrap_or_else(|e| { 57 | println!("Error while listing cache directory: {:?}", e); 58 | std::process::exit(1); 59 | }); 60 | 61 | let bounds = parse_bounds(&args.flag_between).unwrap_or_else(|e| { 62 | println!("Invalid arg: --between={} {}", &args.flag_between, e); 63 | std::process::exit(1); 64 | }); 65 | 66 | let tile_paths: Vec = tile_paths 67 | .into_iter() 68 | .filter(|path| is_tile_pos_in_bounds(get_xz_from_tile_path(path).unwrap(), &bounds)) 69 | .collect(); 70 | 71 | let mut tile_paths_by_pos = Box::new(HashMap::new()); 72 | for tile_path in &tile_paths { 73 | let pos = get_xz_from_tile_path(&tile_path).expect("getting pos from tile path"); 74 | tile_paths_by_pos 75 | .entry(pos) 76 | .or_insert_with(Vec::new) 77 | .push(tile_path.clone()); 78 | } 79 | 80 | // start with most intense tile positions first (most contribs per tile pos) 81 | let mut paths_sorted: Vec<(TilePos, Vec)> = tile_paths_by_pos.into_iter().collect(); 82 | paths_sorted.sort_by(|(_, a), (_, b)| b.len().cmp(&a.len())); 83 | 84 | fs::create_dir_all(&args.arg_output_path).unwrap_or_else(|e| { 85 | println!( 86 | "Failed to create output directory {:?} {:?}", 87 | &args.arg_output_path, e 88 | ); 89 | std::process::exit(1); 90 | }); 91 | 92 | let total_work = paths_sorted.len(); 93 | let mut progress = ProgressTracker::new(total_work); 94 | if verbose { 95 | println!( 96 | "Merging {:?} tiles across {:?} tile positions into {:?}", 97 | tile_paths.len(), 98 | total_work, 99 | &args.arg_output_path 100 | ) 101 | } 102 | 103 | let mut skipped_contribs = HashMap::new(); 104 | let mut total_used = 0; 105 | 106 | let pool = ThreadPool::new(args.arg_threads.unwrap_or(4)); 107 | let (tx, rx) = channel(); 108 | 109 | for (pos, tile_paths) in paths_sorted.into_iter() { 110 | let tx = tx.clone(); 111 | let (x, z) = pos; 112 | let out_path = PathBuf::from(format!("{}/{},{}.zip", args.arg_output_path, x, z)); 113 | pool.execute(move || { 114 | let result = merge_tile_from_contribs(out_path, tile_paths); 115 | tx.send(result).expect("Sending result"); 116 | }); 117 | } 118 | 119 | for work_done in 0..total_work { 120 | let (_out_path, used, skipped) = rx.recv().expect("Receiving next result"); 121 | 122 | for (path, err) in skipped { 123 | let contrib = get_contrib_from_tile_path(&path) 124 | .unwrap_or(path.parent().unwrap().to_string_lossy().into()); 125 | *skipped_contribs.entry(contrib.clone()).or_insert_with(|| { 126 | println!("Skipping contrib {:?} {}", &path, &err); 127 | 0 128 | }) += 1; 129 | } 130 | 131 | total_used += used.len(); 132 | 133 | progress.progress_to(work_done); 134 | if verbose { 135 | progress.print_progress(); 136 | } 137 | } 138 | 139 | if verbose { 140 | let time_total = progress.elapsed(); 141 | let total_min = time_total.as_secs() / 60; 142 | let total_sec = time_total.as_secs() % 60; 143 | let time_per_work_item = if total_used == 0 { 144 | Duration::from_millis(0) 145 | } else { 146 | time_total / total_used as u32 147 | }; 148 | let tile_ms = time_per_work_item.as_secs() * 1_000 149 | + time_per_work_item.subsec_nanos() as u64 / 1_000_000; 150 | println!( 151 | "Done merging. Took {}:{:02} for all {} used tiles, {}ms per tile", 152 | total_min, total_sec, total_used, tile_ms, 153 | ); 154 | }; 155 | } 156 | 157 | pub fn merge_tile_from_contribs( 158 | out_path: PathBuf, 159 | tile_paths: Vec, 160 | ) -> (PathBuf, Vec, Vec<(PathBuf, String)>) { 161 | if tile_paths.len() == 1 { 162 | // just one contrib, no merging needed, hardlink it to destination 163 | let tile_path = tile_paths.into_iter().next().unwrap(); 164 | return match std::fs::hard_link(&tile_path, &out_path) { 165 | Ok(()) => (out_path, vec![tile_path], Vec::new()), 166 | Err(e) => (out_path, Vec::new(), vec![(tile_path, e.to_string())]), 167 | }; 168 | } 169 | 170 | let mut sorted_paths: Vec<(SystemTime, PathBuf)> = tile_paths 171 | .into_iter() 172 | .map(|path| (fs::metadata(&path).unwrap().modified().unwrap(), path)) 173 | .collect(); 174 | // sort most recent first 175 | sorted_paths.sort_by(|(mtime_a, _), (mtime_b, _)| mtime_b.cmp(mtime_a)); 176 | // earliest/least recent mtime 177 | let min_mtime = sorted_paths.last().expect("contribs non-empty").0; 178 | 179 | let mut used = Vec::new(); 180 | let mut skipped = Vec::new(); 181 | 182 | let mut out_tile = Box::new(Tile { 183 | // source: Some(out_path.clone()), 184 | // pos: get_xz_from_tile_path(&out_path).ok(), 185 | pos: None, 186 | data: vec![0; TILE_COLUMNS * COLUMN_BYTES_MODERN], 187 | keys: HashMap::new(), 188 | names: vec![], // HACK: unused 189 | source: None, 190 | }); 191 | 192 | let mut num_chunks_left = TILE_CHUNKS; 193 | let mut chunks_done = vec![false; num_chunks_left]; 194 | 195 | // most recent to least recent 196 | for (_mtime, tile_path) in sorted_paths { 197 | let result = read_tile(&tile_path) 198 | .and_then(|under_tile| merge_two_tiles(&mut out_tile, &under_tile, &mut chunks_done)); 199 | num_chunks_left -= match result { 200 | Ok(chunks_processed) => { 201 | used.push(tile_path); 202 | chunks_processed 203 | } 204 | Err(e) => { 205 | skipped.push((tile_path, e)); 206 | 0 207 | } 208 | }; 209 | 210 | if num_chunks_left <= 0 { 211 | break; 212 | } 213 | } 214 | 215 | if let Err(e) = write_tile(&out_path, &out_tile) { 216 | println!("Failed writing {:?} {:?}", &out_path, e); 217 | } 218 | 219 | set_file_times( 220 | &out_path, 221 | FileTime::from(min_mtime), 222 | FileTime::from(min_mtime), 223 | ) 224 | .expect("Setting mtime"); 225 | 226 | (out_path, used, skipped) 227 | } 228 | 229 | fn merge_two_tiles( 230 | out_tile: &mut Tile, 231 | under_tile: &Tile, 232 | chunks_done: &mut Vec, 233 | ) -> Result { 234 | let mut converter = merge_keys_and_build_converter(&mut out_tile.keys, &under_tile.keys); 235 | 236 | let mut chunks_processed = 0; 237 | 238 | for chunk_nr in 0..TILE_CHUNKS { 239 | if chunks_done[chunk_nr] || under_tile.is_chunk_empty(chunk_nr) { 240 | continue; 241 | } 242 | 243 | copy_convert_chunk(&mut converter, out_tile, under_tile, chunk_nr) 244 | .map_err(|e| e.to_string())?; 245 | 246 | chunks_done[chunk_nr] = true; 247 | chunks_processed += 1; 248 | } 249 | 250 | Ok(chunks_processed) 251 | } 252 | 253 | type BlockIdConverter = Vec; 254 | 255 | fn merge_keys_and_build_converter(keys_out: &mut KeysMap, keys_in: &KeysMap) -> BlockIdConverter { 256 | let len_in = 1 + *keys_in.values().max().unwrap_or(&0) as usize; 257 | let mut next_id = keys_out.len() as u16; 258 | let mut converter = vec![0; len_in]; 259 | for (name, in_id) in keys_in { 260 | let out_id = keys_out.entry(name.clone()).or_insert_with(|| { 261 | next_id += 1; // voxelmap starts at 1 262 | next_id 263 | }); 264 | converter[*in_id as usize] = *out_id; 265 | } 266 | converter 267 | } 268 | 269 | fn copy_convert_chunk( 270 | converter: &mut BlockIdConverter, 271 | out_tile: &mut Tile, 272 | under_tile: &Tile, 273 | chunk_nr: usize, 274 | ) -> Result<(), String> { 275 | let first_chunk_column = first_column_nr_of_chunk_nr(chunk_nr); 276 | for z_in_chunk in 0..CHUNK_HEIGHT { 277 | let line_start = first_chunk_column + z_in_chunk * TILE_WIDTH; 278 | for x_in_chunk in 0..CHUNK_WIDTH { 279 | let column_nr = line_start + x_in_chunk; 280 | 281 | out_tile.set_height(column_nr, under_tile.get_height(column_nr)); 282 | out_tile.set_light(column_nr, under_tile.get_light(column_nr)); 283 | out_tile 284 | .set_ocean_floor_height(column_nr, under_tile.get_ocean_floor_height(column_nr)); 285 | out_tile.set_ocean_floor_light(column_nr, under_tile.get_ocean_floor_light(column_nr)); 286 | out_tile 287 | .set_transparent_height(column_nr, under_tile.get_transparent_height(column_nr)); 288 | out_tile.set_transparent_light(column_nr, under_tile.get_transparent_light(column_nr)); 289 | out_tile.set_foliage_height(column_nr, under_tile.get_foliage_height(column_nr)); 290 | out_tile.set_foliage_light(column_nr, under_tile.get_foliage_light(column_nr)); 291 | out_tile.set_biome_id(column_nr, under_tile.get_biome_id(column_nr)); 292 | 293 | out_tile.set_blockstate( 294 | column_nr, 295 | converter[under_tile.get_blockstate(column_nr) as usize], 296 | ); 297 | out_tile.set_ocean_floor_blockstate( 298 | column_nr, 299 | converter[under_tile.get_ocean_floor_blockstate(column_nr) as usize], 300 | ); 301 | out_tile.set_transparent_blockstate( 302 | column_nr, 303 | converter[under_tile.get_transparent_blockstate(column_nr) as usize], 304 | ); 305 | out_tile.set_foliage_blockstate( 306 | column_nr, 307 | converter[under_tile.get_foliage_blockstate(column_nr) as usize], 308 | ); 309 | } 310 | } 311 | Ok(()) 312 | } 313 | -------------------------------------------------------------------------------- /src/bin/palette.rs: -------------------------------------------------------------------------------- 1 | extern crate lodepng; 2 | extern crate voxelmap_cache; 3 | 4 | use std::time; 5 | use voxelmap_cache::colorizer::*; 6 | 7 | fn main() { 8 | let unix_time = time::SystemTime::now() 9 | .duration_since(time::UNIX_EPOCH) 10 | .unwrap() 11 | .as_secs(); 12 | let img_path = std::env::args() 13 | .skip(1) 14 | .next() 15 | .unwrap_or("palette_{t}.png".to_string()) 16 | .replace("{t}", &unix_time.to_string()); 17 | 18 | let mut pixbuf = [0_u32; 100 * 256]; 19 | for h in 1..257_usize { 20 | let color_land = get_land_color(h as u8); 21 | for x in 0..50 { 22 | pixbuf[x + 100 * (256 - h) as usize] = color_land; 23 | } 24 | let color_water = get_sea_color(h as u8); 25 | for x in 50..100 { 26 | pixbuf[x + 100 * (256 - h) as usize] = color_water; 27 | } 28 | } 29 | 30 | print!("Saving as {}\n", img_path); 31 | lodepng::encode32_file(img_path, &pixbuf, 100, 256).unwrap(); 32 | } 33 | -------------------------------------------------------------------------------- /src/bin/render.rs: -------------------------------------------------------------------------------- 1 | extern crate docopt; 2 | extern crate lodepng; 3 | extern crate serde; 4 | extern crate threadpool; 5 | extern crate voxelmap_cache; 6 | extern crate zip; 7 | 8 | use docopt::Docopt; 9 | use serde::Deserialize; 10 | use std::iter::FromIterator; 11 | use std::path::PathBuf; 12 | use std::sync::mpsc::channel; 13 | use std::sync::Arc; 14 | use std::{fs, time::Duration}; 15 | use threadpool::ThreadPool; 16 | use voxelmap_cache::colorizer::Colorizer; 17 | use voxelmap_cache::mc::blocks::BLOCK_STRINGS_ARR; 18 | use voxelmap_cache::tile::{ 19 | get_tile_paths_in_dirs, get_xz_from_tile_path, is_tile_pos_in_bounds, read_tile, KeysMap, 20 | NamesVec, 21 | }; 22 | use voxelmap_cache::{parse_bounds, ProgressTracker, TILE_COLUMNS, TILE_HEIGHT, TILE_WIDTH}; 23 | 24 | const USAGE: &'static str = " 25 | Usage: render [-q] [-t threads] [--between=] (simple | light | biome | height | height-bw | naturality | terrain) 26 | 27 | cache-path contains voxelmap caches in the format `,.zip` 28 | 29 | output-path is a directory that will contain the rendered tiles 30 | 31 | Options: 32 | -q, --quiet Do not output info messages. 33 | -t, --threads Number of threads to use for parallel processing 34 | --between= Only render tiles at least partially within this bounding box, 35 | format: w,n,e,s [default: -99999,-99999,99999,99999] 36 | "; 37 | 38 | // TODO allow output to be a .png (single output image) 39 | 40 | #[derive(Debug, Deserialize)] 41 | struct Args { 42 | flag_between: String, 43 | arg_output_path: String, 44 | arg_cache_path: String, 45 | flag_quiet: bool, 46 | cmd_simple: bool, 47 | cmd_light: bool, 48 | cmd_biome: bool, 49 | cmd_height: bool, 50 | cmd_height_bw: bool, 51 | cmd_naturality: bool, 52 | cmd_terrain: bool, 53 | arg_threads: Option, 54 | } 55 | 56 | impl Args { 57 | fn get_colorizer(&self) -> Colorizer { 58 | if self.cmd_simple { 59 | Colorizer::Simple 60 | } else if self.cmd_light { 61 | Colorizer::Light 62 | } else if self.cmd_biome { 63 | Colorizer::Biome 64 | } else if self.cmd_height { 65 | Colorizer::Height 66 | } else if self.cmd_height_bw { 67 | Colorizer::HeightBW 68 | } else if self.cmd_naturality { 69 | Colorizer::Naturality 70 | } else { 71 | panic!("Unknown colorizer selected") 72 | } 73 | } 74 | } 75 | 76 | #[derive(Debug)] 77 | struct RenderConfig { 78 | colorizer: Colorizer, 79 | global_keys: KeysMap, 80 | global_names: NamesVec, 81 | } 82 | 83 | #[derive(Debug)] 84 | struct OutputConfig<'a> { 85 | output_path: &'a String, 86 | } 87 | 88 | fn main() { 89 | let args: Args = Docopt::new(USAGE) 90 | .and_then(|d| d.deserialize()) 91 | .unwrap_or_else(|e| e.exit()); 92 | let verbose = !args.flag_quiet; 93 | 94 | let tile_paths = get_tile_paths_in_dirs(&vec![args.arg_cache_path.clone()], verbose) 95 | .unwrap_or_else(|e| { 96 | eprintln!("Error while listing cache directory: {:?}", e); 97 | std::process::exit(1); 98 | }); 99 | 100 | let bounds = parse_bounds(&args.flag_between).unwrap_or_else(|e| { 101 | eprintln!("Invalid arg: --between={} {}", &args.flag_between, e); 102 | std::process::exit(1); 103 | }); 104 | 105 | let tile_paths: Vec = tile_paths 106 | .into_iter() 107 | .filter(|path| is_tile_pos_in_bounds(get_xz_from_tile_path(path).unwrap(), &bounds)) 108 | .collect(); 109 | 110 | fs::create_dir_all(&args.arg_output_path).unwrap_or_else(|e| { 111 | eprintln!( 112 | "Failed to create output directory {:?} {:?}", 113 | &args.arg_output_path, e 114 | ); 115 | std::process::exit(1); 116 | }); 117 | 118 | let total_work = tile_paths.len(); 119 | let mut progress = ProgressTracker::new(total_work); 120 | if verbose { 121 | eprintln!( 122 | "Rendering {:?} tiles to {:?}", 123 | total_work, &args.arg_output_path 124 | ) 125 | } 126 | 127 | let render_config = Arc::new(RenderConfig { 128 | colorizer: args.get_colorizer(), 129 | global_keys: build_global_keys_map(), 130 | global_names: BLOCK_STRINGS_ARR.iter().map(|x| x.to_string()).collect(), 131 | }); 132 | 133 | let pool = ThreadPool::new(args.arg_threads.unwrap_or(4)); 134 | let (tx, rx) = channel(); 135 | 136 | for tile_path in tile_paths.into_iter() { 137 | let tx = tx.clone(); 138 | let render_config = render_config.clone(); 139 | pool.execute(move || { 140 | let result = render_tile(&tile_path, &render_config); 141 | tx.send((tile_path, result)).expect("Sending result"); 142 | }); 143 | } 144 | 145 | let output_config = OutputConfig { 146 | output_path: &args.arg_output_path, 147 | }; 148 | 149 | for work_done in 0..total_work { 150 | let result = rx.recv().expect("Receiving next result"); 151 | 152 | process_result(result, &output_config); 153 | 154 | progress.progress_to(work_done); 155 | if verbose { 156 | progress.print_progress(); 157 | } 158 | } 159 | 160 | if verbose { 161 | let time_total = progress.elapsed(); 162 | let total_min = time_total.as_secs() / 60; 163 | let total_sec = time_total.as_secs() % 60; 164 | let time_per_work_item = if total_work == 0 { 165 | Duration::from_millis(0) 166 | } else { 167 | time_total / total_work as u32 168 | }; 169 | let tile_ms = time_per_work_item.as_secs() * 1_000 170 | + time_per_work_item.subsec_nanos() as u64 / 1_000_000; 171 | eprintln!( 172 | "Done rendering. Took {}:{:02} for all {} tiles, {}ms per tile", 173 | total_min, total_sec, total_work, tile_ms, 174 | ); 175 | }; 176 | } 177 | 178 | fn process_result( 179 | result_with_path: (PathBuf, Result, String>), 180 | config: &OutputConfig, 181 | ) -> () { 182 | let (tile_path, result) = result_with_path; 183 | if let Err(msg) = result { 184 | eprintln!("Failed rendering tile {:?} {}", tile_path, msg); 185 | return; 186 | } 187 | 188 | let (x, z) = get_xz_from_tile_path(&tile_path).expect("Getting tile position"); 189 | let img_path = format!("{}/{:?},{:?}.png", config.output_path, x, z); 190 | 191 | fs::create_dir_all(config.output_path).expect(&format!( 192 | "Creating containing directory for tile {}", 193 | img_path 194 | )); 195 | 196 | let pixbuf = result.expect("error already handled"); 197 | lodepng::encode32_file(&img_path, &pixbuf[..], TILE_WIDTH, TILE_HEIGHT) 198 | .expect(&format!("Encoding tile {}", img_path)); 199 | } 200 | 201 | fn render_tile(tile_path: &PathBuf, config: &RenderConfig) -> Result, String> { 202 | let tile = read_tile(tile_path).map_err(|e| e.to_string())?; 203 | let mut pixbuf = vec![0_u32; TILE_COLUMNS]; 204 | 205 | let get_column_color = config.colorizer.get_column_color_fn(); 206 | for i in 0..TILE_COLUMNS { 207 | pixbuf[i] = get_column_color(&tile, i); 208 | } 209 | 210 | Ok(pixbuf) 211 | } 212 | 213 | fn build_global_keys_map() -> KeysMap { 214 | KeysMap::from_iter( 215 | BLOCK_STRINGS_ARR 216 | .iter() 217 | .enumerate() 218 | .rev() 219 | .map(|(i, s)| (s.to_string(), i as u16)), 220 | ) 221 | } 222 | -------------------------------------------------------------------------------- /src/bin/replay.rs: -------------------------------------------------------------------------------- 1 | extern crate docopt; 2 | extern crate lodepng; 3 | extern crate serde; 4 | extern crate threadpool; 5 | extern crate voxelmap_cache; 6 | extern crate zip; 7 | 8 | use docopt::Docopt; 9 | use serde::Deserialize; 10 | use voxelmap_cache::buf_rw::UUID; 11 | use voxelmap_cache::mc::packet::{ChunkData, McPacket}; 12 | use voxelmap_cache::replay::{read_info, read_replay}; 13 | 14 | const USAGE: &'static str = " 15 | Usage: replay [-q] [--filter=] [--follow=] [--server=
] 16 | 17 | path points to a (date).mcpr file like found in .minecraft/replay_recordings/ 18 | 19 | Options: 20 | -q, --quiet Do not output info messages. 21 | --filter= Only print packets with the given comma-separated ids. 22 | --follow= Track player position for this uuid (usually the recording player). 23 | --server=
Ignore replays on other servers. 24 | "; 25 | 26 | #[derive(Debug, Deserialize)] 27 | struct Args { 28 | flag_filter: Option, 29 | flag_follow: Option, 30 | flag_server: Option, 31 | arg_path: String, 32 | flag_quiet: bool, 33 | } 34 | 35 | impl Args { 36 | fn get_id_filter(&self) -> Result>, String> { 37 | if let Some(ref s) = self.flag_filter { 38 | Ok(Some( 39 | s.as_str() 40 | .split(",") 41 | .map(str::parse) 42 | .collect::, _>>() 43 | .map_err(|e| e.to_string())?, 44 | )) 45 | } else { 46 | Ok(None) 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug)] 52 | struct Player { 53 | uuid: UUID, 54 | x: f64, 55 | y: f64, 56 | z: f64, 57 | } 58 | 59 | impl Player { 60 | fn new(uuid: UUID) -> Self { 61 | Self { 62 | uuid, 63 | x: 0.0, 64 | y: 0.0, 65 | z: 0.0, 66 | } 67 | } 68 | fn set_pos(&mut self, x: f64, y: f64, z: f64) { 69 | self.x = x; 70 | self.y = y; 71 | self.z = z; 72 | } 73 | fn move_by(&mut self, dx: f64, dy: f64, dz: f64) { 74 | self.x += dx; 75 | self.y += dy; 76 | self.z += dz; 77 | } 78 | fn on_packet(&mut self, packet: &McPacket) { 79 | match packet { 80 | McPacket::SpawnPlayer { x, y, z, .. } => self.set_pos(*x, *y, *z), 81 | McPacket::EntityTeleport { x, y, z, .. } => self.set_pos(*x, *y, *z), 82 | McPacket::EntityLookAndRelativeMove { dx, dy, dz, .. } => self.move_by(*dx, *dy, *dz), 83 | McPacket::EntityRelativeMove { dx, dy, dz, .. } => self.move_by(*dx, *dy, *dz), 84 | _ => (), 85 | } 86 | } 87 | } 88 | 89 | fn is_player_id(id: u8) -> bool { 90 | id == 0x05 || id == 0x26 || id == 0x27 || id == 0x4C 91 | } 92 | 93 | fn main() { 94 | let args: Args = Docopt::new(USAGE) 95 | .and_then(|d| d.deserialize()) 96 | .unwrap_or_else(|e| e.exit()); 97 | let verbose = !args.flag_quiet; 98 | let filter_ids = args.get_id_filter().expect("Malformed filter argument"); 99 | 100 | let mut followed = args.flag_follow.as_ref().map(|ref uuid_str| { 101 | match UUID::from_str(uuid_str) { 102 | Ok(uuid) => Player::new(uuid), 103 | Err(e) => { 104 | eprintln!("Error: {}", e); 105 | std::process::exit(1); 106 | } 107 | } 108 | }); 109 | 110 | if let Some(ref address) = args.flag_server { 111 | let replay_info = read_info(&args.arg_path).expect("Reading replay info"); 112 | if address != &replay_info.server_name { 113 | if verbose { 114 | eprintln!( 115 | "Skipping replay on wrong server {}", 116 | &replay_info.server_name 117 | ); 118 | } 119 | return; 120 | } 121 | } 122 | 123 | if let Some(ref player) = followed { 124 | if verbose { 125 | eprintln!( 126 | "Following player {} {:?}", 127 | &args.flag_follow.unwrap(), 128 | player.uuid 129 | ); 130 | } 131 | } 132 | 133 | if verbose { 134 | eprint!("Reading replay {} ... ", &args.arg_path); 135 | } 136 | let replay = read_replay(&args.arg_path).expect("Reading replay file"); 137 | if verbose { 138 | let info = &replay.info; 139 | eprintln!( 140 | "from {} for {}ms with {} on {}", 141 | info.date, info.duration, info.mc_version, info.server_name, 142 | ); 143 | } 144 | 145 | for mut packet in replay { 146 | if followed.is_some() && is_player_id(packet.id) { 147 | if let Err(_e) = packet.parse_packet() { 148 | eprintln!("{} {} failed to parse", &packet.date, &packet.id); 149 | std::process::exit(1); 150 | } 151 | 152 | if let Some(ref mc_packet) = packet.get_packet() { 153 | if let Some(player) = followed.as_mut() { 154 | player.on_packet(mc_packet); 155 | } 156 | } 157 | } 158 | 159 | if let Some(ref ids) = filter_ids { 160 | if ids.iter().all(|id| *id != packet.id) { 161 | continue; 162 | } 163 | } 164 | 165 | if let Err(_e) = packet.parse_packet() { 166 | eprintln!("{} {} failed to parse", &packet.date, &packet.id); 167 | std::process::exit(1); 168 | } 169 | 170 | match packet.get_packet() { 171 | Some(McPacket::Chat { message, position }) => { 172 | println!("{} chat {}: {}", &packet.date, position, message) 173 | } 174 | Some(McPacket::ChunkDataHack(ChunkData { x, z, is_new, .. })) => { 175 | println!("{} chunk {},{} {}", &packet.date, x, z, is_new) 176 | } 177 | 178 | Some(McPacket::SpawnPlayer { 179 | eid, uuid, x, y, z, .. 180 | }) => println!( 181 | "{} player {},{},{} {} {:?}", 182 | &packet.date, x, y, z, eid, uuid 183 | ), 184 | 185 | _ => println!("{} {}", &packet.date, &packet.id), 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/biomes.rs: -------------------------------------------------------------------------------- 1 | pub const BIOME_COLOR_TABLE: [u32; 256] = [ 2 | 0xff700000, 3 | 0xff60b38d, 4 | 0xff1894fa, 5 | 0xff606060, 6 | 0xff216605, 7 | 0xff59660b, 8 | 0xffb2f907, 9 | 0xffff0000, 10 | 0xff0000ff, 11 | 0xffff8080, 12 | 0xffa09090, 13 | 0xffffa0a0, 14 | 0xffffffff, 15 | 0xffa0a0a0, 16 | 0xffff00ff, 17 | 0xffff00a0, 18 | 0xff55defa, 19 | 0xff125fd2, 20 | 0xff1c5522, 21 | 0xff333916, 22 | 0xff9a7872, 23 | 0xff097b53, 24 | 0xff05422c, 25 | 0xff178b62, 26 | 0xff300000, 27 | 0xff84a2a2, 28 | 0xffc0f0fa, 29 | 0xff447430, 30 | 0xff325f1f, 31 | 0xff1a5140, 32 | 0xff4a5531, 33 | 0xff363f24, 34 | 0xff516659, 35 | 0xff3e4f45, 36 | 0xff507050, 37 | 0xff5fb2bd, 38 | 0xff649da7, 39 | 0xff1545d9, 40 | 0xff6597b0, 41 | 0xff658cca, 42 | 0x88ff00ff, 43 | 0x88ff00ff, 44 | 0x88ff00ff, 45 | 0x88ff00ff, 46 | 0x88ff00ff, 47 | 0x88ff00ff, 48 | 0x88ff00ff, 49 | 0x88ff00ff, 50 | 0x88ff00ff, 51 | 0x88ff00ff, 52 | 0x88ff00ff, 53 | 0x88ff00ff, 54 | 0x88ff00ff, 55 | 0x88ff00ff, 56 | 0x88ff00ff, 57 | 0x88ff00ff, 58 | 0x88ff00ff, 59 | 0x88ff00ff, 60 | 0x88ff00ff, 61 | 0x88ff00ff, 62 | 0x88ff00ff, 63 | 0x88ff00ff, 64 | 0x88ff00ff, 65 | 0x88ff00ff, 66 | 0x88ff00ff, 67 | 0x88ff00ff, 68 | 0x88ff00ff, 69 | 0x88ff00ff, 70 | 0x88ff00ff, 71 | 0x88ff00ff, 72 | 0x88ff00ff, 73 | 0x88ff00ff, 74 | 0x88ff00ff, 75 | 0x88ff00ff, 76 | 0x88ff00ff, 77 | 0x88ff00ff, 78 | 0x88ff00ff, 79 | 0x88ff00ff, 80 | 0x88ff00ff, 81 | 0x88ff00ff, 82 | 0x88ff00ff, 83 | 0x88ff00ff, 84 | 0x88ff00ff, 85 | 0x88ff00ff, 86 | 0x88ff00ff, 87 | 0x88ff00ff, 88 | 0x88ff00ff, 89 | 0x88ff00ff, 90 | 0x88ff00ff, 91 | 0x88ff00ff, 92 | 0x88ff00ff, 93 | 0x88ff00ff, 94 | 0x88ff00ff, 95 | 0x88ff00ff, 96 | 0x88ff00ff, 97 | 0x88ff00ff, 98 | 0x88ff00ff, 99 | 0x88ff00ff, 100 | 0x88ff00ff, 101 | 0x88ff00ff, 102 | 0x88ff00ff, 103 | 0x88ff00ff, 104 | 0x88ff00ff, 105 | 0x88ff00ff, 106 | 0x88ff00ff, 107 | 0x88ff00ff, 108 | 0x88ff00ff, 109 | 0x88ff00ff, 110 | 0x88ff00ff, 111 | 0x88ff00ff, 112 | 0x88ff00ff, 113 | 0x88ff00ff, 114 | 0x88ff00ff, 115 | 0x88ff00ff, 116 | 0x88ff00ff, 117 | 0x88ff00ff, 118 | 0x88ff00ff, 119 | 0x88ff00ff, 120 | 0x88ff00ff, 121 | 0x88ff00ff, 122 | 0x88ff00ff, 123 | 0x88ff00ff, 124 | 0x88ff00ff, 125 | 0x88ff00ff, 126 | 0x88ff00ff, 127 | 0x88ff00ff, 128 | 0x88ff00ff, 129 | 0x88ff00ff, 130 | 0xffff77ff, 131 | 0xff60b38d, 132 | 0xff1894fa, 133 | 0xff606060, 134 | 0xff25746a, 135 | 0xff59660b, 136 | 0xffb2f907, 137 | 0x88ff00ff, 138 | 0x88ff00ff, 139 | 0x88ff00ff, 140 | 0x88ff00ff, 141 | 0x88ff00ff, 142 | 0xffffffd2, 143 | 0x88ff00ff, 144 | 0x88ff00ff, 145 | 0x88ff00ff, 146 | 0x88ff00ff, 147 | 0x88ff00ff, 148 | 0x88ff00ff, 149 | 0x88ff00ff, 150 | 0x88ff00ff, 151 | 0xff097b53, 152 | 0x88ff00ff, 153 | 0xff178b62, 154 | 0x88ff00ff, 155 | 0x88ff00ff, 156 | 0x88ff00ff, 157 | 0xff447430, 158 | 0xff325f1f, 159 | 0xff1a5140, 160 | 0xff4a5531, 161 | 0x88ff00ff, 162 | 0xff516659, 163 | 0xff75a4e6, 164 | 0xff507050, 165 | 0xff5fb2bd, 166 | 0xff649da7, 167 | 0xff1545d9, 168 | 0xff6597b0, 169 | 0xff658cca, 170 | 0x88ff00ff, 171 | 0x88ff00ff, 172 | 0x88ff00ff, 173 | 0x88ff00ff, 174 | 0x88ff00ff, 175 | 0x88ff00ff, 176 | 0x88ff00ff, 177 | 0x88ff00ff, 178 | 0x88ff00ff, 179 | 0x88ff00ff, 180 | 0x88ff00ff, 181 | 0x88ff00ff, 182 | 0x88ff00ff, 183 | 0x88ff00ff, 184 | 0x88ff00ff, 185 | 0x88ff00ff, 186 | 0x88ff00ff, 187 | 0x88ff00ff, 188 | 0x88ff00ff, 189 | 0x88ff00ff, 190 | 0x88ff00ff, 191 | 0x88ff00ff, 192 | 0x88ff00ff, 193 | 0x88ff00ff, 194 | 0x88ff00ff, 195 | 0x88ff00ff, 196 | 0x88ff00ff, 197 | 0x88ff00ff, 198 | 0x88ff00ff, 199 | 0x88ff00ff, 200 | 0x88ff00ff, 201 | 0x88ff00ff, 202 | 0x88ff00ff, 203 | 0x88ff00ff, 204 | 0x88ff00ff, 205 | 0x88ff00ff, 206 | 0x88ff00ff, 207 | 0x88ff00ff, 208 | 0x88ff00ff, 209 | 0x88ff00ff, 210 | 0x88ff00ff, 211 | 0x88ff00ff, 212 | 0x88ff00ff, 213 | 0x88ff00ff, 214 | 0x88ff00ff, 215 | 0x88ff00ff, 216 | 0x88ff00ff, 217 | 0x88ff00ff, 218 | 0x88ff00ff, 219 | 0x88ff00ff, 220 | 0x88ff00ff, 221 | 0x88ff00ff, 222 | 0x88ff00ff, 223 | 0x88ff00ff, 224 | 0x88ff00ff, 225 | 0x88ff00ff, 226 | 0x88ff00ff, 227 | 0x88ff00ff, 228 | 0x88ff00ff, 229 | 0x88ff00ff, 230 | 0x88ff00ff, 231 | 0x88ff00ff, 232 | 0x88ff00ff, 233 | 0x88ff00ff, 234 | 0x88ff00ff, 235 | 0x88ff00ff, 236 | 0x88ff00ff, 237 | 0x88ff00ff, 238 | 0x88ff00ff, 239 | 0x88ff00ff, 240 | 0x88ff00ff, 241 | 0x88ff00ff, 242 | 0x88ff00ff, 243 | 0x88ff00ff, 244 | 0x88ff00ff, 245 | 0x88ff00ff, 246 | 0x88ff00ff, 247 | 0x88ff00ff, 248 | 0x88ff00ff, 249 | 0x88ff00ff, 250 | 0x88ff00ff, 251 | 0x88ff00ff, 252 | 0x88ff00ff, 253 | 0x88ff00ff, 254 | 0x88ff00ff, 255 | 0x88ff00ff, 256 | 0x88ff00ff, 257 | 0x88ff00ff, 258 | ]; 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | const unknown_biome_name: &'static str = "?UNKNOWN_BIOME?"; 298 | 299 | pub const BIOME_NAMES: [&'static str; 256] = [ 300 | "Ocean", // 0 301 | "Plains", // 1 302 | "Desert", // 2 303 | "Extreme Hills", // 3 304 | "Forest", // 4 305 | "Taiga", // 5 306 | "Swampland", // 6 307 | "River", // 7 308 | "Hell", // 8 309 | "The End", // 9 310 | "FrozenOcean", // 10 311 | "FrozenRiver", // 11 312 | "Ice Plains", // 12 313 | "Ice Mountains", // 13 314 | "MushroomIsland", // 14 315 | "MushroomIslandShore", // 15 316 | "Beach", // 16 317 | "DesertHills", // 17 318 | "ForestHills", // 18 319 | "TaigaHills", // 19 320 | "Extreme Hills Edge", // 20 321 | "Jungle", // 21 322 | "JungleHills", // 22 323 | "JungleEdge", // 23 324 | "Deep Ocean", // 24 325 | "Stone Beach", // 25 326 | "Cold Beach", // 26 327 | "Birch Forest", // 27 328 | "Birch Forest Hills", // 28 329 | "Roofed Forest", // 29 330 | "Cold Taiga", // 30 331 | "Cold Taiga Hills", // 31 332 | "Mega Taiga", // 32 333 | "Mega Taiga Hills", // 33 334 | "Extreme Hills+", // 34 335 | "Savanna", // 35 336 | "Savanna Plateau", // 36 337 | "Mesa", // 37 338 | "Mesa Plateau F", // 38 339 | "Mesa Plateau", // 39 340 | unknown_biome_name, 341 | unknown_biome_name, 342 | unknown_biome_name, 343 | unknown_biome_name, 344 | unknown_biome_name, 345 | unknown_biome_name, 346 | unknown_biome_name, 347 | unknown_biome_name, 348 | unknown_biome_name, 349 | unknown_biome_name, 350 | unknown_biome_name, 351 | unknown_biome_name, 352 | unknown_biome_name, 353 | unknown_biome_name, 354 | unknown_biome_name, 355 | unknown_biome_name, 356 | unknown_biome_name, 357 | unknown_biome_name, 358 | unknown_biome_name, 359 | unknown_biome_name, 360 | unknown_biome_name, 361 | unknown_biome_name, 362 | unknown_biome_name, 363 | unknown_biome_name, 364 | unknown_biome_name, 365 | unknown_biome_name, 366 | unknown_biome_name, 367 | unknown_biome_name, 368 | unknown_biome_name, 369 | unknown_biome_name, 370 | unknown_biome_name, 371 | unknown_biome_name, 372 | unknown_biome_name, 373 | unknown_biome_name, 374 | unknown_biome_name, 375 | unknown_biome_name, 376 | unknown_biome_name, 377 | unknown_biome_name, 378 | unknown_biome_name, 379 | unknown_biome_name, 380 | unknown_biome_name, 381 | unknown_biome_name, 382 | unknown_biome_name, 383 | unknown_biome_name, 384 | unknown_biome_name, 385 | unknown_biome_name, 386 | unknown_biome_name, 387 | unknown_biome_name, 388 | unknown_biome_name, 389 | unknown_biome_name, 390 | unknown_biome_name, 391 | unknown_biome_name, 392 | unknown_biome_name, 393 | unknown_biome_name, 394 | unknown_biome_name, 395 | unknown_biome_name, 396 | unknown_biome_name, 397 | unknown_biome_name, 398 | unknown_biome_name, 399 | unknown_biome_name, 400 | unknown_biome_name, 401 | unknown_biome_name, 402 | unknown_biome_name, 403 | unknown_biome_name, 404 | unknown_biome_name, 405 | unknown_biome_name, 406 | unknown_biome_name, 407 | unknown_biome_name, 408 | unknown_biome_name, 409 | unknown_biome_name, 410 | unknown_biome_name, 411 | unknown_biome_name, 412 | unknown_biome_name, 413 | unknown_biome_name, 414 | unknown_biome_name, 415 | unknown_biome_name, 416 | unknown_biome_name, 417 | unknown_biome_name, 418 | unknown_biome_name, 419 | unknown_biome_name, 420 | unknown_biome_name, 421 | unknown_biome_name, 422 | unknown_biome_name, 423 | unknown_biome_name, 424 | unknown_biome_name, 425 | unknown_biome_name, 426 | unknown_biome_name, 427 | unknown_biome_name, 428 | "Plains M", // 128 429 | "Sunflower Plains", // 129 430 | "Desert M", // 130 431 | "Extreme Hills M", // 131 432 | "Flower Forest", // 132 433 | "Taiga M", // 133 434 | "Swampland M", // 134 435 | unknown_biome_name, 436 | unknown_biome_name, 437 | unknown_biome_name, 438 | unknown_biome_name, 439 | unknown_biome_name, 440 | "Ice Plains Spikes", // 140 441 | unknown_biome_name, 442 | unknown_biome_name, 443 | unknown_biome_name, 444 | unknown_biome_name, 445 | unknown_biome_name, 446 | unknown_biome_name, 447 | unknown_biome_name, 448 | unknown_biome_name, 449 | "Jungle M", // 149 450 | unknown_biome_name, 451 | "JungleEdge M", // 151 452 | unknown_biome_name, 453 | unknown_biome_name, 454 | unknown_biome_name, 455 | "Birch Forest M", // 155 456 | "Birch Forest Hills M", // 156 457 | "Roofed Forest M", // 157 458 | "Cold Taiga M", // 158 459 | unknown_biome_name, 460 | "Mega Spruce Taiga", // 160 461 | "Redwood Taiga Hills M", // 161 462 | "Extreme Hills+ M", // 162 463 | "Savanna M", // 163 464 | "Savanna Plateau M", // 164 465 | "Mesa (Bryce)", // 165 466 | "Mesa Plateau F M", // 166 467 | "Mesa Plateau M", // 167 468 | unknown_biome_name, 469 | unknown_biome_name, 470 | unknown_biome_name, 471 | unknown_biome_name, 472 | unknown_biome_name, 473 | unknown_biome_name, 474 | unknown_biome_name, 475 | unknown_biome_name, 476 | unknown_biome_name, 477 | unknown_biome_name, 478 | unknown_biome_name, 479 | unknown_biome_name, 480 | unknown_biome_name, 481 | unknown_biome_name, 482 | unknown_biome_name, 483 | unknown_biome_name, 484 | unknown_biome_name, 485 | unknown_biome_name, 486 | unknown_biome_name, 487 | unknown_biome_name, 488 | unknown_biome_name, 489 | unknown_biome_name, 490 | unknown_biome_name, 491 | unknown_biome_name, 492 | unknown_biome_name, 493 | unknown_biome_name, 494 | unknown_biome_name, 495 | unknown_biome_name, 496 | unknown_biome_name, 497 | unknown_biome_name, 498 | unknown_biome_name, 499 | unknown_biome_name, 500 | unknown_biome_name, 501 | unknown_biome_name, 502 | unknown_biome_name, 503 | unknown_biome_name, 504 | unknown_biome_name, 505 | unknown_biome_name, 506 | unknown_biome_name, 507 | unknown_biome_name, 508 | unknown_biome_name, 509 | unknown_biome_name, 510 | unknown_biome_name, 511 | unknown_biome_name, 512 | unknown_biome_name, 513 | unknown_biome_name, 514 | unknown_biome_name, 515 | unknown_biome_name, 516 | unknown_biome_name, 517 | unknown_biome_name, 518 | unknown_biome_name, 519 | unknown_biome_name, 520 | unknown_biome_name, 521 | unknown_biome_name, 522 | unknown_biome_name, 523 | unknown_biome_name, 524 | unknown_biome_name, 525 | unknown_biome_name, 526 | unknown_biome_name, 527 | unknown_biome_name, 528 | unknown_biome_name, 529 | unknown_biome_name, 530 | unknown_biome_name, 531 | unknown_biome_name, 532 | unknown_biome_name, 533 | unknown_biome_name, 534 | unknown_biome_name, 535 | unknown_biome_name, 536 | unknown_biome_name, 537 | unknown_biome_name, 538 | unknown_biome_name, 539 | unknown_biome_name, 540 | unknown_biome_name, 541 | unknown_biome_name, 542 | unknown_biome_name, 543 | unknown_biome_name, 544 | unknown_biome_name, 545 | unknown_biome_name, 546 | unknown_biome_name, 547 | unknown_biome_name, 548 | unknown_biome_name, 549 | unknown_biome_name, 550 | unknown_biome_name, 551 | unknown_biome_name, 552 | unknown_biome_name, 553 | unknown_biome_name, 554 | unknown_biome_name, 555 | unknown_biome_name, 556 | ]; 557 | -------------------------------------------------------------------------------- /src/buf_rw.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_serialize; 2 | 3 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 4 | use std::fmt; 5 | use std::io::{Cursor, Read, Write}; 6 | 7 | error_chain!{ 8 | types { 9 | BufErr, BufErrorKind, BufResult; 10 | } 11 | foreign_links { 12 | Io(::std::io::Error); 13 | Utf8(::std::str::Utf8Error); 14 | FromHex(rustc_serialize::hex::FromHexError); 15 | } 16 | } 17 | 18 | #[derive(PartialEq, Eq, Hash, Clone)] 19 | pub struct UUID(u64, u64); 20 | 21 | impl UUID { 22 | pub fn from_str(s: &str) -> Result { 23 | use self::rustc_serialize::hex::FromHex; 24 | let s = match s.len() { 25 | 36 => s.replace("-", ""), 26 | 32 => s.to_owned(), 27 | _ => return Err("Invalid UUID format")?, 28 | }; 29 | let parts = s.from_hex()?; 30 | let mut high = 0u64; 31 | let mut low = 0u64; 32 | for i in 0..8 { 33 | high |= (parts[i] as u64) << (56 - i * 8); 34 | low |= (parts[i + 8] as u64) << (56 - i * 8); 35 | } 36 | Ok(UUID(high, low)) 37 | } 38 | } 39 | 40 | impl fmt::Debug for UUID { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | write!(f, "{:02x}{:02x}", self.0, self.1) 43 | } 44 | } 45 | 46 | pub struct BufReader { 47 | len: usize, 48 | c: Cursor>, 49 | } 50 | 51 | impl BufReader { 52 | pub fn new(data: Vec) -> Self { 53 | Self { 54 | len: data.len(), 55 | c: Cursor::new(data), 56 | } 57 | } 58 | 59 | pub fn len(&self) -> usize { 60 | self.len 61 | } 62 | 63 | pub fn position(&self) -> u64 { 64 | self.c.position() 65 | } 66 | 67 | pub fn read_bytes(&mut self, size: usize) -> Result, BufErr> { 68 | let mut v = vec![0; size]; 69 | self.c.read_exact(&mut v)?; 70 | Ok(v) 71 | } 72 | 73 | pub fn read_varint_prefixed_bytes(&mut self) -> Result, BufErr> { 74 | let size = self.read_varint()? as usize; 75 | let mut v = vec![0; size]; 76 | self.c.read_exact(&mut v)?; 77 | Ok(v) 78 | } 79 | 80 | pub fn read_remainder(&mut self) -> Result, BufErr> { 81 | let size = self.len() - self.c.position() as usize; 82 | self.read_bytes(size) 83 | } 84 | 85 | pub fn read_u8(&mut self) -> Result { 86 | Ok(self.c.read_u8()?) 87 | } 88 | 89 | pub fn read_i8(&mut self) -> Result { 90 | Ok(self.c.read_i8()?) 91 | } 92 | 93 | pub fn read_bool(&mut self) -> Result { 94 | Ok(self.read_u8()? != 0) 95 | } 96 | 97 | pub fn read_u16(&mut self) -> Result { 98 | Ok(self.c.read_u16::()?) 99 | } 100 | 101 | pub fn read_i16(&mut self) -> Result { 102 | Ok(self.c.read_i16::()?) 103 | } 104 | 105 | pub fn read_u32(&mut self) -> Result { 106 | Ok(self.c.read_u32::()?) 107 | } 108 | 109 | pub fn read_i32(&mut self) -> Result { 110 | Ok(self.c.read_i32::()?) 111 | } 112 | 113 | pub fn read_f64(&mut self) -> Result { 114 | Ok(self.c.read_f64::()?) 115 | } 116 | 117 | pub fn read_uuid(&mut self) -> Result { 118 | Ok(UUID( 119 | self.c.read_u64::()?, 120 | self.c.read_u64::()?, 121 | )) 122 | } 123 | 124 | pub fn read_varint(&mut self) -> Result { 125 | const PART: u32 = 0x7F; 126 | let mut size = 0; 127 | let mut val = 0u32; 128 | loop { 129 | let b = self.c.read_u8()? as u32; 130 | val |= (b & PART) << (size * 7); 131 | size += 1; 132 | if size > 5 { 133 | return Err("VarInt too big")?; 134 | } 135 | if (b & 0x80) == 0 { 136 | break; 137 | } 138 | } 139 | Ok(val as i32) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/ccnatural.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::iter::FromIterator; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 5 | pub enum Naturality { 6 | Natural, 7 | Pond, 8 | Exploited, 9 | Planted, 10 | Built, 11 | Unknown, 12 | } 13 | 14 | pub fn get_naturality_color(n: &Naturality) -> u32 { 15 | match n { 16 | Naturality::Natural => 0xff448844, // dark green muted 17 | Naturality::Pond => 0xff886644, // dark blue-green muted 18 | Naturality::Exploited => 0xff6090c0, // mid brown muted 19 | Naturality::Planted => 0xff88ffff, // bright yellow 20 | Naturality::Built => 0xff88bbff, // bright orange 21 | Naturality::Unknown => 0x88ff00ff, // fuchsia semi-transparent 22 | } 23 | } 24 | 25 | // CCNATURAL_COLORS_BLOCKS_BIOME must be checked before CCNATURAL_COLORS_BLOCK_DEFAULT 26 | // the split is just to make this file shorter 27 | 28 | lazy_static! { 29 | pub static ref CCNATURAL_COLORS_BLOCK_DEFAULT: HashMap<&'static str, Naturality> = HashMap::from_iter([ 30 | ("acacia_door", Naturality::Built), 31 | ("acacia_fence", Naturality::Built), 32 | ("acacia_fence_gate", Naturality::Built), 33 | ("acacia_stairs", Naturality::Built), 34 | ("activator_rail", Naturality::Built), 35 | ("anvil", Naturality::Built), 36 | ("beacon", Naturality::Built), 37 | ("bed", Naturality::Built), 38 | ("bedrock", Naturality::Exploited), 39 | ("beetroots", Naturality::Planted), 40 | ("birch_door", Naturality::Built), 41 | ("birch_fence", Naturality::Built), 42 | ("birch_fence_gate", Naturality::Built), 43 | ("birch_stairs", Naturality::Built), 44 | ("black_glazed_terracotta", Naturality::Built), 45 | ("blue_glazed_terracotta", Naturality::Built), 46 | ("bone_block", Naturality::Built), 47 | ("bookshelf", Naturality::Built), 48 | ("brewing_stand", Naturality::Built), 49 | ("brick_block", Naturality::Built), 50 | ("brick_stairs", Naturality::Built), 51 | ("brown_glazed_terracotta", Naturality::Built), 52 | ("brown_mushroom", Naturality::Unknown), 53 | ("brown_mushroom_block", Naturality::Built), 54 | ("cactus", Naturality::Planted), 55 | ("cake", Naturality::Built), 56 | ("carpet", Naturality::Built), 57 | ("carrots", Naturality::Planted), 58 | ("cauldron", Naturality::Built), 59 | ("chest", Naturality::Built), 60 | ("chorus_flower", Naturality::Planted), 61 | ("chorus_plant", Naturality::Planted), 62 | ("clay", Naturality::Unknown), 63 | ("coal_block", Naturality::Built), 64 | ("coal_ore", Naturality::Exploited), 65 | ("cobblestone", Naturality::Built), 66 | ("cobblestone_wall", Naturality::Built), 67 | ("cocoa", Naturality::Planted), 68 | ("concrete", Naturality::Built), 69 | ("concrete_powder", Naturality::Built), 70 | ("crafting_table", Naturality::Built), 71 | ("dark_oak_door", Naturality::Built), 72 | ("dark_oak_fence", Naturality::Built), 73 | ("dark_oak_fence_gate", Naturality::Built), 74 | ("dark_oak_stairs", Naturality::Built), 75 | ("daylight_detector", Naturality::Built), 76 | ("daylight_detector_inverted", Naturality::Built), 77 | ("deadbush", Naturality::Unknown), 78 | ("detector_rail", Naturality::Built), 79 | ("diamond_block", Naturality::Built), 80 | ("diamond_ore", Naturality::Exploited), 81 | ("dirt", Naturality::Exploited), 82 | ("dispenser", Naturality::Built), 83 | ("double_plant", Naturality::Unknown), 84 | ("double_stone_slab", Naturality::Built), 85 | ("double_stone_slab2", Naturality::Built), 86 | ("double_wooden_slab", Naturality::Built), 87 | ("dropper", Naturality::Built), 88 | ("emerald_block", Naturality::Built), 89 | ("enchanting_table", Naturality::Built), 90 | ("end_bricks", Naturality::Built), 91 | ("end_rod", Naturality::Built), 92 | ("end_stone", Naturality::Built), 93 | ("farmland", Naturality::Planted), 94 | ("fence", Naturality::Built), 95 | ("fence_gate", Naturality::Built), 96 | ("fire", Naturality::Unknown), 97 | ("flower_pot", Naturality::Built), 98 | ("flowing_lava", Naturality::Unknown), 99 | ("flowing_water", Naturality::Unknown), 100 | ("furnace", Naturality::Built), 101 | ("glass", Naturality::Built), 102 | ("glass_pane", Naturality::Built), 103 | ("glowstone", Naturality::Built), 104 | ("gold_block", Naturality::Built), 105 | ("gold_ore", Naturality::Exploited), 106 | ("golden_rail", Naturality::Built), 107 | ("grass", Naturality::Natural), 108 | ("grass_path", Naturality::Built), 109 | ("gravel", Naturality::Exploited), 110 | ("gray_glazed_terracotta", Naturality::Built), 111 | ("green_glazed_terracotta", Naturality::Built), 112 | ("hardened_clay", Naturality::Built), 113 | ("hay_block", Naturality::Built), 114 | ("heavy_weighted_pressure_plate", Naturality::Built), 115 | ("hopper", Naturality::Built), 116 | ("ice", Naturality::Built), 117 | ("iron_bars", Naturality::Built), 118 | ("iron_block", Naturality::Built), 119 | ("iron_door", Naturality::Built), 120 | ("iron_ore", Naturality::Exploited), 121 | ("iron_trapdoor", Naturality::Built), 122 | ("jukebox", Naturality::Built), 123 | ("jungle_door", Naturality::Built), 124 | ("jungle_fence", Naturality::Built), 125 | ("jungle_fence_gate", Naturality::Built), 126 | ("jungle_stairs", Naturality::Built), 127 | ("ladder", Naturality::Built), 128 | ("lapis_block", Naturality::Built), 129 | ("lapis_ore", Naturality::Exploited), 130 | ("lava", Naturality::Unknown), 131 | ("leaves", Naturality::Natural), 132 | ("leaves2", Naturality::Planted), 133 | ("lever", Naturality::Built), 134 | ("light_blue_glazed_terracotta", Naturality::Built), 135 | ("light_weighted_pressure_plate", Naturality::Built), 136 | ("lime_glazed_terracotta", Naturality::Built), 137 | ("lit_pumpkin", Naturality::Built), 138 | ("lit_redstone_lamp", Naturality::Built), 139 | ("lit_redstone_ore", Naturality::Exploited), 140 | ("log", Naturality::Unknown), 141 | ("log2", Naturality::Unknown), 142 | ("magma", Naturality::Built), 143 | ("melon_block", Naturality::Planted), 144 | ("melon_stem", Naturality::Planted), 145 | ("mob_spawner", Naturality::Unknown), 146 | ("mossy_cobblestone", Naturality::Exploited), 147 | ("mycelium", Naturality::Built), 148 | ("nether_brick", Naturality::Built), 149 | ("nether_brick_fence", Naturality::Built), 150 | ("nether_brick_stairs", Naturality::Built), 151 | ("nether_wart", Naturality::Planted), 152 | ("nether_wart_block", Naturality::Built), 153 | ("netherrack", Naturality::Built), 154 | ("noteblock", Naturality::Built), 155 | ("oak_stairs", Naturality::Built), 156 | ("observer", Naturality::Built), 157 | ("obsidian", Naturality::Built), 158 | ("orange_glazed_terracotta", Naturality::Built), 159 | ("packed_ice", Naturality::Built), 160 | ("pink_glazed_terracotta", Naturality::Built), 161 | ("piston", Naturality::Built), 162 | ("piston_head", Naturality::Built), 163 | ("planks", Naturality::Built), 164 | ("potatoes", Naturality::Planted), 165 | ("powered_repeater", Naturality::Built), 166 | ("prismarine", Naturality::Unknown), 167 | ("pumpkin", Naturality::Planted), 168 | ("pumpkin_stem", Naturality::Planted), 169 | ("purple_glazed_terracotta", Naturality::Built), 170 | ("purpur_block", Naturality::Built), 171 | ("purpur_double_slab", Naturality::Built), 172 | ("purpur_pillar", Naturality::Built), 173 | ("purpur_slab", Naturality::Built), 174 | ("purpur_stairs", Naturality::Built), 175 | ("quartz_block", Naturality::Built), 176 | ("quartz_ore", Naturality::Built), 177 | ("quartz_stairs", Naturality::Built), 178 | ("rail", Naturality::Built), 179 | ("red_flower", Naturality::Unknown), 180 | ("red_glazed_terracotta", Naturality::Built), 181 | ("red_mushroom", Naturality::Built), 182 | ("red_mushroom_block", Naturality::Built), 183 | ("red_nether_brick", Naturality::Built), 184 | ("red_sandstone", Naturality::Built), 185 | ("red_sandstone_stairs", Naturality::Built), 186 | ("redstone_block", Naturality::Built), 187 | ("redstone_lamp", Naturality::Built), 188 | ("redstone_ore", Naturality::Exploited), 189 | ("redstone_torch", Naturality::Built), 190 | ("redstone_wire", Naturality::Built), 191 | ("reeds", Naturality::Planted), 192 | ("sand", Naturality::Unknown), 193 | ("sandstone", Naturality::Built), 194 | ("sandstone_stairs", Naturality::Built), 195 | ("sapling", Naturality::Built), 196 | ("sea_lantern", Naturality::Built), 197 | ("silver_glazed_terracotta", Naturality::Built), 198 | ("skull", Naturality::Built), 199 | ("slime", Naturality::Built), 200 | ("snow", Naturality::Built), 201 | ("snow_layer", Naturality::Built), 202 | ("soul_sand", Naturality::Built), 203 | ("spruce_door", Naturality::Built), 204 | ("spruce_fence", Naturality::Built), 205 | ("spruce_fence_gate", Naturality::Built), 206 | ("spruce_stairs", Naturality::Built), 207 | ("stained_glass", Naturality::Built), 208 | ("stained_glass_pane", Naturality::Built), 209 | ("stained_hardened_clay", Naturality::Built), 210 | ("standing_banner", Naturality::Built), 211 | ("standing_sign", Naturality::Built), 212 | ("sticky_piston", Naturality::Built), 213 | ("stone", Naturality::Exploited), 214 | ("stone_brick_stairs", Naturality::Built), 215 | ("stone_button", Naturality::Built), 216 | ("stone_pressure_plate", Naturality::Built), 217 | ("stone_slab", Naturality::Built), 218 | ("stone_slab2", Naturality::Built), 219 | ("stone_stairs", Naturality::Built), 220 | ("stonebrick", Naturality::Built), 221 | ("tallgrass", Naturality::Natural), 222 | ("tnt", Naturality::Built), 223 | ("torch", Naturality::Built), 224 | ("trapdoor", Naturality::Built), 225 | ("trapped_chest", Naturality::Built), 226 | ("tripwire", Naturality::Built), 227 | ("tripwire_hook", Naturality::Built), 228 | ("unlit_redstone_torch", Naturality::Built), 229 | ("unpowered_comparator", Naturality::Built), 230 | ("unpowered_repeater", Naturality::Built), 231 | ("vine", Naturality::Unknown), 232 | ("wall_banner", Naturality::Built), 233 | ("wall_sign", Naturality::Built), 234 | ("water", Naturality::Pond), 235 | ("waterlily", Naturality::Built), 236 | ("web", Naturality::Built), 237 | ("wheat", Naturality::Planted), 238 | ("white_glazed_terracotta", Naturality::Built), 239 | ("wooden_button", Naturality::Built), 240 | ("wooden_door", Naturality::Built), 241 | ("wooden_pressure_plate", Naturality::Built), 242 | ("wooden_slab", Naturality::Built), 243 | ("wool", Naturality::Built), 244 | ("yellow_flower", Naturality::Built), 245 | ("yellow_glazed_terracotta", Naturality::Built), 246 | ].iter().map(|(n,c)| (*n, *c))); 247 | 248 | pub static ref CCNATURAL_COLORS_BLOCK_BIOME: HashMap<(&'static str, u8), Naturality> = HashMap::from_iter([ 249 | (("brown_mushroom", 14), Naturality::Natural), 250 | (("brown_mushroom", 4), Naturality::Natural), 251 | (("brown_mushroom", 6), Naturality::Natural), 252 | (("brown_mushroom_block", 14), Naturality::Natural), 253 | (("brown_mushroom_block", 157), Naturality::Natural), 254 | (("brown_mushroom_block", 29), Naturality::Natural), 255 | (("brown_mushroom_block", 4), Naturality::Natural), 256 | (("cactus", 130), Naturality::Natural), 257 | (("cactus", 2), Naturality::Natural), 258 | (("clay", 0), Naturality::Natural), 259 | (("clay", 11), Naturality::Exploited), 260 | (("clay", 12), Naturality::Exploited), 261 | (("clay", 13), Naturality::Exploited), 262 | (("clay", 133), Naturality::Exploited), 263 | (("clay", 14), Naturality::Exploited), 264 | (("clay", 158), Naturality::Exploited), 265 | (("clay", 17), Naturality::Exploited), 266 | (("clay", 21), Naturality::Exploited), 267 | (("clay", 27), Naturality::Exploited), 268 | (("clay", 28), Naturality::Exploited), 269 | (("clay", 31), Naturality::Exploited), 270 | (("clay", 36), Naturality::Exploited), 271 | (("clay", 4), Naturality::Exploited), 272 | (("clay", 6), Naturality::Natural), 273 | (("clay", 7), Naturality::Natural), 274 | (("cocoa", 149), Naturality::Natural), 275 | (("cocoa", 21), Naturality::Natural), 276 | (("deadbush", 1), Naturality::Built), 277 | (("deadbush", 12), Naturality::Built), 278 | (("deadbush", 130), Naturality::Natural), 279 | (("deadbush", 166), Naturality::Natural), 280 | (("deadbush", 167), Naturality::Natural), 281 | (("deadbush", 2), Naturality::Natural), 282 | (("deadbush", 24), Naturality::Built), 283 | (("deadbush", 29), Naturality::Built), 284 | (("deadbush", 38), Naturality::Natural), 285 | (("deadbush", 39), Naturality::Natural), 286 | (("deadbush", 7), Naturality::Built), 287 | (("dirt", 0), Naturality::Natural), 288 | (("dirt", 11), Naturality::Natural), 289 | (("dirt", 12), Naturality::Unknown), 290 | (("dirt", 131), Naturality::Unknown), 291 | (("dirt", 134), Naturality::Unknown), 292 | (("dirt", 15), Naturality::Unknown), 293 | (("dirt", 155), Naturality::Unknown), 294 | (("dirt", 156), Naturality::Unknown), 295 | (("dirt", 162), Naturality::Unknown), 296 | (("dirt", 163), Naturality::Natural), 297 | (("dirt", 164), Naturality::Unknown), 298 | (("dirt", 165), Naturality::Unknown), 299 | (("dirt", 166), Naturality::Natural), 300 | (("dirt", 17), Naturality::Unknown), 301 | (("dirt", 19), Naturality::Unknown), 302 | (("dirt", 24), Naturality::Natural), 303 | (("dirt", 25), Naturality::Unknown), 304 | (("dirt", 26), Naturality::Unknown), 305 | (("dirt", 28), Naturality::Unknown), 306 | (("dirt", 3), Naturality::Natural), 307 | (("dirt", 30), Naturality::Unknown), 308 | (("dirt", 31), Naturality::Unknown), 309 | (("dirt", 32), Naturality::Unknown), 310 | (("dirt", 33), Naturality::Unknown), 311 | (("dirt", 36), Naturality::Unknown), 312 | (("dirt", 37), Naturality::Unknown), 313 | (("dirt", 38), Naturality::Natural), 314 | (("dirt", 4), Naturality::Natural), 315 | (("dirt", 6), Naturality::Natural), 316 | (("dirt", 7), Naturality::Natural), 317 | (("dirt", 8), Naturality::Built), 318 | (("flowing_lava", 8), Naturality::Natural), 319 | (("glowstone", 8), Naturality::Natural), 320 | (("grass", 0), Naturality::Built), 321 | (("grass", 11), Naturality::Unknown), 322 | (("grass", 12), Naturality::Unknown), 323 | (("grass", 130), Naturality::Built), 324 | (("grass", 131), Naturality::Unknown), 325 | (("grass", 134), Naturality::Unknown), 326 | (("grass", 14), Naturality::Built), 327 | (("grass", 140), Naturality::Unknown), 328 | (("grass", 15), Naturality::Unknown), 329 | (("grass", 162), Naturality::Unknown), 330 | (("grass", 19), Naturality::Unknown), 331 | (("grass", 2), Naturality::Built), 332 | (("grass", 24), Naturality::Built), 333 | (("grass", 25), Naturality::Unknown), 334 | (("grass", 26), Naturality::Unknown), 335 | (("grass", 28), Naturality::Unknown), 336 | (("grass", 30), Naturality::Unknown), 337 | (("grass", 32), Naturality::Unknown), 338 | (("grass", 33), Naturality::Unknown), 339 | (("grass", 36), Naturality::Unknown), 340 | (("grass", 37), Naturality::Unknown), 341 | (("grass", 39), Naturality::Planted), 342 | (("grass", 7), Naturality::Unknown), 343 | (("grass", 8), Naturality::Built), 344 | (("gravel", 24), Naturality::Natural), 345 | (("hardened_clay", 166), Naturality::Natural), 346 | (("hardened_clay", 167), Naturality::Natural), 347 | (("hardened_clay", 38), Naturality::Natural), 348 | (("hardened_clay", 39), Naturality::Natural), 349 | (("ice", 11), Naturality::Natural), 350 | (("ice", 13), Naturality::Natural), 351 | (("ice", 133), Naturality::Natural), 352 | (("ice", 140), Naturality::Natural), 353 | (("ice", 158), Naturality::Natural), 354 | (("ice", 20), Naturality::Unknown), 355 | (("ice", 3), Naturality::Unknown), 356 | (("ice", 34), Naturality::Unknown), 357 | (("lava", 8), Naturality::Natural), 358 | (("leaves", 0), Naturality::Built), 359 | (("leaves", 13), Naturality::Planted), 360 | (("leaves", 130), Naturality::Unknown), 361 | (("leaves", 131), Naturality::Unknown), 362 | (("leaves", 134), Naturality::Unknown), 363 | (("leaves", 14), Naturality::Planted), 364 | (("leaves", 15), Naturality::Unknown), 365 | (("leaves", 162), Naturality::Unknown), 366 | (("leaves", 163), Naturality::Planted), 367 | (("leaves", 19), Naturality::Unknown), 368 | (("leaves", 2), Naturality::Planted), 369 | (("leaves", 23), Naturality::Unknown), 370 | (("leaves", 24), Naturality::Built), 371 | (("leaves", 26), Naturality::Unknown), 372 | (("leaves", 30), Naturality::Unknown), 373 | (("leaves", 33), Naturality::Unknown), 374 | (("leaves", 36), Naturality::Unknown), 375 | (("leaves", 37), Naturality::Unknown), 376 | (("leaves", 39), Naturality::Planted), 377 | (("leaves", 8), Naturality::Built), 378 | (("leaves2", 0), Naturality::Built), 379 | (("leaves2", 12), Naturality::Natural), 380 | (("leaves2", 13), Naturality::Unknown), 381 | (("leaves2", 130), Naturality::Unknown), 382 | (("leaves2", 132), Naturality::Unknown), 383 | (("leaves2", 134), Naturality::Unknown), 384 | (("leaves2", 157), Naturality::Natural), 385 | (("leaves2", 163), Naturality::Natural), 386 | (("leaves2", 17), Naturality::Natural), 387 | (("leaves2", 20), Naturality::Unknown), 388 | (("leaves2", 22), Naturality::Unknown), 389 | (("leaves2", 24), Naturality::Built), 390 | (("leaves2", 26), Naturality::Unknown), 391 | (("leaves2", 27), Naturality::Natural), 392 | (("leaves2", 29), Naturality::Natural), 393 | (("leaves2", 3), Naturality::Unknown), 394 | (("leaves2", 30), Naturality::Unknown), 395 | (("leaves2", 35), Naturality::Natural), 396 | (("leaves2", 36), Naturality::Natural), 397 | (("leaves2", 37), Naturality::Unknown), 398 | (("leaves2", 5), Naturality::Unknown), 399 | (("leaves2", 7), Naturality::Natural), 400 | (("leaves2", 8), Naturality::Built), 401 | (("log", 0), Naturality::Built), 402 | (("log", 1), Naturality::Exploited), 403 | (("log", 129), Naturality::Exploited), 404 | (("log", 132), Naturality::Exploited), 405 | (("log", 149), Naturality::Exploited), 406 | (("log", 157), Naturality::Exploited), 407 | (("log", 18), Naturality::Exploited), 408 | (("log", 21), Naturality::Exploited), 409 | (("log", 22), Naturality::Exploited), 410 | (("log", 3), Naturality::Exploited), 411 | (("log", 35), Naturality::Planted), 412 | (("log", 4), Naturality::Exploited), 413 | (("log", 5), Naturality::Exploited), 414 | (("log", 6), Naturality::Exploited), 415 | (("log", 8), Naturality::Built), 416 | (("log2", 129), Naturality::Exploited), 417 | (("log2", 157), Naturality::Exploited), 418 | (("log2", 163), Naturality::Exploited), 419 | (("log2", 29), Naturality::Exploited), 420 | (("log2", 35), Naturality::Natural), 421 | (("log2", 8), Naturality::Built), 422 | (("magma", 8), Naturality::Natural), 423 | (("melon_block", 149), Naturality::Unknown), 424 | (("melon_block", 21), Naturality::Unknown), 425 | (("melon_block", 22), Naturality::Unknown), 426 | (("mossy_cobblestone", 149), Naturality::Unknown), 427 | (("mossy_cobblestone", 21), Naturality::Unknown), 428 | (("mossy_cobblestone", 22), Naturality::Unknown), 429 | (("mossy_cobblestone", 8), Naturality::Built), 430 | (("mycelium", 14), Naturality::Natural), 431 | (("netherrack", 8), Naturality::Natural), 432 | (("obsidian", 8), Naturality::Natural), 433 | (("packed_ice", 11), Naturality::Unknown), 434 | (("packed_ice", 140), Naturality::Natural), 435 | (("quartz_ore", 8), Naturality::Exploited), 436 | (("red_flower", 0), Naturality::Built), 437 | (("red_flower", 1), Naturality::Natural), 438 | (("red_flower", 129), Naturality::Natural), 439 | (("red_flower", 13), Naturality::Natural), 440 | (("red_flower", 130), Naturality::Built), 441 | (("red_flower", 132), Naturality::Natural), 442 | (("red_flower", 157), Naturality::Natural), 443 | (("red_flower", 166), Naturality::Built), 444 | (("red_flower", 167), Naturality::Built), 445 | (("red_flower", 18), Naturality::Natural), 446 | (("red_flower", 2), Naturality::Built), 447 | (("red_flower", 20), Naturality::Natural), 448 | (("red_flower", 24), Naturality::Built), 449 | (("red_flower", 29), Naturality::Natural), 450 | (("red_flower", 3), Naturality::Natural), 451 | (("red_flower", 38), Naturality::Built), 452 | (("red_flower", 4), Naturality::Natural), 453 | (("red_flower", 5), Naturality::Natural), 454 | (("red_flower", 7), Naturality::Built), 455 | (("red_mushroom", 14), Naturality::Natural), 456 | (("red_mushroom", 4), Naturality::Natural), 457 | (("red_mushroom", 6), Naturality::Natural), 458 | (("red_mushroom_block", 14), Naturality::Natural), 459 | (("red_mushroom_block", 157), Naturality::Natural), 460 | (("red_mushroom_block", 29), Naturality::Natural), 461 | (("red_sandstone", 166), Naturality::Exploited), 462 | (("red_sandstone", 167), Naturality::Exploited), 463 | (("red_sandstone", 2), Naturality::Exploited), 464 | (("red_sandstone", 35), Naturality::Unknown), 465 | (("red_sandstone", 38), Naturality::Exploited), 466 | (("red_sandstone", 39), Naturality::Exploited), 467 | (("reeds", 16), Naturality::Natural), 468 | (("reeds", 6), Naturality::Natural), 469 | (("reeds", 7), Naturality::Natural), 470 | (("sand", 0), Naturality::Natural), 471 | (("sand", 1), Naturality::Pond), 472 | (("sand", 11), Naturality::Exploited), 473 | (("sand", 13), Naturality::Exploited), 474 | (("sand", 130), Naturality::Natural), 475 | (("sand", 132), Naturality::Pond), 476 | (("sand", 16), Naturality::Natural), 477 | (("sand", 166), Naturality::Pond), 478 | (("sand", 167), Naturality::Pond), 479 | (("sand", 18), Naturality::Pond), 480 | (("sand", 2), Naturality::Natural), 481 | (("sand", 24), Naturality::Natural), 482 | (("sand", 3), Naturality::Pond), 483 | (("sand", 38), Naturality::Pond), 484 | (("sand", 39), Naturality::Pond), 485 | (("sand", 4), Naturality::Pond), 486 | (("sand", 5), Naturality::Pond), 487 | (("sand", 6), Naturality::Natural), 488 | (("sand", 7), Naturality::Natural), 489 | (("sand", 8), Naturality::Built), 490 | (("sandstone", 130), Naturality::Exploited), 491 | (("sandstone", 16), Naturality::Exploited), 492 | (("sandstone", 2), Naturality::Exploited), 493 | (("snow", 13), Naturality::Exploited), 494 | (("snow", 140), Naturality::Exploited), 495 | (("snow", 20), Naturality::Unknown), 496 | (("snow", 34), Naturality::Unknown), 497 | (("snow_layer", 11), Naturality::Natural), 498 | (("snow_layer", 12), Naturality::Unknown), 499 | (("snow_layer", 13), Naturality::Natural), 500 | (("snow_layer", 131), Naturality::Unknown), 501 | (("snow_layer", 133), Naturality::Natural), 502 | (("snow_layer", 140), Naturality::Natural), 503 | (("snow_layer", 158), Naturality::Natural), 504 | (("snow_layer", 162), Naturality::Unknown), 505 | (("snow_layer", 20), Naturality::Natural), 506 | (("snow_layer", 26), Naturality::Unknown), 507 | (("snow_layer", 3), Naturality::Natural), 508 | (("snow_layer", 30), Naturality::Unknown), 509 | (("snow_layer", 32), Naturality::Unknown), 510 | (("snow_layer", 34), Naturality::Natural), 511 | (("snow_layer", 5), Naturality::Unknown), 512 | (("soul_sand", 4), Naturality::Natural), 513 | (("soul_sand", 8), Naturality::Natural), 514 | (("stained_hardened_clay", 166), Naturality::Natural), 515 | (("stained_hardened_clay", 167), Naturality::Natural), 516 | (("stained_hardened_clay", 38), Naturality::Natural), 517 | (("stained_hardened_clay", 39), Naturality::Natural), 518 | (("stone", 13), Naturality::Natural), 519 | (("stone", 8), Naturality::Built), 520 | (("tallgrass", 0), Naturality::Built), 521 | (("tallgrass", 11), Naturality::Unknown), 522 | (("tallgrass", 12), Naturality::Unknown), 523 | (("tallgrass", 130), Naturality::Unknown), 524 | (("tallgrass", 131), Naturality::Unknown), 525 | (("tallgrass", 140), Naturality::Unknown), 526 | (("tallgrass", 16), Naturality::Unknown), 527 | (("tallgrass", 167), Naturality::Unknown), 528 | (("tallgrass", 2), Naturality::Unknown), 529 | (("tallgrass", 21), Naturality::Unknown), 530 | (("tallgrass", 22), Naturality::Unknown), 531 | (("tallgrass", 24), Naturality::Unknown), 532 | (("tallgrass", 30), Naturality::Unknown), 533 | (("tallgrass", 32), Naturality::Unknown), 534 | (("tallgrass", 33), Naturality::Unknown), 535 | (("tallgrass", 36), Naturality::Unknown), 536 | (("tallgrass", 39), Naturality::Unknown), 537 | (("tallgrass", 7), Naturality::Unknown), 538 | (("vine", 0), Naturality::Planted), 539 | (("vine", 129), Naturality::Planted), 540 | (("vine", 149), Naturality::Natural), 541 | (("vine", 2), Naturality::Planted), 542 | (("vine", 21), Naturality::Natural), 543 | (("vine", 22), Naturality::Natural), 544 | (("vine", 35), Naturality::Planted), 545 | (("vine", 39), Naturality::Planted), 546 | (("vine", 4), Naturality::Natural), 547 | (("vine", 6), Naturality::Natural), 548 | (("water", 12), Naturality::Unknown), 549 | (("water", 130), Naturality::Built), 550 | (("water", 131), Naturality::Unknown), 551 | (("water", 134), Naturality::Unknown), 552 | (("water", 15), Naturality::Unknown), 553 | (("water", 155), Naturality::Unknown), 554 | (("water", 156), Naturality::Unknown), 555 | (("water", 162), Naturality::Unknown), 556 | (("water", 164), Naturality::Unknown), 557 | (("water", 165), Naturality::Unknown), 558 | (("water", 17), Naturality::Unknown), 559 | (("water", 19), Naturality::Unknown), 560 | (("water", 23), Naturality::Unknown), 561 | (("water", 24), Naturality::Natural), 562 | (("water", 25), Naturality::Unknown), 563 | (("water", 26), Naturality::Unknown), 564 | (("water", 28), Naturality::Unknown), 565 | (("water", 30), Naturality::Unknown), 566 | (("water", 31), Naturality::Unknown), 567 | (("water", 32), Naturality::Unknown), 568 | (("water", 33), Naturality::Unknown), 569 | (("water", 36), Naturality::Unknown), 570 | (("water", 37), Naturality::Unknown), 571 | (("waterlily", 0), Naturality::Unknown), 572 | (("waterlily", 16), Naturality::Unknown), 573 | (("waterlily", 6), Naturality::Natural), 574 | (("waterlily", 7), Naturality::Unknown), 575 | (("web", 4), Naturality::Natural), 576 | (("yellow_flower", 1), Naturality::Natural), 577 | (("yellow_flower", 129), Naturality::Natural), 578 | (("yellow_flower", 13), Naturality::Natural), 579 | (("yellow_flower", 133), Naturality::Unknown), 580 | (("yellow_flower", 149), Naturality::Unknown), 581 | (("yellow_flower", 157), Naturality::Unknown), 582 | (("yellow_flower", 158), Naturality::Unknown), 583 | (("yellow_flower", 163), Naturality::Natural), 584 | (("yellow_flower", 18), Naturality::Natural), 585 | (("yellow_flower", 20), Naturality::Natural), 586 | (("yellow_flower", 29), Naturality::Unknown), 587 | (("yellow_flower", 3), Naturality::Unknown), 588 | (("yellow_flower", 34), Naturality::Unknown), 589 | (("yellow_flower", 35), Naturality::Natural), 590 | (("yellow_flower", 4), Naturality::Unknown), 591 | (("yellow_flower", 5), Naturality::Natural), 592 | ].iter().map(|((n,b),c)| ((*n, *b), *c))); 593 | } 594 | -------------------------------------------------------------------------------- /src/colorizer.rs: -------------------------------------------------------------------------------- 1 | use crate::biomes::BIOME_COLOR_TABLE; 2 | use crate::ccnatural::{ 3 | get_naturality_color, Naturality, CCNATURAL_COLORS_BLOCK_BIOME, CCNATURAL_COLORS_BLOCK_DEFAULT, 4 | }; 5 | use crate::tile::Tile; 6 | use std::convert::TryInto; 7 | use std::u16; 8 | 9 | pub fn colorize_biome(tile: &Tile, column_nr: usize) -> u32 { 10 | if tile.is_col_empty(column_nr) { 11 | return 0; 12 | } 13 | let b = tile.get_biome_id(column_nr); 14 | BIOME_COLOR_TABLE[b as usize] 15 | } 16 | 17 | pub fn colorize_naturality(tile: &Tile, column_nr: usize) -> u32 { 18 | if tile.is_col_empty(column_nr) { 19 | return 0; 20 | } 21 | let biome: u8 = tile.get_biome_id(column_nr).try_into().unwrap(); 22 | 23 | let mut final_naturality = None; 24 | 25 | let steps_block_getters: Vec u16> = vec![ 26 | Tile::get_blockstate, 27 | Tile::get_ocean_floor_blockstate, 28 | Tile::get_transparent_blockstate, 29 | Tile::get_foliage_blockstate, 30 | ]; 31 | for get_block_nr in steps_block_getters { 32 | let block_nr = get_block_nr(tile, column_nr); 33 | if block_nr != 0 { 34 | let block_name_full = &tile.names[block_nr as usize]; 35 | if block_name_full == "?UNKNOWN_BLOCK?" || block_name_full.ends_with(":air") { 36 | continue; 37 | } 38 | let block_name_prefixed = block_name_full.split("[").next().unwrap(); 39 | let block_name_stem = block_name_prefixed.rsplit(":").next().unwrap(); 40 | 41 | let block_naturality = CCNATURAL_COLORS_BLOCK_BIOME 42 | .get(&(block_name_stem, biome)) 43 | .or_else(|| CCNATURAL_COLORS_BLOCK_DEFAULT.get(&block_name_stem)) 44 | .unwrap_or(&Naturality::Unknown); 45 | if final_naturality.is_none() || final_naturality.unwrap() < *block_naturality { 46 | final_naturality = Some(*block_naturality); 47 | } 48 | } 49 | } 50 | 51 | final_naturality 52 | .map(|n| get_naturality_color(&n)) 53 | .unwrap_or(0) 54 | } 55 | 56 | const S_WATER: u32 = 0xff_ff_c5_a6; // #a6c5ff 57 | const S_LAND: u32 = 0xff_dc_e4_e7; // #e7e4dc 58 | 59 | pub fn colorize_simple(tile: &Tile, column_nr: usize) -> u32 { 60 | if tile.is_col_empty(column_nr) { 61 | return 0; 62 | } 63 | if is_water(tile, column_nr) { 64 | return S_WATER; 65 | } 66 | return S_LAND; 67 | } 68 | 69 | pub fn colorize_light(tile: &Tile, column_nr: usize) -> u32 { 70 | if tile.is_col_empty(column_nr) { 71 | return 0; 72 | } 73 | let bl = tile.get_light(column_nr) & 0xf; 74 | rgb(bl * 17, bl * 17, bl * 17) 75 | } 76 | 77 | pub fn colorize_height_bw(tile: &Tile, column_nr: usize) -> u32 { 78 | if tile.is_col_empty(column_nr) { 79 | return 0; 80 | } 81 | let mut h = tile.get_height(column_nr); 82 | if h == 0 { 83 | h = 255 // 0 = 256 = actually at height limit 84 | } 85 | rgb(h, h, h) 86 | } 87 | 88 | const BLACK: u32 = 0xff_00_00_00; 89 | const WHITE: u32 = 0xff_ff_ff_ff; 90 | const SKY_COLOR: u32 = 0xff_88_00_88; // #880088 pink 91 | const MTN_COLOR: u32 = 0xff_32_6e_9f; // #9f6e32 brown 92 | const MID_COLOR: u32 = 0xff_00_ff_ff; // #ffff00 yellow 93 | const COAST_COLOR: u32 = 0xff_00_b6_00; // #00b600 dark green 94 | const SEA_COLOR: u32 = 0xff_ff_d9_00; // #00d9ff light blue 95 | 96 | const HIGH_LEVEL: u8 = 240; 97 | const MTN_LEVEL: u8 = 150; 98 | const MID_LEVEL: u8 = 100; 99 | const SEA_LEVEL: u8 = 64; 100 | 101 | pub fn colorize_height(tile: &Tile, column_nr: usize) -> u32 { 102 | if tile.is_col_empty(column_nr) { 103 | return 0; // unpopulated 104 | } 105 | if is_water(tile, column_nr) { 106 | get_sea_color(tile.get_ocean_floor_height(column_nr)) 107 | } else { 108 | get_land_color(tile.get_height(column_nr)) 109 | } 110 | } 111 | 112 | pub fn get_sea_color(ocean_floor_height: u8) -> u32 { 113 | if ocean_floor_height < SEA_LEVEL { 114 | interpolate(BLACK, SEA_COLOR, 0, SEA_LEVEL, ocean_floor_height) 115 | } else { 116 | SEA_COLOR 117 | } 118 | } 119 | 120 | pub fn get_land_color(surface_height: u8) -> u32 { 121 | let h = match surface_height { 122 | 0 => 255, // wrapped around 123 | h => h, 124 | }; 125 | if h < SEA_LEVEL { 126 | interpolate(BLACK, COAST_COLOR, 0, SEA_LEVEL, h) 127 | } else if h < MID_LEVEL { 128 | interpolate(COAST_COLOR, MID_COLOR, SEA_LEVEL, MID_LEVEL, h) 129 | } else if h < MTN_LEVEL { 130 | interpolate(MID_COLOR, MTN_COLOR, MID_LEVEL, MTN_LEVEL, h) 131 | } else if h < HIGH_LEVEL { 132 | interpolate(MTN_COLOR, WHITE, MTN_LEVEL, HIGH_LEVEL, h) 133 | } else { 134 | interpolate(WHITE, SKY_COLOR, HIGH_LEVEL, 255, h) 135 | } 136 | } 137 | 138 | fn is_water(tile: &Tile, column_nr: usize) -> bool { 139 | let block_nr = tile.get_blockstate(column_nr); 140 | return block_nr 141 | == *tile 142 | .keys 143 | .get("minecraft:water[level=0]") 144 | .unwrap_or(&u16::MAX); 145 | } 146 | 147 | fn interpolate(col_start: u32, col_stop: u32, val_start: u8, val_stop: u8, val: u8) -> u32 { 148 | let r_st = col_start & 0xff; 149 | let g_st = col_start >> 8 & 0xff; 150 | let b_st = col_start >> 16 & 0xff; 151 | let r_sp = col_stop & 0xff; 152 | let g_sp = col_stop >> 8 & 0xff; 153 | let b_sp = col_stop >> 16 & 0xff; 154 | rgb( 155 | interpolate_color_component(r_st, r_sp, val_start, val_stop, val), 156 | interpolate_color_component(g_st, g_sp, val_start, val_stop, val), 157 | interpolate_color_component(b_st, b_sp, val_start, val_stop, val), 158 | ) 159 | } 160 | 161 | fn interpolate_color_component( 162 | col_start: u32, 163 | col_stop: u32, 164 | val_start: u8, 165 | val_stop: u8, 166 | val: u8, 167 | ) -> u8 { 168 | let diff_start = val - val_start; 169 | let diff_stop = val_stop - val; 170 | let val_diff = val_stop - val_start; 171 | ((col_start * diff_stop as u32 + col_stop * diff_start as u32) / val_diff as u32) as u8 172 | } 173 | 174 | fn rgb(r: u8, g: u8, b: u8) -> u32 { 175 | 0xff000000 | ((b as u32) << 16) | ((g as u32) << 8) | (r as u32) 176 | } 177 | 178 | #[derive(Debug)] 179 | pub enum Colorizer { 180 | Biome, 181 | Height, 182 | HeightBW, 183 | Light, 184 | Simple, 185 | Naturality, 186 | } 187 | 188 | impl Colorizer { 189 | pub fn get_column_color_fn(&self) -> Box u32> { 190 | Box::new(match *self { 191 | Colorizer::Biome => colorize_biome, 192 | Colorizer::Height => colorize_height, 193 | Colorizer::HeightBW => colorize_height_bw, 194 | Colorizer::Light => colorize_light, 195 | Colorizer::Naturality => colorize_naturality, 196 | Colorizer::Simple => colorize_simple, 197 | }) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate byteorder; 2 | #[macro_use] 3 | extern crate error_chain; 4 | #[macro_use] 5 | extern crate lazy_static; 6 | extern crate zip; 7 | 8 | use mc::blocks::BLOCK_STRINGS_ARR; 9 | use std::fs; 10 | use std::path::PathBuf; 11 | use std::time::{Duration, Instant, SystemTime}; 12 | 13 | pub mod biomes; 14 | pub mod buf_rw; 15 | pub mod ccnatural; 16 | pub mod colorizer; 17 | pub mod mc; 18 | pub mod replay; 19 | pub mod tile; 20 | 21 | pub const CHUNK_WIDTH: usize = 16; 22 | pub const CHUNK_HEIGHT: usize = 16; 23 | pub const CHUNK_COLUMNS: usize = CHUNK_WIDTH * CHUNK_HEIGHT; 24 | 25 | pub const TILE_WIDTH: usize = 256; 26 | pub const TILE_HEIGHT: usize = 256; 27 | pub const TILE_COLUMNS: usize = TILE_WIDTH * TILE_HEIGHT; 28 | pub const TILE_CHUNKS: usize = TILE_COLUMNS / CHUNK_COLUMNS; 29 | 30 | pub const REGION_WIDTH: usize = 512; 31 | pub const REGION_HEIGHT: usize = 512; 32 | pub const REGION_COLUMNS: usize = REGION_WIDTH * REGION_HEIGHT; 33 | pub const REGION_CHUNKS: usize = REGION_COLUMNS / CHUNK_COLUMNS; 34 | 35 | pub fn get_block_name_from_voxelmap(vm_a: u8, vm_b: u8) -> &'static str { 36 | // BLOCK_STRINGS_ARR is id << 4 | meta 37 | // voxelmap is meta << 12 | id 38 | BLOCK_STRINGS_ARR[(vm_b as usize) << 4 | (vm_a as usize) >> 4] 39 | } 40 | 41 | pub fn get_mtime_or_0(path: &PathBuf) -> u64 { 42 | fs::metadata(path) 43 | .map(|metadata| match metadata.modified() { 44 | Ok(time) => time 45 | .duration_since(SystemTime::UNIX_EPOCH) 46 | .map(|x| x.as_secs()) 47 | .unwrap_or(0), 48 | _ => 0, 49 | }) 50 | .unwrap_or(0) 51 | } 52 | 53 | pub fn parse_bounds(bounds_str: &str) -> Result, String> { 54 | let bounds = bounds_str 55 | .splitn(4, ",") 56 | .map(|s| match &s[0..1] { 57 | "c" => s[1..].parse::().map(|c| c * CHUNK_WIDTH as i32 + 1), 58 | "t" => s[1..].parse::().map(|c| c * TILE_WIDTH as i32 + 1), 59 | "r" => s[1..].parse::().map(|c| c * REGION_WIDTH as i32 + 1), 60 | _ => s.parse(), 61 | }) 62 | .collect::, _>>() 63 | .map_err(|e| e.to_string())?; 64 | 65 | if bounds.len() != 4 || bounds[0] > bounds[2] || bounds[1] > bounds[3] { 66 | Err("should be: w,n,e,s".to_string()) 67 | } else { 68 | Ok(bounds) 69 | } 70 | } 71 | 72 | pub const PROGRESS_INTERVAL: u64 = 3; 73 | 74 | pub struct ProgressTracker { 75 | pub done: usize, 76 | pub total: usize, 77 | pub start_time: Instant, 78 | pub next_msg_elapsed: u64, 79 | } 80 | 81 | impl ProgressTracker { 82 | pub fn new(total: usize) -> Self { 83 | Self { 84 | total: total, 85 | start_time: Instant::now(), 86 | done: 0, 87 | next_msg_elapsed: PROGRESS_INTERVAL, 88 | } 89 | } 90 | pub fn progress_by(&mut self, delta: usize) -> &mut Self { 91 | self.done += delta; 92 | self 93 | } 94 | pub fn progress_to(&mut self, delta: usize) -> &mut Self { 95 | self.done = delta; 96 | self 97 | } 98 | pub fn elapsed(&mut self) -> Duration { 99 | self.start_time.elapsed() 100 | } 101 | pub fn print_progress(&mut self) { 102 | print_progress( 103 | self.done, 104 | self.total, 105 | self.start_time, 106 | &mut self.next_msg_elapsed, 107 | ) 108 | } 109 | } 110 | 111 | // TODO put more weight on recent measurements 112 | pub fn print_progress(done: usize, total: usize, start_time: Instant, next_msg_elapsed: &mut u64) { 113 | if total <= 0 || done == 0 { 114 | return; 115 | } 116 | 117 | let elapsed = start_time.elapsed().as_secs(); 118 | if elapsed < *next_msg_elapsed { 119 | return; 120 | } 121 | 122 | if *next_msg_elapsed < elapsed { 123 | *next_msg_elapsed = elapsed; 124 | } 125 | *next_msg_elapsed += PROGRESS_INTERVAL; 126 | 127 | let work_left = total - done; 128 | let sec_left = elapsed as usize * work_left / done; 129 | let min = sec_left / 60; 130 | let sec = sec_left % 60; 131 | eprintln!("{}/{} processed, {}:{:02?} left", done, total, min, sec); 132 | } 133 | 134 | #[cfg(test)] 135 | mod tests { 136 | use super::*; 137 | 138 | #[test] 139 | fn parse_bounds_with_world_coords() { 140 | let bounds_str = "1,-22222,33333,-4"; 141 | let bounds = parse_bounds(bounds_str); 142 | assert_eq!(Ok(vec![1, -22222, 33333, -4]), bounds); 143 | } 144 | 145 | #[test] 146 | fn parse_bounds_with_tile_coords() { 147 | let bounds_str = "t-2,t-33,t4,t5"; 148 | let bounds = parse_bounds(bounds_str); 149 | let bounds_regions = bounds.map(|b| b.iter().map(|c| *c >> 8).collect::>()); 150 | assert_eq!(Ok(vec![-2, -33, 4, 5]), bounds_regions); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/mc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blocks; 2 | pub mod packet; 3 | -------------------------------------------------------------------------------- /src/mc/packet.rs: -------------------------------------------------------------------------------- 1 | use buf_rw::{BufErr, BufReader, UUID}; 2 | 3 | // TODO relace enum with structs implementing a de/encode trait 4 | 5 | #[derive(Debug)] 6 | pub struct ChunkData { 7 | pub x: i32, 8 | pub z: i32, 9 | pub is_new: bool, 10 | pub sections_mask: u16, 11 | pub chunk_data: Vec, 12 | pub tile_entities: Vec, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub enum McPacket { 17 | SpawnPlayer { 18 | eid: i32, 19 | uuid: UUID, 20 | x: f64, 21 | y: f64, 22 | z: f64, 23 | yaw: u8, 24 | pitch: u8, 25 | metadata: Vec, 26 | }, 27 | Chat { 28 | message: String, 29 | position: i8, 30 | }, 31 | ChunkDataHack(ChunkData), 32 | EntityRelativeMove { 33 | eid: i32, 34 | dx: f64, 35 | dy: f64, 36 | dz: f64, 37 | on_ground: bool, 38 | }, 39 | EntityLookAndRelativeMove { 40 | eid: i32, 41 | dx: f64, 42 | dy: f64, 43 | dz: f64, 44 | yaw: u8, 45 | pitch: u8, 46 | on_ground: bool, 47 | }, 48 | EntityTeleport { 49 | eid: i32, 50 | x: f64, 51 | y: f64, 52 | z: f64, 53 | yaw: u8, 54 | pitch: u8, 55 | on_ground: bool, 56 | }, 57 | Unimplemented, 58 | } 59 | 60 | impl McPacket { 61 | pub fn decode(data: &Vec) -> Result { 62 | let mut data = BufReader::new(data.clone()); // TODO operate on data ref directly, no clone 63 | match data.read_u8()? { 64 | 0x05 => { 65 | Ok(McPacket::SpawnPlayer { 66 | eid: data.read_varint()?, 67 | uuid: data.read_uuid()?, 68 | x: data.read_f64()?, 69 | y: data.read_f64()?, 70 | z: data.read_f64()?, 71 | yaw: data.read_u8()?, 72 | pitch: data.read_u8()?, 73 | metadata: data.read_remainder()?, // TODO custom format 74 | }) 75 | } 76 | 77 | 0x0f => { 78 | let message_bytes = data.read_varint_prefixed_bytes()?; 79 | let message = ::std::str::from_utf8(&message_bytes)?.to_owned(); 80 | let position = data.read_i8()?; 81 | 82 | Ok(McPacket::Chat { message, position }) 83 | } 84 | 85 | 0x20 => { 86 | Ok(McPacket::ChunkDataHack(ChunkData { 87 | x: data.read_i32()?, 88 | z: data.read_i32()?, 89 | is_new: data.read_bool()?, 90 | sections_mask: data.read_u16()?, 91 | chunk_data: data.read_varint_prefixed_bytes()?, 92 | tile_entities: data.read_remainder()?, // TODO read this many NBTs 93 | })) 94 | } 95 | 96 | 0x26 => Ok(McPacket::EntityRelativeMove { 97 | eid: data.read_varint()?, 98 | dx: (data.read_i16()? as f64) / 4096_f64, 99 | dy: (data.read_i16()? as f64) / 4096_f64, 100 | dz: (data.read_i16()? as f64) / 4096_f64, 101 | on_ground: data.read_bool()?, 102 | }), 103 | 104 | 0x27 => Ok(McPacket::EntityLookAndRelativeMove { 105 | eid: data.read_varint()?, 106 | dx: (data.read_i16()? as f64) / 4096_f64, 107 | dy: (data.read_i16()? as f64) / 4096_f64, 108 | dz: (data.read_i16()? as f64) / 4096_f64, 109 | yaw: data.read_u8()?, 110 | pitch: data.read_u8()?, 111 | on_ground: data.read_bool()?, 112 | }), 113 | 114 | 0x4C => Ok(McPacket::EntityTeleport { 115 | eid: data.read_varint()?, 116 | x: data.read_f64()?, 117 | y: data.read_f64()?, 118 | z: data.read_f64()?, 119 | yaw: data.read_u8()?, 120 | pitch: data.read_u8()?, 121 | on_ground: data.read_bool()?, 122 | }), 123 | 124 | // TODO other packets 125 | _ => Ok(McPacket::Unimplemented), 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/replay.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | extern crate serde_json; 3 | extern crate zip; 4 | 5 | use super::buf_rw::{BufErr, BufReader}; 6 | use super::mc::packet::McPacket; 7 | use std::fs::File; 8 | use std::io::Read; 9 | use std::path::Path; 10 | use zip::ZipArchive; 11 | 12 | error_chain!{ 13 | types { 14 | ReplayErr, ReplayErrorKind, ReplayResult; 15 | } 16 | foreign_links { 17 | Buf(BufErr); 18 | Io(::std::io::Error); 19 | Zip(::zip::result::ZipError); 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | pub struct ReplayPacket { 25 | pub date: usize, 26 | pub size: usize, 27 | pub id: u8, 28 | pub data: Vec, 29 | decoded: Option, 30 | } 31 | 32 | impl ReplayPacket { 33 | pub fn get_packet(&self) -> &Option { 34 | &self.decoded 35 | } 36 | pub fn parse_packet(&mut self) -> Result<(), BufErr> { 37 | if let None = self.decoded { 38 | self.decoded = Some(McPacket::decode(&self.data)?); 39 | } 40 | Ok(()) 41 | } 42 | } 43 | 44 | #[derive(Debug)] 45 | pub struct ReplayInfo { 46 | pub date: usize, // milliseconds since UNIX epoch 47 | pub duration: usize, // duration of the recording in milliseconds 48 | pub mc_version: String, // for example "1.12.2" 49 | pub players: Vec, // UUIDs of all seen players 50 | pub server_name: String, 51 | } 52 | 53 | pub struct Replay { 54 | pub info: ReplayInfo, 55 | data: BufReader, // TODO lazy read 56 | } 57 | 58 | impl Iterator for Replay { 59 | type Item = ReplayPacket; 60 | 61 | fn next(&mut self) -> Option { 62 | if self.data.position() as usize + 9 > self.data.len() { 63 | return None; 64 | } 65 | 66 | let time_offset = self.data.read_u32().expect("malformed tmcpr") as usize; 67 | let size = self.data.read_u32().expect("malformed tmcpr") as usize; 68 | 69 | let packet_data = self.data.read_bytes(size).expect("malformed tmcpr"); 70 | 71 | let id = packet_data[0]; 72 | let date = self.info.date + time_offset; 73 | 74 | Some(ReplayPacket { 75 | date: date, 76 | size: size, 77 | id: id, 78 | data: packet_data, 79 | decoded: None, 80 | }) 81 | } 82 | } 83 | 84 | pub fn read_info

(path: &P) -> Result 85 | where 86 | P: AsRef, 87 | { 88 | let zip_file = File::open(&path)?; 89 | let mut zip_archive = ZipArchive::new(zip_file)?; 90 | read_info_from_zip(&mut zip_archive) 91 | } 92 | 93 | pub fn read_replay

(path: &P) -> Result 94 | where 95 | P: AsRef, 96 | { 97 | let zip_file = File::open(&path)?; 98 | let mut zip_archive = ZipArchive::new(zip_file)?; 99 | 100 | let info = read_info_from_zip(&mut zip_archive)?; 101 | 102 | let data = { 103 | let mut data_file = zip_archive 104 | .by_name("recording.tmcpr") 105 | .chain_err(|| "No recording in mcpr")?; 106 | let mut data = vec![0; data_file.size() as usize]; 107 | data_file.read_exact(&mut *data)?; 108 | data 109 | }; 110 | 111 | Ok(Replay { 112 | info: info, 113 | data: BufReader::new(data), 114 | }) 115 | } 116 | 117 | fn read_info_from_zip(zip_archive: &mut ZipArchive) -> Result { 118 | let info_file = zip_archive 119 | .by_name("metaData.json") 120 | .chain_err(|| "No metadata in mcpr")?; 121 | let json: serde_json::Value = 122 | serde_json::from_reader(info_file).chain_err(|| "Malformed JSON in replay info")?; 123 | let o: &serde_json::Map = 124 | json.as_object().ok_or("No object in replay info")?; 125 | 126 | Ok(ReplayInfo { 127 | date: o 128 | .get("date") 129 | .ok_or("No date in replay info")? 130 | .as_i64() 131 | .ok_or("Malformed date in replay info")? as usize, 132 | duration: o 133 | .get("duration") 134 | .ok_or("No duration in replay info")? 135 | .as_i64() 136 | .ok_or("Malformed duration in replay info")? as usize, 137 | mc_version: String::from( 138 | o.get("mcversion") 139 | .ok_or("No mcversion in replay info")? 140 | .as_str() 141 | .ok_or("Malformed mcversion in replay info")?, 142 | ), 143 | server_name: String::from( 144 | o.get("serverName") 145 | .ok_or("No serverName in replay info")? 146 | .as_str() 147 | .ok_or("Malformed serverName in replay info")?, 148 | ), 149 | players: o 150 | .get("players") 151 | .ok_or("No players in replay info")? 152 | .as_array() 153 | .ok_or("Malformed players in replay info")? 154 | .iter() 155 | .map(|v| { 156 | v.as_str() 157 | .map(String::from) 158 | .ok_or("Malformed serverName in replay info") 159 | }) 160 | .collect::, &str>>()?, 161 | }) 162 | } 163 | -------------------------------------------------------------------------------- /src/tile.rs: -------------------------------------------------------------------------------- 1 | extern crate zip; 2 | 3 | use std::collections::{HashMap, LinkedList}; 4 | use std::fmt; 5 | use std::fs; 6 | use std::num::ParseIntError; 7 | use std::path::PathBuf; 8 | 9 | pub const COLUMN_BYTES_OLD: usize = 17; 10 | pub const COLUMN_BYTES_MODERN: usize = 18; 11 | 12 | pub const TILE_WIDTH: usize = 256; 13 | pub const TILE_HEIGHT: usize = 256; 14 | pub const TILE_COLUMNS: usize = TILE_WIDTH * TILE_HEIGHT; 15 | 16 | pub const CHUNK_WIDTH: usize = 16; 17 | pub const CHUNK_HEIGHT: usize = 16; 18 | pub const CHUNK_COLUMNS: usize = CHUNK_WIDTH * CHUNK_HEIGHT; 19 | 20 | pub const TILE_CHUNKS: usize = TILE_COLUMNS / CHUNK_COLUMNS; 21 | 22 | const HEIGHTPOS: usize = 0; 23 | const BLOCKSTATEPOS: usize = 1; 24 | const LIGHTPOS: usize = 3; 25 | const OCEANFLOORHEIGHTPOS: usize = 4; 26 | const OCEANFLOORBLOCKSTATEPOS: usize = 5; 27 | const OCEANFLOORLIGHTPOS: usize = 7; 28 | const TRANSPARENTHEIGHTPOS: usize = 8; 29 | const TRANSPARENTBLOCKSTATEPOS: usize = 9; 30 | const TRANSPARENTLIGHTPOS: usize = 11; 31 | const FOLIAGEHEIGHTPOS: usize = 12; 32 | const FOLIAGEBLOCKSTATEPOS: usize = 13; 33 | const FOLIAGELIGHTPOS: usize = 15; 34 | const BIOMEIDPOS: usize = 16; 35 | 36 | pub type TilePos = (i32, i32); 37 | pub type KeysMap = HashMap; 38 | pub type NamesVec = Vec; 39 | 40 | pub struct Tile { 41 | // TODO make private 42 | /// in v2 format (layer-then-coords) 43 | pub data: Vec, 44 | pub keys: KeysMap, 45 | pub names: NamesVec, 46 | pub pos: Option, 47 | pub source: Option, 48 | } 49 | 50 | impl Tile { 51 | pub fn is_chunk_empty(&self, chunk_nr: usize) -> bool { 52 | let column_nr = first_column_nr_of_chunk_nr(chunk_nr); 53 | self.is_col_empty(column_nr) 54 | } 55 | pub fn is_col_empty(&self, column_nr: usize) -> bool { 56 | self.get_height(column_nr) == 0 57 | && self.get_biome_id(column_nr) == 0 58 | && self.get_blockstate(column_nr) == 0 59 | } 60 | 61 | pub fn get_height(&self, column_nr: usize) -> u8 { 62 | self.get_u8(column_nr, HEIGHTPOS) 63 | } 64 | pub fn get_blockstate(&self, column_nr: usize) -> u16 { 65 | self.get_u16(column_nr, BLOCKSTATEPOS) 66 | } 67 | pub fn get_light(&self, column_nr: usize) -> u8 { 68 | self.get_u8(column_nr, LIGHTPOS) 69 | } 70 | pub fn get_ocean_floor_height(&self, column_nr: usize) -> u8 { 71 | self.get_u8(column_nr, OCEANFLOORHEIGHTPOS) 72 | } 73 | pub fn get_ocean_floor_blockstate(&self, column_nr: usize) -> u16 { 74 | self.get_u16(column_nr, OCEANFLOORBLOCKSTATEPOS) 75 | } 76 | pub fn get_ocean_floor_light(&self, column_nr: usize) -> u8 { 77 | self.get_u8(column_nr, OCEANFLOORLIGHTPOS) 78 | } 79 | pub fn get_transparent_height(&self, column_nr: usize) -> u8 { 80 | self.get_u8(column_nr, TRANSPARENTHEIGHTPOS) 81 | } 82 | pub fn get_transparent_blockstate(&self, column_nr: usize) -> u16 { 83 | self.get_u16(column_nr, TRANSPARENTBLOCKSTATEPOS) 84 | } 85 | pub fn get_transparent_light(&self, column_nr: usize) -> u8 { 86 | self.get_u8(column_nr, TRANSPARENTLIGHTPOS) 87 | } 88 | pub fn get_foliage_height(&self, column_nr: usize) -> u8 { 89 | self.get_u8(column_nr, FOLIAGEHEIGHTPOS) 90 | } 91 | pub fn get_foliage_blockstate(&self, column_nr: usize) -> u16 { 92 | self.get_u16(column_nr, FOLIAGEBLOCKSTATEPOS) 93 | } 94 | pub fn get_foliage_light(&self, column_nr: usize) -> u8 { 95 | self.get_u8(column_nr, FOLIAGELIGHTPOS) 96 | } 97 | pub fn get_biome_id(&self, column_nr: usize) -> u16 { 98 | self.get_u16(column_nr, BIOMEIDPOS) 99 | } 100 | 101 | pub fn set_height(&mut self, column_nr: usize, value: u8) { 102 | self.set_u8(column_nr, HEIGHTPOS, value); 103 | } 104 | pub fn set_blockstate(&mut self, column_nr: usize, id: u16) { 105 | self.set_u16(column_nr, BLOCKSTATEPOS, id); 106 | } 107 | pub fn set_light(&mut self, column_nr: usize, value: u8) { 108 | self.set_u8(column_nr, LIGHTPOS, value); 109 | } 110 | pub fn set_ocean_floor_height(&mut self, column_nr: usize, value: u8) { 111 | self.set_u8(column_nr, OCEANFLOORHEIGHTPOS, value); 112 | } 113 | pub fn set_ocean_floor_blockstate(&mut self, column_nr: usize, id: u16) { 114 | self.set_u16(column_nr, OCEANFLOORBLOCKSTATEPOS, id); 115 | } 116 | pub fn set_ocean_floor_light(&mut self, column_nr: usize, value: u8) { 117 | self.set_u8(column_nr, OCEANFLOORLIGHTPOS, value); 118 | } 119 | pub fn set_transparent_height(&mut self, column_nr: usize, value: u8) { 120 | self.set_u8(column_nr, TRANSPARENTHEIGHTPOS, value); 121 | } 122 | pub fn set_transparent_blockstate(&mut self, column_nr: usize, id: u16) { 123 | self.set_u16(column_nr, TRANSPARENTBLOCKSTATEPOS, id); 124 | } 125 | pub fn set_transparent_light(&mut self, column_nr: usize, value: u8) { 126 | self.set_u8(column_nr, TRANSPARENTLIGHTPOS, value); 127 | } 128 | pub fn set_foliage_height(&mut self, column_nr: usize, value: u8) { 129 | self.set_u8(column_nr, FOLIAGEHEIGHTPOS, value); 130 | } 131 | pub fn set_foliage_blockstate(&mut self, column_nr: usize, id: u16) { 132 | self.set_u16(column_nr, FOLIAGEBLOCKSTATEPOS, id); 133 | } 134 | pub fn set_foliage_light(&mut self, column_nr: usize, value: u8) { 135 | self.set_u8(column_nr, FOLIAGELIGHTPOS, value); 136 | } 137 | pub fn set_biome_id(&mut self, column_nr: usize, value: u16) { 138 | self.set_u16(column_nr, BIOMEIDPOS, value); 139 | } 140 | 141 | fn get_u8(&self, column_nr: usize, layer_nr: usize) -> u8 { 142 | let index = column_nr + TILE_COLUMNS * layer_nr; 143 | self.data[index] 144 | } 145 | fn get_u16(&self, column_nr: usize, layer_nr: usize) -> u16 { 146 | let index = column_nr + TILE_COLUMNS * layer_nr; 147 | (self.data[index] as u16) << 8 | (self.data[index + TILE_COLUMNS] as u16) 148 | } 149 | 150 | fn set_u8(&mut self, column_nr: usize, layer_offset: usize, value: u8) { 151 | let index = column_nr + TILE_COLUMNS * layer_offset; 152 | self.data[index] = value; 153 | } 154 | fn set_u16(&mut self, column_nr: usize, layer_offset: usize, value: u16) { 155 | let index = column_nr + TILE_COLUMNS * layer_offset; 156 | self.data[index] = (value >> 8) as u8; 157 | self.data[index + TILE_COLUMNS] = value as u8; 158 | } 159 | } 160 | 161 | impl fmt::Debug for Tile { 162 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 163 | write!( 164 | fmt, 165 | "Tile with {} keys{}{}", 166 | self.keys.len(), 167 | match &self.pos { 168 | Some(pos) => format!(" at {:?}", pos), 169 | None => "".to_owned(), 170 | }, 171 | match &self.source { 172 | Some(source) => format!(" from {:?}", source), 173 | None => "".to_owned(), 174 | }, 175 | ) 176 | } 177 | } 178 | 179 | pub fn column_nr_of_pos(x: usize, z: usize) -> usize { 180 | x + z * TILE_WIDTH 181 | } 182 | 183 | pub fn first_column_nr_of_chunk_nr(chunk_nr: usize) -> usize { 184 | (chunk_nr * CHUNK_WIDTH) % TILE_WIDTH 185 | + (chunk_nr * CHUNK_WIDTH / TILE_WIDTH) * TILE_WIDTH * CHUNK_HEIGHT 186 | } 187 | 188 | pub fn read_tile(tile_path: &PathBuf) -> Result, String> { 189 | use std::io::{BufRead, BufReader, Read}; 190 | 191 | let zip_file = fs::File::open(&tile_path).map_err(|e| e.to_string())?; 192 | let mut zip_archive = zip::ZipArchive::new(zip_file).map_err(|e| e.to_string())?; 193 | 194 | let mut max_key = 0; 195 | let keys = zip_archive 196 | .by_name("key") 197 | .ok() 198 | .map(|key_file| { 199 | let mut keys = Box::new(HashMap::new()); 200 | for line in BufReader::new(key_file).lines() { 201 | let line = line.unwrap(); 202 | if line.is_empty() { 203 | continue; 204 | } 205 | let mut split = line.split(" "); 206 | let block_id = split 207 | .next() 208 | .expect("getting block num from key line split") 209 | .parse::() 210 | .expect("converting block num to int"); 211 | let block_name = split 212 | .next() 213 | .expect("getting block name from key line split") 214 | .to_string(); 215 | if max_key < block_id { 216 | max_key = block_id; 217 | } 218 | keys.insert(block_name, block_id); 219 | } 220 | *keys 221 | }) 222 | .expect("XXX support old keyless format"); 223 | 224 | let unknown_name = "?".to_string(); 225 | let mut names: NamesVec = vec![unknown_name; 1 + max_key as usize]; 226 | for (name, nr) in keys.iter() { 227 | names[*nr as usize] = name.clone(); 228 | } 229 | 230 | let mut data = vec![0; TILE_COLUMNS * COLUMN_BYTES_MODERN]; 231 | { 232 | let mut data_file = zip_archive 233 | .by_name("data") 234 | .map_err(|_e| "No data file in tile zip")?; 235 | data_file 236 | .read_exact(&mut *data) 237 | .map_err(|e| e.to_string())?; 238 | } 239 | 240 | let tile = Box::new(Tile { 241 | source: Some(tile_path.clone()), 242 | pos: get_xz_from_tile_path(tile_path).ok(), 243 | data: data, 244 | keys: keys, 245 | names: names, 246 | }); 247 | 248 | Ok(tile) 249 | } 250 | 251 | pub fn write_tile(tile_path: &PathBuf, tile: &Tile) -> Result<(), String> { 252 | use std::io::Write; 253 | 254 | let zip_file = fs::File::create(&tile_path).map_err(|e| e.to_string())?; 255 | let mut zip_archive = zip::ZipWriter::new(zip_file); 256 | 257 | let options = 258 | zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); 259 | 260 | // we can only write version 2 261 | 262 | zip_archive 263 | .start_file("control", options) 264 | .map_err(|e| e.to_string())?; 265 | zip_archive 266 | .write_all("version:2\r\n".as_bytes()) 267 | .map_err(|e| e.to_string())?; 268 | 269 | zip_archive 270 | .start_file("data", options) 271 | .map_err(|e| e.to_string())?; 272 | zip_archive 273 | .write_all(&tile.data) 274 | .map_err(|e| e.to_string())?; 275 | 276 | zip_archive 277 | .start_file("key", options) 278 | .map_err(|e| e.to_string())?; 279 | 280 | for (name, nr) in &tile.keys { 281 | zip_archive 282 | .write_fmt(format_args!("{} {}\r\n", nr, name)) 283 | .map_err(|e| e.to_string())?; 284 | } 285 | 286 | // Optionally finish the zip. (this is also done on drop) 287 | zip_archive.finish().map_err(|e| e.to_string())?; 288 | 289 | Ok(()) 290 | } 291 | 292 | pub fn get_xz_from_tile_path(tile_path: &PathBuf) -> Result { 293 | let fname = tile_path.file_name().unwrap().to_str().unwrap(); 294 | if fname.len() <= 4 { 295 | return Err("file name too short".to_owned()); 296 | } 297 | let (coords_part, _) = fname.split_at(fname.len() - 4); 298 | let mut it = coords_part.splitn(3, ','); 299 | let x = it 300 | .next() 301 | .ok_or("no x coord in filename".to_owned())? 302 | .parse() 303 | .map_err(|e: ParseIntError| e.to_string())?; 304 | let z = it 305 | .next() 306 | .ok_or("no z coord in filename".to_owned())? 307 | .parse() 308 | .map_err(|e: ParseIntError| e.to_string())?; 309 | Ok((x, z)) 310 | } 311 | 312 | pub fn get_contrib_from_tile_path(tile_path: &PathBuf) -> Result { 313 | let fname = tile_path.file_name().unwrap().to_str().unwrap(); 314 | if fname.len() <= 4 { 315 | return Err("no contrib in filename".to_owned()); 316 | } 317 | let (coords_part, _) = fname.split_at(fname.len() - 4); 318 | Ok(coords_part 319 | .splitn(3, ',') 320 | .skip(2) 321 | .next() 322 | .ok_or("No contrib in tile name")? 323 | .to_string()) 324 | } 325 | 326 | pub fn get_tile_paths_in_dirs( 327 | dirs: &Vec, 328 | verbose: bool, 329 | ) -> Result, String> { 330 | let mut tile_paths = LinkedList::new(); 331 | for dir in dirs { 332 | for zip_dir_entry in fs::read_dir(dir.as_str()).map_err(|e| e.to_string())? { 333 | let tile_path = zip_dir_entry.map_err(|e| e.to_string())?.path(); 334 | match get_xz_from_tile_path(&tile_path) { 335 | Ok(_pos) => { 336 | if tile_path.to_string_lossy().ends_with(".zip") { 337 | tile_paths.push_back(tile_path) 338 | } else { 339 | eprintln!("Ignoring non-tile file {:?}", &tile_path); 340 | } 341 | } 342 | Err(e) => { 343 | if tile_path.to_string_lossy().ends_with("_chunk-times.gz") { 344 | // ignore chunk timestamp info file 345 | } else { 346 | if verbose { 347 | eprintln!("Ignoring non-tile file {:?} {:?}", &tile_path, e); 348 | } 349 | } 350 | } 351 | } 352 | } 353 | } 354 | Ok(tile_paths) 355 | } 356 | 357 | pub fn is_tile_pos_in_bounds((tile_x, tile_z): (i32, i32), bounds: &Vec) -> bool { 358 | let tw = TILE_WIDTH as i32; 359 | let th = TILE_HEIGHT as i32; 360 | let x = tile_x * tw; 361 | let z = tile_z * th; 362 | let (w, n, e, s) = (bounds[0], bounds[1], bounds[2], bounds[3]); 363 | 364 | x + tw > w && x < e && z + th > n && z < s 365 | } 366 | -------------------------------------------------------------------------------- /voxelmap-cache-format.md: -------------------------------------------------------------------------------- 1 | one .zip per region with a file named `data` 2 | 3 | 17 byte per column: 4 | 1. (4 byte) layer 1: highest partially light blocking block including lava 5 | 2. (4 byte) layer 2: seafloor 6 | 3. (4 byte) layer 3: highest rain blocking block 7 | 4. (4 byte) layer 4: one block above layer 1 ("vegetation") 8 | 5. (1 byte) biome ID 9 | 10 | 4 byte per layer: 11 | 1. (1 byte) height 12 | 2. (2 byte) blockstate ID 13 | 3. (1 byte) blockLight + skyLight*16 14 | 15 | https://mods.curse.com/mc-mods/minecraft/225179-voxelmap?page=5#c36 16 | 17 | no that's cool I'll detail it. Was interested to see if you'd get any of it and you did so my curiosity is satisfied :) 18 | 19 | so with zero based counting, the last byte (16) is the biomeID. 20 | The first 16 bytes are in 4 groups of 4, each representing a block from a different "layer" (will explain the layers in a moment) 21 | 22 | Within those groups the first is the height, from 0-255. 23 | In Java a byte is -128 to 127 so before using it I bitwise-and it with 255 to get the 0-255 value as an int or short or whatever. Same for the rest of the values 24 | 25 | the second and third together are the blockstateID, which minecraft gets with 26 | getIdFromBlock(state.getBlock()) + (state.getBlock().getMetaFromState(state) << 12) 27 | Max value for that is 65536, a Short, or two bytes. 28 | I am storing them big endian because my human brain likes that better and I'm handling it explicitly as such and not relying on the platform to figure out big vs little. 29 | 30 | the fourth is the light level of the block (blockLight + skyLight*16) 31 | which can be used as an entry into the EntityRenderer.lightmapTexture.getTextureData() array, 32 | which changes as the sun goes up or down and torches flicker etc. 33 | 34 | So the layers. 35 | 1. highest partially light blocking block (plus lava. lava blocked light up through 1.5 or 1.6 or something). 36 | 2. seafloor (when the first layer is water). 37 | 3. highest rain blocking block (catches glass blocks and fences up in the air etc). 38 | 4. mostly vegetation: one block above the first layer, stuff like flowers, torches, rails etc that don't block anything, but also allows for stuff like fences (on the ground anyway) to show under glass. 39 | 40 | The last three layers might or might not exist at a given coordinate (they'll be zerod out if not, leads to nice zip compression). 41 | 42 | If during a conversion there's nothing to put into the last three layers, it won't hurt anything, they just won't show on the map. 43 | 44 | So, how does Journeymap store things? I know MapWriter reads (and stores) anvil .mca files. 45 | VoxelMap now has the capability of outputting image files for offline viewing (see the edited description of the mod). 46 | Converting the stored data to an image involves a lot of Minecraft code but I'm sure an offline converter could be done if you really wanted to. 47 | Journeymap cache file to voxelmap cache file would definitely be pretty nice, for people who have explored a lot in that mod! vice versa would be nice too if people want to convert in the opposite direction 48 | 49 | -------------------------------------------------------------------------------- /xaero-format.md: -------------------------------------------------------------------------------- 1 | # known 2 | - zip contains `region.xaero` which is uncompressed binary 3 | - per file: 512x512 blocks or one MCRegion 4 | - any 2d-arrays are indexed `[x][z]`, so serialized as z-then-x, unlike MC's usual x-then-z order 5 | - region contains 8x8 `ChunksChunk`s 6 | - chunk contains 4x4 tiles 7 | - tile contains 16x16 `pixels` (block columns) 8 | - pixel contains overlays, blockState, biome, height, light, heightShade, colorType 9 | - overlay contains opacity, overlayBlockState, customColor 10 | 11 | ## region: 12 | - prelude: `00ff` 13 | - version: `00000001` 14 | - loop over present chunks: 15 | - byte chunkCoords (`x << 4 | z`) 16 | - loop over tiles: 17 | - absent: int -1 18 | - present: dump pixels 19 | 20 | ## pixel: 21 | - int pixelParams 22 | - not grass? => int blockState 23 | - overlays? => 24 | - byte numOverlays 25 | - dump overlays 26 | - colorType == 3? => int customColor 27 | - hasBiome? => byte biome 28 | 29 | ### pixelParams: 30 | - bit 0: notGrass 31 | - bit 1: hasOverlays 32 | - bit 2-3: colorType 33 | - bit 4-5: heightShade 34 | - bit 6: height in extra byte instead of param (not written, legacy?) 35 | - bit 7: isCaveBlock (always false?) 36 | - bit 8-11: light 37 | - bit 12-19: height 38 | - bit 20: hasBiome 39 | 40 | ### heightShade: 41 | - val 0: normal 42 | - val 1: brighter 43 | - val 2: darker 44 | - val 3: uninitialized (renders as normal) 45 | 46 | ### colorType: 47 | TODO biomeStuff[0] 48 | 49 | ### customColor: 50 | TODO biomeStuff[2] 51 | 52 | ## overlay: 53 | - int overlayParams 54 | - isWater? => int overlayBlockState 55 | - colorType == 2? => int customColour 56 | - opacity > 1? => int opacity 57 | 58 | ### overlayParams: 59 | - bit 0: notWater 60 | - bit 4: opacity > 1 61 | - bit 4-7: light 62 | - bit 8-11: colorType 63 | --------------------------------------------------------------------------------