├── .github └── workflows │ └── publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── Manual.md ├── README.md ├── misc ├── mapeditr_banner.png └── mapeditr_banner.svg ├── src ├── block_utils.rs ├── cmd_line.rs ├── commands │ ├── clone.rs │ ├── delete_blocks.rs │ ├── delete_meta.rs │ ├── delete_objects.rs │ ├── delete_timers.rs │ ├── fill.rs │ ├── mod.rs │ ├── overlay.rs │ ├── replace_in_inv.rs │ ├── replace_nodes.rs │ ├── set_meta_var.rs │ ├── set_param2.rs │ └── vacuum.rs ├── instance.rs ├── main.rs ├── map_block │ ├── map_block.rs │ ├── metadata.rs │ ├── mod.rs │ ├── name_id_map.rs │ ├── node_data.rs │ ├── node_timer.rs │ └── static_object.rs ├── map_database.rs ├── spatial │ ├── area.rs │ ├── mod.rs │ └── vec3.rs ├── testing.rs └── utils.rs └── testing ├── mapblock_v25.bin ├── mapblock_v28.bin ├── mapblock_v29.bin └── test_mod ├── init.lua └── textures └── test_mod_test.png /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish builds 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | name: Publish build for Windows 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | 21 | - name: Build 22 | run: | 23 | cargo build --release --locked 24 | tar.exe -c -a -f mapeditr-windows.zip LICENSE.txt README.md Manual.md -C target/release mapeditr.exe 25 | 26 | - name: Upload artifacts to release 27 | uses: softprops/action-gh-release@v1 28 | with: 29 | files: mapeditr-windows.zip 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | # Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # VSCode files 17 | .vscode/* 18 | *.code-workspace 19 | 20 | # Local History for Visual Studio Code 21 | .history/ 22 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "ansi_term" 13 | version = "0.12.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 16 | dependencies = [ 17 | "winapi", 18 | ] 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.66" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" 25 | 26 | [[package]] 27 | name = "atty" 28 | version = "0.2.14" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 31 | dependencies = [ 32 | "hermit-abi", 33 | "libc", 34 | "winapi", 35 | ] 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "1.3.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 42 | 43 | [[package]] 44 | name = "byteorder" 45 | version = "1.4.3" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 48 | 49 | [[package]] 50 | name = "cc" 51 | version = "1.0.74" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" 54 | dependencies = [ 55 | "jobserver", 56 | ] 57 | 58 | [[package]] 59 | name = "cfg-if" 60 | version = "1.0.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 63 | 64 | [[package]] 65 | name = "clap" 66 | version = "2.34.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 69 | dependencies = [ 70 | "ansi_term", 71 | "atty", 72 | "bitflags", 73 | "strsim", 74 | "textwrap", 75 | "unicode-width", 76 | "vec_map", 77 | ] 78 | 79 | [[package]] 80 | name = "crc32fast" 81 | version = "1.3.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 84 | dependencies = [ 85 | "cfg-if", 86 | ] 87 | 88 | [[package]] 89 | name = "flate2" 90 | version = "1.0.24" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" 93 | dependencies = [ 94 | "crc32fast", 95 | "miniz_oxide", 96 | ] 97 | 98 | [[package]] 99 | name = "hermit-abi" 100 | version = "0.1.19" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 103 | dependencies = [ 104 | "libc", 105 | ] 106 | 107 | [[package]] 108 | name = "jobserver" 109 | version = "0.1.25" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" 112 | dependencies = [ 113 | "libc", 114 | ] 115 | 116 | [[package]] 117 | name = "libc" 118 | version = "0.2.137" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" 121 | 122 | [[package]] 123 | name = "mapeditr" 124 | version = "1.1.0" 125 | dependencies = [ 126 | "anyhow", 127 | "byteorder", 128 | "clap", 129 | "flate2", 130 | "memmem", 131 | "sqlite", 132 | "thiserror", 133 | "zstd", 134 | ] 135 | 136 | [[package]] 137 | name = "memmem" 138 | version = "0.1.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" 141 | 142 | [[package]] 143 | name = "miniz_oxide" 144 | version = "0.5.4" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" 147 | dependencies = [ 148 | "adler", 149 | ] 150 | 151 | [[package]] 152 | name = "pkg-config" 153 | version = "0.3.26" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 156 | 157 | [[package]] 158 | name = "proc-macro2" 159 | version = "1.0.47" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 162 | dependencies = [ 163 | "unicode-ident", 164 | ] 165 | 166 | [[package]] 167 | name = "quote" 168 | version = "1.0.21" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 171 | dependencies = [ 172 | "proc-macro2", 173 | ] 174 | 175 | [[package]] 176 | name = "sqlite" 177 | version = "0.26.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "3fb1a534c07ec276fbbe0e55a1c00814d8563da3a2f4d9d9d4c802bd1278db6a" 180 | dependencies = [ 181 | "libc", 182 | "sqlite3-sys", 183 | ] 184 | 185 | [[package]] 186 | name = "sqlite3-src" 187 | version = "0.3.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "a260b07ce75a0644c6f5891f34f46db9869e731838e95293469ab17999abcfa3" 190 | dependencies = [ 191 | "cc", 192 | "pkg-config", 193 | ] 194 | 195 | [[package]] 196 | name = "sqlite3-sys" 197 | version = "0.13.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "04d2f028faeb14352df7934b4771806f60d61ce61be1928ec92396d7492e2e54" 200 | dependencies = [ 201 | "libc", 202 | "sqlite3-src", 203 | ] 204 | 205 | [[package]] 206 | name = "strsim" 207 | version = "0.8.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 210 | 211 | [[package]] 212 | name = "syn" 213 | version = "1.0.103" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" 216 | dependencies = [ 217 | "proc-macro2", 218 | "quote", 219 | "unicode-ident", 220 | ] 221 | 222 | [[package]] 223 | name = "textwrap" 224 | version = "0.11.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 227 | dependencies = [ 228 | "unicode-width", 229 | ] 230 | 231 | [[package]] 232 | name = "thiserror" 233 | version = "1.0.37" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 236 | dependencies = [ 237 | "thiserror-impl", 238 | ] 239 | 240 | [[package]] 241 | name = "thiserror-impl" 242 | version = "1.0.37" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 245 | dependencies = [ 246 | "proc-macro2", 247 | "quote", 248 | "syn", 249 | ] 250 | 251 | [[package]] 252 | name = "unicode-ident" 253 | version = "1.0.5" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 256 | 257 | [[package]] 258 | name = "unicode-width" 259 | version = "0.1.10" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 262 | 263 | [[package]] 264 | name = "vec_map" 265 | version = "0.8.2" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 268 | 269 | [[package]] 270 | name = "winapi" 271 | version = "0.3.9" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 274 | dependencies = [ 275 | "winapi-i686-pc-windows-gnu", 276 | "winapi-x86_64-pc-windows-gnu", 277 | ] 278 | 279 | [[package]] 280 | name = "winapi-i686-pc-windows-gnu" 281 | version = "0.4.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 284 | 285 | [[package]] 286 | name = "winapi-x86_64-pc-windows-gnu" 287 | version = "0.4.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 290 | 291 | [[package]] 292 | name = "zstd" 293 | version = "0.11.2+zstd.1.5.2" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" 296 | dependencies = [ 297 | "zstd-safe", 298 | ] 299 | 300 | [[package]] 301 | name = "zstd-safe" 302 | version = "5.0.2+zstd.1.5.2" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" 305 | dependencies = [ 306 | "libc", 307 | "zstd-sys", 308 | ] 309 | 310 | [[package]] 311 | name = "zstd-sys" 312 | version = "2.0.1+zstd.1.5.2" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" 315 | dependencies = [ 316 | "cc", 317 | "libc", 318 | ] 319 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mapeditr" 3 | version = "1.1.0" 4 | authors = ["random-geek (github.com/random-geek)"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1" 9 | byteorder = "1" 10 | clap = "2" 11 | flate2 = "1" 12 | memmem = "0.1" 13 | sqlite = "0.26" 14 | thiserror = "1" 15 | zstd = "0.11" 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MapEditr copyright notice and MIT license terms 2 | ----------------------------------------------- 3 | 4 | Copyright (c) 2021 random-geek (github.com/random-geek) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | END OF MAPEDITR LICENSE TERMS 25 | 26 | The following dependencies are included in builds of MapEditr and are licensed 27 | under the terms of the MIT license, reproduced below. 28 | 29 | anyhow 30 | byteorder: Copyright (c) 2015 Andrew Gallant 31 | clap: Copyright (c) 2015-2016 Kevin B. Knapp 32 | flate2: Copyright (c) 2014 Alex Crichton 33 | memmem: Copyright (c) 2014 The Rust Project Developers 34 | sqlite: Copyright 2015–2021 The sqlite Developers 35 | thiserror 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy 38 | of this software and associated documentation files (the "Software"), to deal 39 | in the Software without restriction, including without limitation the rights 40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the Software is 42 | furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all 45 | copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 53 | SOFTWARE. 54 | -------------------------------------------------------------------------------- /Manual.md: -------------------------------------------------------------------------------- 1 | # The MapEditr Manual 2 | 3 | ## Introduction 4 | 5 | MapEditr is a command-line tool for editing Minetest worlds. Note that MapEditr 6 | is not a mod or plugin; it is a separate program which operates independently 7 | of Minetest. 8 | 9 | MapEditr reads and edits *map databases*, usually a file named `map.sqlite` 10 | within each Minetest world directory. As such, the terms "world" and "map" may 11 | be used interchangeably. Note that only SQLite format maps are currently 12 | supported. 13 | 14 | For most commands to work, the parts of the map to be read/modified must 15 | already be generated. This can be done by either exploring the area in-game, 16 | or by using Minetest's built-in `/emergeblocks` command. 17 | 18 | MapEditr supports all maps created since Minetest version 0.4.2-rc1, released 19 | July 2012. Any unsupported parts of the map will be skipped. 20 | 21 | ## General usage 22 | 23 | `mapeditr [-h] [-y] ` 24 | 25 | Arguments: 26 | 27 | - `-h, --help`: Print help information and exit. 28 | - `-y, --yes`: Skip the default confirmation prompt (for those who feel brave). 29 | - ``: Path to the Minetest world/map to edit; this can be either a world 30 | directory or a `map.sqlite` file. This world/map will be modified, so *always* 31 | shut down the game or server before executing any command. 32 | - ``: Command to execute. See the "Commands" section below. 33 | 34 | ### Common command arguments 35 | 36 | - `--p1 ` and `--p2 `: Used to select a box-shaped 37 | area with corners at `p1` and `p2`, similarly to how WorldEdit's area selection 38 | works. Any two opposite corners can be used. These coordinates can be found 39 | using Minetest's F5 debug menu. 40 | - Node/item names, including `node`, `new_node`, etc.: Must be the full name, 41 | e.g. "default:stone", not just "stone". 42 | 43 | ### Other tips 44 | 45 | Optional arguments are indicated using [square brackets]. 46 | 47 | Text-like arguments can be surrounded with "quotes" if they contain spaces. 48 | 49 | Due to technical limitations, MapEditr will often leave lighting glitches. To 50 | fix these, use Minetest's built-in `/fixlight` command, or the equivalent 51 | WorldEdit `//fixlight` command. 52 | 53 | ## Commands 54 | 55 | ### clone 56 | 57 | Usage: `clone --p1 x y z --p2 x y z --offset x y z` 58 | 59 | Clone (copy) the contents of an area to a new location. 60 | 61 | Arguments: 62 | 63 | - `--p1, --p2`: Area to clone. 64 | - `--offset x y z`: Vector to shift the area's contents by. For example, to 65 | copy an area 50 nodes downward (negative Y direction), use `--offset 0 -50 0`. 66 | Directions may be determined using Minetest's F5 debug menu. 67 | 68 | This command copies nodes, param1, param2, and metadata. Nothing will be copied 69 | from or into mapblocks that are not yet generated. 70 | 71 | Examples: 72 | 73 | - Clone an area surrounding spawn 500 nodes west and 360 nodes north: 74 | `clone --p1 200 80 200 --p2 -200 -15 -200 --offset -500 0 360` 75 | 76 | ### deleteblocks 77 | 78 | Usage: `deleteblocks --p1 x y z --p2 x y z [--invert]` 79 | 80 | Delete all mapblocks inside or outside an area. This command is often much 81 | faster than Minetest's built-in `/deleteblocks` command. 82 | 83 | Arguments: 84 | 85 | - `--p1, --p2`: Area containing mapblocks to delete. By default, only mapblocks 86 | fully within this area will be deleted. 87 | - `--invert`: Delete all mapblocks fully *outside* the given area. Use with 88 | caution; you could erase a large portion of your world! 89 | 90 | **Note:** Deleting mapblocks is *not* the same as filling them with air! Mapgen 91 | will be invoked where the blocks were deleted, and this sometimes causes 92 | terrain glitches. 93 | 94 | Examples: 95 | 96 | - Delete all saved mapblocks below y = -200 and above y = 200: 97 | `deleteblocks --p1 -31000 -200 -31000 --p2 31000 200 31000 --invert` 98 | 99 | ### deletemeta 100 | 101 | Usage: `deletemeta [--node ] [--p1 x y z] [--p2 x y z] [--invert]` 102 | 103 | Delete node metadata of certain nodes. Node inventories (such as chest/furnace 104 | contents) are also deleted. 105 | 106 | Arguments: 107 | 108 | - `--node `: (Optional) Name of node to delete metadata from. If not 109 | specified, metadata will be deleted from any node. 110 | - `--p1, --p2`: (Optional) Area in which to delete metadata. If not specified, 111 | metadata will be deleted everywhere. 112 | - `--invert`: Delete metadata *outside* the given area. 113 | 114 | ### deleteobjects 115 | 116 | Usage: `deleteobjects [--obj ] [--items [items]] [--p1 x y z] [--p2 x y z] [--invert]` 117 | 118 | Delete certain objects (entities) and/or item entities (dropped items). 119 | 120 | Arguments: 121 | 122 | - `--obj `: (Optional) Name of object to delete, e.g. "boats:boat". 123 | - `--items [items]`: (Optional) Delete only item entities (dropped items). If 124 | one or more item names are listed after `--items`, only those items will be 125 | deleted. 126 | - `--p1, --p2`: (Optional) Area in which to delete objects. If not specified, 127 | objects will be deleted everywhere. 128 | - `--invert`: Delete objects *outside* the given area. 129 | 130 | `--obj` and `--items` cannot be used simultaneously. 131 | 132 | Examples: 133 | 134 | - Delete all objects: `deleteobjects` 135 | - Delete all cart entities: `deleteobjects --obj carts:cart` 136 | - Delete dropped stone and gravel: 137 | `deleteobjects --items default:stone default:gravel` 138 | 139 | ### deletetimers 140 | 141 | Usage: `deletetimers [--node ] [--p1 x y z] [--p2 x y z] [--invert]` 142 | 143 | Delete node timers of certain nodes. 144 | 145 | Arguments: 146 | 147 | - `--node `: (Optional) Name of node to delete node timers from. If not 148 | specified, node timers of any node will be deleted. 149 | - `--p1, --p2`: (Optional) Area in which to delete node timers. If not 150 | specified, node timers will be deleted everywhere. 151 | - `--invert`: Delete node timers *outside* the given area. 152 | 153 | ### fill 154 | 155 | Usage: `fill --p1 x y z --p2 x y z [--invert] ` 156 | 157 | Set all nodes inside or outside an area. Mapblocks that are not yet generated 158 | will not be affected. 159 | 160 | This command does not affect param2, node metadata, etc. 161 | 162 | Arguments: 163 | 164 | - `--p1, --p2`: Area to fill. 165 | - `--invert`: Fill all generated nodes *outside* the given area. 166 | - ``: Name of the node to fill with. 167 | 168 | Examples: 169 | 170 | - Carve out a large hole in the ground: 171 | `fill --p1 224 50 347 --p2 817 -40 73 air` 172 | - Build a long obsidian glass wall travelling north/south: 173 | `fill --p1 0 -30 -10000 --p2 0 30 10000 default:obsidian_glass` 174 | 175 | ### overlay 176 | 177 | Usage: `overlay [--p1 x y z] [--p2 x y z] [--invert] [--offset x y z]` 178 | 179 | Copy part or all of a source map into the main map. 180 | 181 | Arguments: 182 | 183 | - ``: Path to the source map/world. This map will not be modified. 184 | - `--p1, --p2`: (Optional) Area to copy from. If not specified, MapEditr will 185 | try to copy everything from the source map. 186 | - `--invert`: Copy everything *outside* the given area. 187 | - `--offset x y z`: (Optional) Vector to shift nodes by when copying; default 188 | is no offset. Currently, an offset cannot be used with an inverted selection. 189 | 190 | **If an area and/or offset is used:** To ensure that all data is copied, make 191 | sure at least the "edges" of the destination area are generated, or the entire 192 | destination area if using an offset. 193 | 194 | This command will always copy nodes, param1, param2, and metadata. If no 195 | offset is used, objects/entities and node timers may also be copied. 196 | 197 | **Tip:** Overlay will be significantly faster if no offset is used. 198 | 199 | Examples (your world/map paths will vary): 200 | 201 | - Copy all of map `backup.sqlite` into the main world: 202 | `overlay backup-maps/backup.sqlite` 203 | - Copy world `test` into the main world, excluding the area within 120 nodes of 204 | spawn: `overlay test --p1 -120 -120 -120 --p2 120 120 120 --invert` 205 | - Copy an area from `map.sqlite` into the main world, moving it 32 nodes north: 206 | `overlay map.sqlite --p1 6 36 -49 --p2 -9 74 -78 --offset 0 0 32` 207 | 208 | ### replaceininv 209 | 210 | Usage: `replaceininv [new_item] [--delete] [--deletemeta] [--nodes ] [--p1 x y z] [--p2 x y z] [--invert]` 211 | 212 | Replace, delete, or modify items in certain node inventories. 213 | 214 | Arguments: 215 | 216 | - ``: Name of the item to replace/delete. 217 | - `[new_item]`: (Optional) Name of the new item, if replacing items. 218 | - `--delete`: Delete items instead of replacing them. 219 | - `--deletemeta`: Delete metadata of affected items. May be used with or 220 | without `new_item`, depending on whether items should also be replaced. 221 | - `--nodes `: (Optional) Names of one or more nodes to modify 222 | inventories of. If not specified, items will be modified in any node with an 223 | inventory. 224 | - `--p1, --p2`: (Optional) Area in which to modify node inventories. If not 225 | specified, items will be modified everywhere. 226 | - `--invert`: Modify node inventories *outside* the given area. 227 | 228 | Examples: 229 | 230 | - Delete all lava buckets: 231 | `replaceininv bucket:bucket_lava --delete` 232 | - Replace all written books in chests with unwritten books, deleting metadata: 233 | `replaceininv default:book_written default:book --deletemeta --nodes default:chest default:chest_locked` 234 | 235 | ### replacenodes 236 | 237 | Usage: `replacenodes [--p1 x y z] [--p2 x y z] [--invert]` 238 | 239 | Replace one node with another node. Can also be used to remove unknown nodes 240 | or swap a node that changed names. 241 | 242 | This command does not affect param2, metadata, etc. 243 | 244 | Arguments: 245 | 246 | - ``: Name of node to replace. 247 | - ``: Name of node to replace with. 248 | - `--p1, --p2`: (Optional) Area in which to replace nodes. If not specified, 249 | nodes will be replaced across the entire map. 250 | - `--invert`: Replace nodes *outside* the given area. 251 | 252 | Examples: 253 | 254 | - Replace all legacy PB&J pup nodes with mese blocks: 255 | `replacenodes pbj_pup:pbj_pup default:mese` 256 | - Remove fire nodes near ground level: 257 | `replacenodes fire:basic_flame air --p1 -31000 -80 -31000 --p2 31000 200 31000` 258 | 259 | ### setmetavar 260 | 261 | Usage: `setmetavar [value] [--delete] [--nodes ] [--p1 x y z] [--p2 x y z] [--invert]` 262 | 263 | Set or delete a variable in node metadata of certain nodes. This only works on 264 | nodes where the variable is already set. 265 | 266 | Arguments: 267 | 268 | - ``: Name of variable to set/delete, e.g. `infotext`, `formspec`, etc. 269 | - `[value]`: Value to set variable to, if setting a value. This should be a 270 | string. 271 | - `--delete`: Delete the variable. 272 | - `--nodes `: (Optional) Names of one or more nodes to modify. If not 273 | specified, any node with the given variable will be modified. 274 | - `--p1, --p2`: (Optional) Area in which to modify node metadata. If not 275 | specified, nodes will be modified everywhere. 276 | - `--invert`: Modify node metadata *outside* the given area. 277 | 278 | Examples: 279 | 280 | - Clear infotext of signs: 281 | `setmetavar infotext --delete --nodes default:sign_wall_steel default:sign_wall_wood` 282 | - Set "player1" as the owner of all steel trapdoors: 283 | `setmetavar owner player1 --nodes doors:trapdoor_steel` 284 | 285 | ### setparam2 286 | 287 | Usage: `setparam2 [--node ] [--p1 x y z] [--p2 x y z] [--invert] ` 288 | 289 | Set param2 values of certain nodes. 290 | 291 | Arguments: 292 | 293 | - `--node `: (Optional) Name of node to modify. If not specified, the 294 | param2 values of any node will be set. 295 | - `--p1, --p2`: (Optional) Area in which to set param2 values. If not 296 | specified, param2 will be set everywhere. 297 | - `--invert`: Set param2 values *outside* the given area. 298 | - ``: New param2 value, between 0 and 255. 299 | 300 | An area and/or node is required for this command. 301 | 302 | ### vacuum 303 | 304 | Usage: `vacuum` 305 | 306 | Rebuild the map database to reduce its size. Vacuuming may take a long time for 307 | large maps. 308 | 309 | This command simply executes the SQLite `VACUUM` command, which shrinks and 310 | optimizes the map database by efficiently "repacking" all mapblocks. No map 311 | data is changed or deleted. 312 | 313 | **Note:** Because data is temporarily copied into another file, vacuum could 314 | require as much free disk space as is already occupied by the map. For example, 315 | if map.sqlite is 10 GB, make sure you have **at least 10 GB** of free space! 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MapEditr 2 | 3 | [![](https://img.shields.io/badge/Minetest%20Forums-MapEditr-4E9A06)](https://forum.minetest.net/viewtopic.php?f=14&t=26803) 4 | ![](https://img.shields.io/github/v/release/random-geek/MapEditr) 5 | 6 | MapEditr is a command-line tool for fast manipulation of Minetest worlds. It 7 | can replace nodes and items, fill areas, combine parts of different worlds, and 8 | much more. 9 | 10 | This tool is functionally similar to [WorldEdit][1], but designed for large 11 | operations that would be impractical to do within Minetest. Since it is mainly 12 | optimized for speed, MapEditr lacks some of the more specialty features of 13 | WorldEdit. 14 | 15 | MapEditr was originally based on [MapEdit][2], except written in Rust rather 16 | than Python (hence the added "r"). Switching to a compiled language will make 17 | MapEditr more robust and easier to maintain in the future. 18 | 19 | ## Compilation/Installation 20 | 21 | ### Option 1: Pre-built releases 22 | 23 | If you are using Windows and don't have Rust installed, you can download a 24 | build of the latest release of MapEditr from the [Releases page][3]. Only 25 | 64-bit Windows builds are currently available. 26 | 27 | To run the `mapeditr` command from anywhere, the path to the executable file 28 | must be included in your system's Path variable. [Here is one article][4] 29 | explaining how to edit the Path variable on Windows. 30 | 31 | ### Option 2: Install using Cargo 32 | 33 | This method works on any operating system. To use Cargo, you must have Rust 34 | installed first, which can be downloaded from [the Rust website][5]. Then, 35 | simply run: 36 | 37 | `cargo install --git https://github.com/random-geek/MapEditr.git` 38 | 39 | This will download MapEditr and install it to `$HOME/.cargo/bin`. After 40 | installing, you should be able to run MapEditr from anywhere with the 41 | `mapeditr` command. 42 | 43 | ### Option 3: Build normally 44 | 45 | If you don't wish to install MapEditr, you can build it normally using Cargo. 46 | In the MapEditr directory, run: 47 | 48 | `cargo build --release` 49 | 50 | The `--release` flag is important, as it produces an optimized executable which 51 | runs much faster than the default, unoptimized version. The compiled executable 52 | will be in the `target/release` directory. 53 | 54 | ## Usage 55 | 56 | For an overview of how MapEditr works and a listing of commands and their 57 | usages, see [Manual.md](Manual.md). 58 | 59 | These are just a few of the useful things you can do with MapEditr: 60 | 61 | - Remove unknown nodes left by old mods with `replacenodes`. 62 | - Build extremely long walls and roads in seconds using `fill`. 63 | - Selectively delete entities and/or dropped items using `deleteobjects`. 64 | - Combine multiple worlds or map saves with `overlay`. 65 | 66 | ## License 67 | 68 | MapEditr is under the terms of the MIT license as defined in `LICENSE.txt`. 69 | 70 | Additionally, if you use code from MapEditr in another project, I would 71 | greatly appreciate a reasonable acknowledgement/attribution of MapEditr in your 72 | project's readme or documentation. 73 | 74 | ## Acknowledgments 75 | 76 | The [Minetest][6] project has been rather important for the making of 77 | MapEdit/MapEditr, for obvious reasons. 78 | 79 | Some parts of the original MapEdit code were adapted from AndrejIT's 80 | [map_unexplore][7] project. All due credit goes to the author(s) of that 81 | project. 82 | 83 | Thank you also to ExeterDad and the moderators of the late Hometown server, for 84 | partially inspiring MapEdit/MapEditr. 85 | 86 | [1]: https://github.com/Uberi/Minetest-WorldEdit 87 | [2]: https://github.com/random-geek/MapEdit 88 | [3]: https://github.com/random-geek/MapEditr/releases 89 | [4]: https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/ 90 | [5]: https://www.rust-lang.org 91 | [6]: https://github.com/minetest/minetest 92 | [7]: https://github.com/AndrejIT/map_unexplore 93 | -------------------------------------------------------------------------------- /misc/mapeditr_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/random-geek/MapEditr/d75676679b468eebd31493c8a127e88418af1df8/misc/mapeditr_banner.png -------------------------------------------------------------------------------- /misc/mapeditr_banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 34 | 38 | 43 | 47 | 51 | 55 | 59 | 63 | 68 | 72 | 73 | 77 | 81 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/block_utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::map_block::{MapBlock, NodeMetadataList, NameIdMap}; 4 | use crate::spatial::{Vec3, Area}; 5 | 6 | 7 | fn block_parts_valid(a: &Area, b: &Area) -> bool { 8 | fn part_valid(a: &Area) -> bool { 9 | a.min.x >= 0 && a.min.y >= 0 && a.min.z >= 0 10 | && a.max.x < 16 && a.max.y < 16 && a.max.z < 16 11 | } 12 | part_valid(a) && part_valid(b) && a.max - a.min == b.max - b.min 13 | } 14 | 15 | 16 | /// Copy an area of nodes from one mapblock to another. 17 | /// 18 | /// Will not remove duplicate/unused name IDs. 19 | pub fn merge_blocks( 20 | src_block: &MapBlock, 21 | dst_block: &mut MapBlock, 22 | src_area: Area, 23 | dst_area: Area 24 | ) { 25 | assert!(block_parts_valid(&src_area, &dst_area)); 26 | 27 | let src_nd = &src_block.node_data; 28 | let dst_nd = &mut dst_block.node_data; 29 | let offset = dst_area.min - src_area.min; 30 | // Warning: diff can be negative! 31 | let diff = offset.x + offset.y * 16 + offset.z * 256; 32 | 33 | // Copy name-ID mappings 34 | let nimap_diff = dst_block.nimap.get_max_id().unwrap() + 1; 35 | for (id, name) in &src_block.nimap.0 { 36 | dst_block.nimap.0.insert(id + nimap_diff, name.to_vec()); 37 | } 38 | 39 | // Copy node IDs 40 | for z in src_area.min.z ..= src_area.max.z { 41 | for y in src_area.min.y ..= src_area.max.y { 42 | for x in src_area.min.x ..= src_area.max.x { 43 | let idx = x + y * 16 + z * 256; 44 | dst_nd.nodes[(idx + diff) as usize] = 45 | src_nd.nodes[idx as usize] + nimap_diff; 46 | } 47 | } 48 | } 49 | 50 | // Copy param1 and param2 51 | for z in src_area.min.z ..= src_area.max.z { 52 | for y in src_area.min.y ..= src_area.max.y { 53 | let row_start = y * 16 + z * 256; 54 | let start = row_start + src_area.min.x; 55 | let end = row_start + src_area.max.x; 56 | 57 | dst_nd.param1[(start + diff) as usize ..= (end + diff) as usize] 58 | .clone_from_slice( 59 | &src_nd.param1[start as usize ..= end as usize] 60 | ); 61 | dst_nd.param2[(start + diff) as usize ..= (end + diff) as usize] 62 | .clone_from_slice( 63 | &src_nd.param2[start as usize ..= end as usize] 64 | ); 65 | } 66 | } 67 | } 68 | 69 | 70 | /// Copy an area of node metadata from one mapblock to another. 71 | pub fn merge_metadata( 72 | src_meta: &NodeMetadataList, 73 | dst_meta: &mut NodeMetadataList, 74 | src_area: Area, 75 | dst_area: Area 76 | ) { 77 | assert!(block_parts_valid(&src_area, &dst_area)); 78 | 79 | let offset = dst_area.min - src_area.min; 80 | // Warning: diff can be negative! 81 | let diff = offset.x + offset.y * 16 + offset.z * 256; 82 | 83 | // Delete any existing metadata in the destination area. 84 | let mut to_delete = Vec::with_capacity(dst_meta.len()); 85 | for (&idx, _) in dst_meta.iter() { 86 | let pos = Vec3::from_u16_key(idx); 87 | if dst_area.contains(pos) { 88 | to_delete.push(idx); 89 | } 90 | } 91 | for idx in &to_delete { 92 | dst_meta.remove(idx); 93 | } 94 | 95 | // Copy new metadata 96 | for (&idx, meta) in src_meta { 97 | let pos = Vec3::from_u16_key(idx); 98 | if src_area.contains(pos) { 99 | dst_meta.insert((idx as i32 + diff) as u16, meta.clone()); 100 | } 101 | } 102 | } 103 | 104 | 105 | /// Culls duplicate and unused IDs from the name-ID map and node data. 106 | pub fn clean_name_id_map(block: &mut MapBlock) { 107 | let id_count = (block.nimap.get_max_id().unwrap() + 1) as usize; 108 | 109 | // Determine which IDs are used. 110 | let mut used = vec![false; id_count]; 111 | for id in &block.node_data.nodes { 112 | used[*id as usize] = true; 113 | } 114 | 115 | // Rebuild the name-ID map. 116 | let mut new_nimap = NameIdMap(BTreeMap::new()); 117 | let mut map = vec![0u16; id_count]; // map[old_node_id] == new_node_id 118 | for (&id, name) in &block.nimap.0 { 119 | // Skip unused IDs. 120 | if !used[id as usize] { 121 | continue; 122 | } 123 | 124 | if let Some(first_id) = new_nimap.get_id(&name) { 125 | // Name is already in the map; map old, duplicate ID to the 126 | // existing ID. 127 | map[id as usize] = first_id as u16; 128 | } else { 129 | // Name is not yet in the map; assign it to the next ID. 130 | new_nimap.0.insert(new_nimap.0.len() as u16, name.clone()); 131 | // Map old ID to newly-inserted ID. 132 | map[id as usize] = new_nimap.0.len() as u16 - 1; 133 | } 134 | } 135 | block.nimap = new_nimap; 136 | 137 | // Re-assign node IDs. 138 | for id in &mut block.node_data.nodes { 139 | *id = map[*id as usize]; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/cmd_line.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::time::{Duration, Instant}; 3 | 4 | use clap::{App, Arg, SubCommand, AppSettings, crate_version, crate_authors}; 5 | use anyhow::Context; 6 | 7 | use crate::spatial::{Vec3, Area}; 8 | use crate::instance::{LogType, ArgType, InstArgs}; 9 | use crate::commands::{get_commands}; 10 | use crate::utils::fmt_duration; 11 | 12 | 13 | fn arg_to_pos(p: clap::Values) -> anyhow::Result { 14 | let vals: Vec<_> = p.collect(); 15 | if vals.len() != 3 { 16 | anyhow::bail!(""); 17 | } 18 | Ok(Vec3::new( 19 | vals[0].parse()?, 20 | vals[1].parse()?, 21 | vals[2].parse()? 22 | )) 23 | } 24 | 25 | 26 | fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str)) 27 | -> Vec> 28 | { 29 | let arg_type = tup.0.clone(); 30 | let help_msg = tup.1; 31 | if let ArgType::Area(req) = arg_type { 32 | return vec![ 33 | Arg::with_name("p1") 34 | .long("p1") 35 | .allow_hyphen_values(true) 36 | .number_of_values(3) 37 | .value_names(&["x", "y", "z"]) 38 | .required(req) 39 | .requires("p2") 40 | .help(help_msg), 41 | Arg::with_name("p2") 42 | .long("p2") 43 | .allow_hyphen_values(true) 44 | .number_of_values(3) 45 | .value_names(&["x", "y", "z"]) 46 | .required(req) 47 | .requires("p1") 48 | .help(help_msg) 49 | ]; 50 | } 51 | 52 | let arg = match arg_type { 53 | ArgType::Area(_) => unreachable!(), 54 | ArgType::InputMapPath => 55 | Arg::with_name("input_map") 56 | .required(true), 57 | ArgType::Invert => 58 | Arg::with_name("invert") 59 | .long("invert"), 60 | ArgType::Offset(req) => 61 | Arg::with_name("offset") 62 | .long("offset") 63 | .allow_hyphen_values(true) 64 | .number_of_values(3) 65 | .value_names(&["x", "y", "z"]) 66 | .required(req), 67 | ArgType::Node(req) => { 68 | let a = Arg::with_name("node"); 69 | if req { 70 | a.required(true) 71 | } else { 72 | a.long("node").takes_value(true) 73 | } 74 | }, 75 | ArgType::Nodes => 76 | Arg::with_name("nodes") 77 | .long("nodes") 78 | .min_values(1), 79 | ArgType::NewNode => 80 | Arg::with_name("new_node") 81 | .takes_value(true) 82 | .required(true), 83 | ArgType::Object => 84 | Arg::with_name("object") 85 | .long("obj") 86 | .takes_value(true), 87 | ArgType::Item => 88 | Arg::with_name("item") 89 | .takes_value(true) 90 | .required(true), 91 | ArgType::Items => 92 | Arg::with_name("items") 93 | .long("items") 94 | .min_values(0), 95 | ArgType::NewItem => 96 | Arg::with_name("new_item") 97 | .takes_value(true), 98 | ArgType::Delete => 99 | Arg::with_name("delete") 100 | .long("delete"), 101 | ArgType::DeleteMeta => 102 | Arg::with_name("delete_meta") 103 | .long("deletemeta"), 104 | ArgType::Key => 105 | Arg::with_name("key") 106 | .takes_value(true) 107 | .required(true), 108 | ArgType::Value => 109 | Arg::with_name("value") 110 | .takes_value(true), 111 | ArgType::Param2 => 112 | Arg::with_name("param2") 113 | .required(true), 114 | }.help(help_msg); 115 | 116 | vec![arg] 117 | } 118 | 119 | 120 | fn parse_cmd_line_args() -> anyhow::Result { 121 | /* Create the clap app */ 122 | let commands = get_commands(); 123 | 124 | let app_commands = commands.iter().map(|(cmd_name, cmd)| { 125 | let args: Vec<_> = cmd.args.iter().flat_map(to_cmd_line_args) 126 | .collect(); 127 | SubCommand::with_name(cmd_name) 128 | .about(cmd.help) 129 | .args(&args) 130 | .after_help("For additional information, see the manual.") 131 | }); 132 | 133 | let app = App::new("MapEditr") 134 | .about("Edits Minetest worlds/map databases.") 135 | .after_help( 136 | "For command-specific help, run: mapeditr -h\n\ 137 | For additional information, see the manual.") 138 | .version(crate_version!()) 139 | .author(crate_authors!()) 140 | .arg(Arg::with_name("yes") 141 | .long("yes") 142 | .short("y") 143 | .global(true) 144 | .help("Skip the default confirmation prompt.") 145 | ) 146 | .arg(Arg::with_name("map") 147 | .required(true) 148 | .help("Path to world directory or map database to edit") 149 | ) 150 | .setting(AppSettings::SubcommandRequired) 151 | .subcommands(app_commands); 152 | 153 | /* Parse the arguments */ 154 | let matches = app.get_matches(); 155 | let sub_name = matches.subcommand_name().unwrap().to_string(); 156 | let sub_matches = matches.subcommand_matches(&sub_name).unwrap(); 157 | 158 | Ok(InstArgs { 159 | do_confirmation: !matches.is_present("yes"), 160 | command: sub_name, 161 | map_path: matches.value_of("map").unwrap().to_string(), 162 | input_map_path: sub_matches.value_of("input_map").map(str::to_string), 163 | area: { 164 | let p1_maybe = sub_matches.values_of("p1").map(arg_to_pos) 165 | .transpose().context("Invalid p1 value.")?; 166 | let p2_maybe = sub_matches.values_of("p2").map(arg_to_pos) 167 | .transpose().context("Invalid p2 value.")?; 168 | if let (Some(p1), Some(p2)) = (p1_maybe, p2_maybe) { 169 | Some(Area::from_unsorted(p1, p2)) 170 | } else { 171 | None 172 | } 173 | }, 174 | invert: sub_matches.is_present("invert"), 175 | offset: sub_matches.values_of("offset").map(arg_to_pos).transpose() 176 | .context("Invalid offset value.")?, 177 | node: sub_matches.value_of("node").map(str::to_string), 178 | nodes: sub_matches.values_of("nodes").iter_mut().flatten() 179 | .map(str::to_string).collect(), 180 | new_node: sub_matches.value_of("new_node").map(str::to_string), 181 | object: sub_matches.value_of("object").map(str::to_string), 182 | item: sub_matches.value_of("item").map(str::to_string), 183 | items: sub_matches.values_of("items") 184 | .map(|v| v.map(str::to_string).collect()), 185 | new_item: sub_matches.value_of("new_item").map(str::to_string), 186 | delete: sub_matches.is_present("delete"), 187 | delete_meta: sub_matches.is_present("delete_meta"), 188 | key: sub_matches.value_of("key").map(str::to_string), 189 | value: sub_matches.value_of("value").map(str::to_string), 190 | param2: sub_matches.value_of("param2_val").map(|val| val.parse()) 191 | .transpose().context("Invalid param2 value.")?, 192 | }) 193 | } 194 | 195 | 196 | fn print_editing_status(done: usize, total: usize, real_start: Instant, 197 | eta_start: Instant, show_progress: bool) 198 | { 199 | let now = Instant::now(); 200 | let real_elapsed = now.duration_since(real_start); 201 | 202 | if show_progress { 203 | let eta_elapsed = now.duration_since(eta_start); 204 | let progress = match total { 205 | 0 => 0., 206 | _ => done as f32 / total as f32 207 | }; 208 | 209 | let remaining = if progress >= 0.1 { 210 | Some(Duration::from_secs_f32( 211 | eta_elapsed.as_secs_f32() / progress * (1. - progress) 212 | )) 213 | } else { 214 | None 215 | }; 216 | 217 | const TOTAL_BARS: usize = 25; 218 | let num_bars = (progress * TOTAL_BARS as f32) as usize; 219 | let bars = "=".repeat(num_bars); 220 | 221 | print!( 222 | "\r[{bars:>() 245 | .join(&format!( "\n{}", " ".repeat(prefix.len()) )); 246 | println!("{}{}", prefix, indented); 247 | } 248 | 249 | 250 | fn get_confirmation() -> bool { 251 | print!("Proceed? (Y/n): "); 252 | std::io::stdout().flush().unwrap(); 253 | let mut result = String::new(); 254 | std::io::stdin().read_line(&mut result).unwrap(); 255 | result.trim().to_ascii_lowercase() == "y" 256 | } 257 | 258 | 259 | pub fn run_cmd_line() { 260 | use std::sync::mpsc; 261 | use crate::instance::{InstState, ServerEvent, spawn_compute_thread}; 262 | 263 | let args = match parse_cmd_line_args() { 264 | Ok(a) => a, 265 | Err(e) => { 266 | print_log(LogType::Error, e.to_string()); 267 | return; 268 | } 269 | }; 270 | let (handle, status) = spawn_compute_thread(args); 271 | 272 | const TICK: Duration = Duration::from_millis(25); 273 | const UPDATE_INTERVAL: Duration = Duration::from_millis(500); 274 | 275 | let mut last_update = Instant::now(); 276 | let mut querying_start = last_update; 277 | let mut editing_start = last_update; 278 | let mut cur_state = InstState::Ignore; 279 | let mut need_newline = false; 280 | 281 | let newline_if = |condition: &mut bool| { 282 | if *condition { 283 | println!(); 284 | *condition = false; 285 | } 286 | }; 287 | 288 | loop { /* Main command-line logging loop */ 289 | let now = Instant::now(); 290 | let mut forced_update = InstState::Ignore; 291 | 292 | match status.receiver().recv_timeout(TICK) { 293 | Ok(event) => match event { 294 | ServerEvent::Log(log_type, msg) => { 295 | newline_if(&mut need_newline); 296 | print_log(log_type, msg); 297 | }, 298 | ServerEvent::NewState(new_state) => { 299 | // Force progress updates at the beginning and end of 300 | // querying/editing stages. 301 | if (cur_state == InstState::Ignore) != 302 | (new_state == InstState::Ignore) 303 | { 304 | forced_update = 305 | if cur_state == InstState::Ignore { new_state } 306 | else { cur_state }; 307 | } 308 | if new_state == InstState::Querying { 309 | // Store time for determining elapsed time. 310 | querying_start = now; 311 | } else if new_state == InstState::Editing { 312 | // Store start time for determining ETA. 313 | editing_start = now; 314 | } 315 | cur_state = new_state; 316 | }, 317 | ServerEvent::ConfirmRequest => { 318 | newline_if(&mut need_newline); 319 | status.send_confirmation(get_confirmation()); 320 | }, 321 | }, 322 | Err(err) => { 323 | // Compute thread has exited; break out of the loop. 324 | if err == mpsc::RecvTimeoutError::Disconnected { 325 | break; 326 | } 327 | } 328 | } 329 | 330 | let timed_update_ready = now >= last_update + UPDATE_INTERVAL; 331 | 332 | if forced_update == InstState::Querying 333 | || (cur_state == InstState::Querying && timed_update_ready) 334 | { 335 | print!("\rQuerying mapblocks... {} found.", 336 | status.get_status().blocks_total); 337 | std::io::stdout().flush().unwrap(); 338 | last_update = now; 339 | need_newline = true; 340 | } 341 | else if forced_update == InstState::Editing 342 | || (cur_state == InstState::Editing && timed_update_ready) 343 | { 344 | let s = status.get_status(); 345 | print_editing_status(s.blocks_done, s.blocks_total, 346 | querying_start, editing_start, s.show_progress); 347 | last_update = now; 348 | need_newline = true; 349 | } 350 | 351 | // Print a newline after the last querying/editing message. 352 | if cur_state == InstState::Ignore { 353 | newline_if(&mut need_newline); 354 | } 355 | } 356 | 357 | let _ = handle.join(); 358 | } 359 | -------------------------------------------------------------------------------- /src/commands/clone.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult, BLOCK_CACHE_SIZE}; 2 | 3 | use crate::{unwrap_or, opt_unwrap_or}; 4 | use crate::spatial::{Vec3, Area, MAP_LIMIT}; 5 | use crate::map_database::MapDatabase; 6 | use crate::map_block::{MapBlock, MapBlockError, is_valid_generated}; 7 | use crate::block_utils::{merge_blocks, merge_metadata, clean_name_id_map}; 8 | use crate::instance::{ArgType, InstBundle, InstArgs}; 9 | use crate::utils::{CacheMap, query_keys}; 10 | 11 | 12 | fn verify_args(args: &InstArgs) -> ArgResult { 13 | let map_area = Area::new( 14 | Vec3::new(-MAP_LIMIT, -MAP_LIMIT, -MAP_LIMIT), 15 | Vec3::new(MAP_LIMIT, MAP_LIMIT, MAP_LIMIT) 16 | ); 17 | 18 | if map_area.intersection(args.area.unwrap() + args.offset.unwrap()) 19 | .is_none() 20 | { 21 | return ArgResult::error("Destination area is outside map bounds."); 22 | } 23 | 24 | ArgResult::Ok 25 | } 26 | 27 | 28 | type BlockResult = Option>; 29 | 30 | fn get_cached( 31 | db: &mut MapDatabase, 32 | cache: &mut CacheMap, 33 | key: i64 34 | ) -> BlockResult { 35 | match cache.get(&key) { 36 | Some(data) => data.clone(), 37 | None => { 38 | let block = db.get_block(key).ok() 39 | .filter(|d| is_valid_generated(d)) 40 | .map(|d| MapBlock::deserialize(&d)); 41 | cache.insert(key, block.clone()); 42 | block 43 | } 44 | } 45 | } 46 | 47 | 48 | fn clone(inst: &mut InstBundle) { 49 | let src_area = inst.args.area.unwrap(); 50 | let offset = inst.args.offset.unwrap(); 51 | let dst_area = src_area + offset; 52 | let mut dst_keys = query_keys(&mut inst.db, &inst.status, 53 | &[], Some(dst_area), false, true); 54 | 55 | // Sort blocks according to offset such that we don't read blocks that 56 | // have already been written. 57 | let sort_dir = offset.map(|v| if v > 0 { -1 } else { 1 }); 58 | // Subtract one from inverted axes to keep values from overflowing. 59 | let sort_offset = sort_dir.map(|v| if v == -1 { -1 } else { 0 }); 60 | 61 | dst_keys.sort_unstable_by_key(|k| { 62 | (Vec3::from_block_key(*k) * sort_dir + sort_offset).to_block_key() 63 | }); 64 | 65 | let mut block_cache = CacheMap::with_capacity(BLOCK_CACHE_SIZE); 66 | inst.status.begin_editing(); 67 | 68 | for dst_key in dst_keys { 69 | inst.status.inc_done(); 70 | 71 | let mut dst_block = unwrap_or!( 72 | opt_unwrap_or!( 73 | get_cached(&mut inst.db, &mut block_cache, dst_key), 74 | continue 75 | ), 76 | { inst.status.inc_failed(); continue; } 77 | ); 78 | 79 | let dst_pos = Vec3::from_block_key(dst_key); 80 | let dst_part_abs = dst_area.abs_block_overlap(dst_pos).unwrap(); 81 | let src_part_abs = dst_part_abs - offset; 82 | let src_blocks_needed = src_part_abs.to_touching_block_area(); 83 | 84 | for src_pos in &src_blocks_needed { 85 | if !src_pos.is_valid_block_pos() { 86 | continue; 87 | } 88 | let src_key = src_pos.to_block_key(); 89 | // Continue if a None or Some(Err) value is retrieved. 90 | let src_block = opt_unwrap_or!( 91 | get_cached(&mut inst.db, &mut block_cache, src_key) 92 | .map(|res| res.ok()).flatten(), 93 | continue 94 | ); 95 | 96 | let src_frag_abs = src_part_abs.abs_block_overlap(src_pos) 97 | .unwrap(); 98 | let src_frag_rel = src_frag_abs - src_pos * 16; 99 | let dst_frag_rel = (src_frag_abs + offset) 100 | .rel_block_overlap(dst_pos).unwrap(); 101 | 102 | merge_blocks(&src_block, &mut dst_block, 103 | src_frag_rel, dst_frag_rel); 104 | merge_metadata(&src_block.metadata, &mut dst_block.metadata, 105 | src_frag_rel, dst_frag_rel); 106 | } 107 | 108 | clean_name_id_map(&mut dst_block); 109 | inst.db.set_block(dst_key, &dst_block.serialize()).unwrap(); 110 | } 111 | 112 | inst.status.end_editing(); 113 | } 114 | 115 | 116 | pub fn get_command() -> Command { 117 | Command { 118 | func: clone, 119 | verify_args: Some(verify_args), 120 | args: vec![ 121 | (ArgType::Area(true), "Area to clone"), 122 | (ArgType::Offset(true), "Vector to shift the area's contents by") 123 | ], 124 | help: "Clone (copy) the contents of an area to a new location." 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/commands/delete_blocks.rs: -------------------------------------------------------------------------------- 1 | use super::Command; 2 | 3 | use crate::instance::{ArgType, InstBundle}; 4 | use crate::utils::query_keys; 5 | 6 | 7 | fn delete_blocks(inst: &mut InstBundle) { 8 | let keys = query_keys(&mut inst.db, &inst.status, 9 | &[], inst.args.area, inst.args.invert, false); 10 | inst.status.begin_editing(); 11 | 12 | for key in keys { 13 | inst.status.inc_done(); 14 | inst.db.delete_block(key).unwrap(); 15 | } 16 | 17 | inst.status.end_editing(); 18 | } 19 | 20 | 21 | pub fn get_command() -> Command { 22 | Command { 23 | func: delete_blocks, 24 | verify_args: None, 25 | args: vec![ 26 | (ArgType::Area(true), "Area containing mapblocks to delete"), 27 | (ArgType::Invert, 28 | "Delete all mapblocks fully *outside* the given area.") 29 | ], 30 | help: "Delete all mapblocks inside or outside an area." 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/delete_meta.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult}; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::Vec3; 5 | use crate::instance::{ArgType, InstArgs, InstBundle}; 6 | use crate::map_block::MapBlock; 7 | use crate::utils::{query_keys, to_bytes, to_slice, fmt_big_num}; 8 | 9 | 10 | fn verify_args(args: &InstArgs) -> ArgResult { 11 | if !args.area.is_some() && !args.node.is_some() { 12 | return ArgResult::warning( 13 | "No area or node specified. ALL metadata will be deleted!"); 14 | } 15 | 16 | ArgResult::Ok 17 | } 18 | 19 | 20 | fn delete_metadata(inst: &mut InstBundle) { 21 | let node = inst.args.node.as_ref().map(to_bytes); 22 | 23 | let keys = query_keys(&mut inst.db, &mut inst.status, 24 | to_slice(&node), inst.args.area, inst.args.invert, true); 25 | 26 | inst.status.begin_editing(); 27 | let mut count: u64 = 0; 28 | 29 | for key in keys { 30 | inst.status.inc_done(); 31 | let data = inst.db.get_block(key).unwrap(); 32 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 33 | { inst.status.inc_failed(); continue; }); 34 | 35 | let node_id = node.as_deref().and_then(|n| block.nimap.get_id(n)); 36 | if node.is_some() && node_id.is_none() { 37 | continue; // Block doesn't contain the required node. 38 | } 39 | 40 | let block_corner = Vec3::from_block_key(key) * 16; 41 | let mut to_delete = Vec::with_capacity(block.metadata.len()); 42 | 43 | for (&idx, _) in &block.metadata { 44 | let abs_pos = Vec3::from_u16_key(idx) + block_corner; 45 | 46 | if let Some(a) = inst.args.area { 47 | if a.contains(abs_pos) == inst.args.invert { 48 | continue; 49 | } 50 | } 51 | if let Some(id) = node_id { 52 | if block.node_data.nodes[idx as usize] != id { 53 | continue; 54 | } 55 | } 56 | 57 | to_delete.push(idx); 58 | } 59 | 60 | if !to_delete.is_empty() { 61 | for idx in &to_delete { 62 | block.metadata.remove(idx); 63 | } 64 | count += to_delete.len() as u64; 65 | inst.db.set_block(key, &block.serialize()).unwrap(); 66 | } 67 | } 68 | 69 | inst.status.end_editing(); 70 | inst.status.log_info( 71 | format!("Deleted metadata from {} nodes.", fmt_big_num(count))); 72 | } 73 | 74 | 75 | pub fn get_command() -> Command { 76 | Command { 77 | func: delete_metadata, 78 | verify_args: Some(verify_args), 79 | args: vec![ 80 | (ArgType::Node(false), "Name of node to delete metadata from"), 81 | (ArgType::Area(false), "Area in which to delete metadata"), 82 | (ArgType::Invert, "Delete metadata *outside* the given area."), 83 | ], 84 | help: "Delete node metadata of certain nodes." 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/commands/delete_objects.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult}; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::Area; 5 | use crate::instance::{ArgType, InstArgs, InstBundle}; 6 | use crate::map_block::{MapBlock, StaticObject, LuaEntityData}; 7 | use crate::utils::{query_keys, to_bytes, to_slice, fmt_big_num}; 8 | 9 | use memmem::{Searcher, TwoWaySearcher}; 10 | 11 | const ITEM_ENT_NAME: &[u8] = b"__builtin:item"; 12 | const ITEM_NAME_PAT_OLD: &[u8] = b"[\"itemstring\"] = \""; 13 | const ITEM_NAME_PAT_NEW: &[u8] = b"itemstring=\""; 14 | 15 | thread_local! { 16 | static ITEM_NAME_SEARCHER_OLD: TwoWaySearcher<'static> = 17 | TwoWaySearcher::new(ITEM_NAME_PAT_OLD); 18 | static ITEM_NAME_SEARCHER_NEW: TwoWaySearcher<'static> = 19 | TwoWaySearcher::new(ITEM_NAME_PAT_NEW); 20 | } 21 | 22 | 23 | fn verify_args(args: &InstArgs) -> ArgResult { 24 | if args.object.is_some() && args.items.is_some() { 25 | return ArgResult::error("Cannot use both --obj and --items."); 26 | } 27 | ArgResult::Ok 28 | } 29 | 30 | 31 | #[inline] 32 | fn get_item_name_start(data: &[u8]) -> Option { 33 | if let Some(idx) = ITEM_NAME_SEARCHER_NEW.with(|s| s.search_in(data)) { 34 | Some(idx + ITEM_NAME_PAT_NEW.len()) 35 | } else if let Some(idx) = ITEM_NAME_SEARCHER_OLD.with(|s| s.search_in(data)) { 36 | Some(idx + ITEM_NAME_PAT_OLD.len()) 37 | } else { 38 | None 39 | } 40 | } 41 | 42 | 43 | #[inline] 44 | fn get_item_name<'a>(data: &'a [u8]) -> &'a[u8] { 45 | if data.starts_with(b"return") { 46 | let item_name_start = get_item_name_start(data); 47 | if let Some(idx) = item_name_start { 48 | let name = &data[idx..].split(|&c| c == b' ' || c == b'"').next(); 49 | if let Some(n) = name { 50 | return n; 51 | } 52 | } 53 | b"" 54 | } else { 55 | data 56 | } 57 | } 58 | 59 | 60 | fn can_delete( 61 | obj: &StaticObject, 62 | area: &Option, 63 | invert: bool, 64 | obj_name: &Option>, 65 | item_names: &[Vec] 66 | ) -> bool { 67 | // Check area requirement 68 | if let Some(a) = area { 69 | const DIV_FAC: i32 = 10_000; 70 | let rounded_pos = obj.f_pos 71 | .map(|v| (v + DIV_FAC / 2).div_euclid(DIV_FAC)); 72 | if a.contains(rounded_pos) == invert { 73 | return false; // Object not included in area. 74 | } 75 | } 76 | 77 | // Check name requirements 78 | if let Some(name) = obj_name { 79 | if let Ok(le_data) = LuaEntityData::deserialize(obj) { 80 | if &le_data.name != name { 81 | return false; // Object name does not match. 82 | } 83 | 84 | if !item_names.is_empty() { 85 | let item_name = get_item_name(&le_data.data); 86 | if !item_names.iter().any(|n| n == item_name) { 87 | // Item entity's item name does not match. 88 | return false 89 | } 90 | } 91 | } else { 92 | return false; // Keep invalid or unsupported objects. 93 | } 94 | } 95 | 96 | true // Delete if all tests pass. 97 | } 98 | 99 | 100 | fn delete_objects(inst: &mut InstBundle) { 101 | let obj_name = if inst.args.items.is_some() { 102 | Some(ITEM_ENT_NAME.to_owned()) 103 | } else { 104 | inst.args.object.as_ref().map(to_bytes) 105 | }; 106 | 107 | let item_names: Vec<_> = inst.args.items.as_ref().unwrap_or(&Vec::new()) 108 | .iter().map(to_bytes).collect(); 109 | 110 | let keys = query_keys(&mut inst.db, &mut inst.status, 111 | to_slice(&obj_name), inst.args.area, inst.args.invert, true); 112 | 113 | inst.status.begin_editing(); 114 | let mut count: u64 = 0; 115 | for key in keys { 116 | inst.status.inc_done(); 117 | let data = inst.db.get_block(key).unwrap(); 118 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 119 | { inst.status.inc_failed(); continue; }); 120 | 121 | let mut modified = false; 122 | for i in (0 .. block.static_objects.len()).rev() { 123 | if can_delete( 124 | &block.static_objects[i], 125 | &inst.args.area, 126 | inst.args.invert, 127 | &obj_name, 128 | &item_names 129 | ) { 130 | block.static_objects.remove(i); 131 | modified = true; 132 | count += 1; 133 | } 134 | } 135 | 136 | if modified { 137 | inst.db.set_block(key, &block.serialize()).unwrap(); 138 | } 139 | } 140 | 141 | inst.status.end_editing(); 142 | inst.status.log_info(format!("Deleted {} objects.", fmt_big_num(count))); 143 | } 144 | 145 | 146 | pub fn get_command() -> Command { 147 | Command { 148 | func: delete_objects, 149 | verify_args: Some(verify_args), 150 | args: vec![ 151 | (ArgType::Object, "Name of object to delete"), 152 | (ArgType::Items, 153 | "Delete only item entities. Optionally list one or more item \ 154 | names after `--items` to delete only those items."), 155 | (ArgType::Area(false), "Area in which to delete objects"), 156 | (ArgType::Invert, "Delete objects *outside* the given area."), 157 | ], 158 | help: "Delete certain objects and/or item entities." 159 | } 160 | } 161 | 162 | 163 | #[cfg(test)] 164 | mod tests { 165 | use super::*; 166 | 167 | #[test] 168 | fn test_delete_objects() { 169 | let pairs: &[(&[u8], &[u8])] = &[ 170 | (b"default:glass", b"default:glass"), 171 | (b"return {}", b""), 172 | (b"return {[\"itemstring\"] = \"\", [\"age\"] = 100}", b""), 173 | (b"return {[\"itemstring\"] = \"mod:item\"}", b"mod:item"), 174 | (b"return {[\"age\"] = 400, [\"itemstring\"] = \"one:two 99 32\"}", 175 | b"one:two"), 176 | (b"return {itemstring=\"\",age=100}", b""), 177 | (b"return {itemstring=\"mod:item\"}", b"mod:item"), 178 | (b"return {age=400,itemstring=\"one:two 99 32\"}", b"one:two"), 179 | ]; 180 | for &(data, name) in pairs { 181 | assert_eq!(get_item_name(data), name); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/commands/delete_timers.rs: -------------------------------------------------------------------------------- 1 | use super::Command; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::Vec3; 5 | use crate::instance::{ArgType, InstBundle}; 6 | use crate::map_block::MapBlock; 7 | use crate::utils::{query_keys, to_bytes, to_slice, fmt_big_num}; 8 | 9 | 10 | fn delete_timers(inst: &mut InstBundle) { 11 | let node = inst.args.node.as_ref().map(to_bytes); 12 | 13 | let keys = query_keys(&mut inst.db, &mut inst.status, 14 | to_slice(&node), inst.args.area, inst.args.invert, true); 15 | 16 | inst.status.begin_editing(); 17 | let mut count: u64 = 0; 18 | 19 | for key in keys { 20 | inst.status.inc_done(); 21 | let data = inst.db.get_block(key).unwrap(); 22 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 23 | { inst.status.inc_failed(); continue; }); 24 | 25 | let node_id = node.as_deref().and_then(|n| block.nimap.get_id(n)); 26 | if node.is_some() && node_id.is_none() { 27 | continue; // Block doesn't contain the required node. 28 | } 29 | 30 | let block_corner = Vec3::from_block_key(key) * 16; 31 | let mut modified = false; 32 | 33 | for i in (0..block.node_timers.len()).rev() { 34 | let pos_idx = block.node_timers[i].pos; 35 | let pos = Vec3::from_u16_key(pos_idx); 36 | let abs_pos = pos + block_corner; 37 | 38 | if let Some(a) = inst.args.area { 39 | if a.contains(abs_pos) == inst.args.invert { 40 | continue; 41 | } 42 | } 43 | if let Some(id) = node_id { 44 | if block.node_data.nodes[pos_idx as usize] != id { 45 | continue; 46 | } 47 | } 48 | 49 | block.node_timers.remove(i); 50 | count += 1; 51 | modified = true; 52 | } 53 | 54 | if modified { 55 | inst.db.set_block(key, &block.serialize()).unwrap(); 56 | } 57 | } 58 | 59 | inst.status.end_editing(); 60 | inst.status.log_info( 61 | format!("Deleted {} node timers.", fmt_big_num(count))); 62 | } 63 | 64 | 65 | pub fn get_command() -> Command { 66 | Command { 67 | func: delete_timers, 68 | verify_args: None, 69 | args: vec![ 70 | (ArgType::Node(false), "Name of node to delete node timers from"), 71 | (ArgType::Area(false), "Area in which to delete node timers"), 72 | (ArgType::Invert, "Delete node timers *outside* the given area."), 73 | ], 74 | help: "Delete node timers of certain nodes." 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/fill.rs: -------------------------------------------------------------------------------- 1 | use super::Command; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::{Vec3, Area, InverseBlockIterator}; 5 | use crate::instance::{ArgType, InstBundle}; 6 | use crate::map_block::MapBlock; 7 | use crate::block_utils::clean_name_id_map; 8 | use crate::utils::{query_keys, to_bytes, fmt_big_num}; 9 | 10 | 11 | fn fill_area(block: &mut MapBlock, id: u16, area: Area, invert: bool) { 12 | if invert { 13 | for i in InverseBlockIterator::new(area) { 14 | block.node_data.nodes[i] = id; 15 | } 16 | } else { 17 | for z in area.min.z ..= area.max.z { 18 | let z_start = z * 256; 19 | for y in area.min.y ..= area.max.y { 20 | let zy_start = z_start + y * 16; 21 | for x in area.min.x ..= area.max.x { 22 | block.node_data.nodes[(zy_start + x) as usize] = id; 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | 30 | fn fill(inst: &mut InstBundle) { 31 | let area = inst.args.area.unwrap(); 32 | let node = to_bytes(inst.args.new_node.as_ref().unwrap()); 33 | 34 | let keys = query_keys(&mut inst.db, &mut inst.status, 35 | &[], Some(area), inst.args.invert, true); 36 | 37 | inst.status.begin_editing(); 38 | 39 | let mut count: u64 = 0; 40 | for key in keys { 41 | inst.status.inc_done(); 42 | 43 | let pos = Vec3::from_block_key(key); 44 | let data = inst.db.get_block(key).unwrap(); 45 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 46 | { inst.status.inc_failed(); continue; }); 47 | 48 | if area.contains_block(pos) != area.touches_block(pos) { 49 | // Fill part of block 50 | let block_part = area.rel_block_overlap(pos).unwrap(); 51 | let fill_id = block.nimap.get_id(&node).unwrap_or_else(|| { 52 | let next = block.nimap.get_max_id().unwrap() + 1; 53 | block.nimap.0.insert(next, node.to_vec()); 54 | next 55 | }); 56 | fill_area(&mut block, fill_id, block_part, inst.args.invert); 57 | clean_name_id_map(&mut block); 58 | count += block_part.volume(); 59 | } else { // Fill entire block 60 | block.node_data.nodes.fill(0); 61 | block.nimap.0.clear(); 62 | block.nimap.0.insert(0, node.to_vec()); 63 | count += block.node_data.nodes.len() as u64; 64 | } 65 | 66 | inst.db.set_block(key, &block.serialize()).unwrap(); 67 | } 68 | 69 | inst.status.end_editing(); 70 | inst.status.log_info(format!("{} nodes filled.", fmt_big_num(count))); 71 | } 72 | 73 | 74 | pub fn get_command() -> Command { 75 | Command { 76 | func: fill, 77 | verify_args: None, 78 | args: vec![ 79 | (ArgType::Area(true), "Area to fill"), 80 | (ArgType::Invert, 81 | "Fill all generated nodes *outside* the given area."), 82 | (ArgType::NewNode, "Name of the node to fill with"), 83 | ], 84 | help: "Set all nodes inside or outside an area." 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::instance::{ArgType, InstArgs, InstBundle}; 4 | 5 | mod clone; 6 | mod delete_blocks; 7 | mod delete_meta; 8 | mod delete_objects; 9 | mod delete_timers; 10 | mod fill; 11 | mod overlay; 12 | mod replace_in_inv; 13 | mod replace_nodes; 14 | mod set_meta_var; 15 | mod set_param2; 16 | mod vacuum; 17 | 18 | 19 | pub const BLOCK_CACHE_SIZE: usize = 1024; 20 | 21 | 22 | pub enum ArgResult { 23 | Ok, 24 | Warning(String), 25 | Error(String), 26 | } 27 | 28 | impl ArgResult { 29 | /// Create a new ArgResult::Warning from a &str. 30 | #[inline] 31 | pub fn warning(msg: &str) -> Self { 32 | Self::Warning(msg.to_string()) 33 | } 34 | 35 | /// Create a new ArgResult::Error from a &str. 36 | #[inline] 37 | pub fn error(msg: &str) -> Self { 38 | Self::Error(msg.to_string()) 39 | } 40 | } 41 | 42 | 43 | pub struct Command { 44 | pub func: fn(&mut InstBundle), 45 | pub verify_args: Option ArgResult>, 46 | pub help: &'static str, 47 | pub args: Vec<(ArgType, &'static str)> 48 | } 49 | 50 | 51 | pub fn get_commands() -> BTreeMap<&'static str, Command> { 52 | let mut commands = BTreeMap::new(); 53 | macro_rules! new_cmd { 54 | ($name:expr, $module:ident) => { 55 | commands.insert($name, $module::get_command()) 56 | } 57 | } 58 | 59 | new_cmd!("clone", clone); 60 | new_cmd!("deleteblocks", delete_blocks); 61 | new_cmd!("deletemeta", delete_meta); 62 | new_cmd!("deleteobjects", delete_objects); 63 | new_cmd!("deletetimers", delete_timers); 64 | new_cmd!("fill", fill); 65 | new_cmd!("replacenodes", replace_nodes); 66 | new_cmd!("replaceininv", replace_in_inv); 67 | new_cmd!("overlay", overlay); 68 | new_cmd!("setmetavar", set_meta_var); 69 | new_cmd!("setparam2", set_param2); 70 | new_cmd!("vacuum", vacuum); 71 | 72 | commands 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/overlay.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult, BLOCK_CACHE_SIZE}; 2 | 3 | use crate::{unwrap_or, opt_unwrap_or}; 4 | use crate::spatial::{Vec3, Area, MAP_LIMIT}; 5 | use crate::instance::{ArgType, InstArgs, InstBundle}; 6 | use crate::map_database::MapDatabase; 7 | use crate::map_block::{MapBlock, MapBlockError, is_valid_generated}; 8 | use crate::block_utils::{merge_blocks, merge_metadata, clean_name_id_map}; 9 | use crate::utils::{query_keys, CacheMap}; 10 | 11 | 12 | fn verify_args(args: &InstArgs) -> ArgResult { 13 | if args.invert 14 | && args.offset.filter(|&ofs| ofs != Vec3::new(0, 0, 0)).is_some() 15 | { 16 | return ArgResult::error("Inverted selections cannot be offset."); 17 | } 18 | 19 | let offset = args.offset.unwrap_or(Vec3::new(0, 0, 0)); 20 | let map_area = Area::new( 21 | Vec3::new(-MAP_LIMIT, -MAP_LIMIT, -MAP_LIMIT), 22 | Vec3::new(MAP_LIMIT, MAP_LIMIT, MAP_LIMIT) 23 | ); 24 | 25 | if map_area.intersection(args.area.unwrap_or(map_area) + offset) 26 | .is_none() 27 | { 28 | return ArgResult::error("Destination area is outside map bounds."); 29 | } 30 | 31 | ArgResult::Ok 32 | } 33 | 34 | 35 | /// Overlay without offsetting anything. 36 | /// 37 | /// Possible argument configurations: 38 | /// - No arguments (copy everything) 39 | /// - Area 40 | /// - Area + Invert 41 | #[inline] 42 | fn overlay_no_offset(inst: &mut InstBundle) { 43 | let db = &mut inst.db; 44 | let idb = inst.idb.as_mut().unwrap(); 45 | let invert = inst.args.invert; 46 | 47 | // Get keys from input database. 48 | let keys = query_keys(idb, &inst.status, 49 | &[], inst.args.area, invert, true); 50 | inst.status.begin_editing(); 51 | 52 | for key in keys { 53 | inst.status.inc_done(); 54 | 55 | if let Some(area) = inst.args.area { 56 | let pos = Vec3::from_block_key(key); 57 | 58 | if (!invert && area.contains_block(pos)) 59 | || (invert && !area.touches_block(pos)) 60 | { // If possible, copy whole mapblock. 61 | let data = idb.get_block(key).unwrap(); 62 | if is_valid_generated(&data) { 63 | db.set_block(key, &data).unwrap(); 64 | } 65 | } else { // Copy part of mapblock 66 | let res = || -> Result<(), MapBlockError> { 67 | let dst_data = opt_unwrap_or!( 68 | db.get_block(key).ok() 69 | .filter(|d| is_valid_generated(&d)), 70 | return Ok(())); 71 | let src_data = idb.get_block(key).unwrap(); 72 | 73 | let mut src_block = MapBlock::deserialize(&src_data)?; 74 | let mut dst_block = MapBlock::deserialize(&dst_data)?; 75 | 76 | let block_part = area.rel_block_overlap(pos).unwrap(); 77 | if invert { 78 | // For inverted selections, reverse the order of the 79 | // overlay operations. 80 | merge_blocks(&dst_block, &mut src_block, 81 | block_part, block_part); 82 | merge_metadata(&dst_block.metadata, &mut src_block.metadata, 83 | block_part, block_part); 84 | clean_name_id_map(&mut src_block); 85 | db.set_block(key, &src_block.serialize()).unwrap(); 86 | } else { 87 | merge_blocks(&src_block, &mut dst_block, 88 | block_part, block_part); 89 | merge_metadata(&src_block.metadata, &mut dst_block.metadata, 90 | block_part, block_part); 91 | clean_name_id_map(&mut dst_block); 92 | db.set_block(key, &dst_block.serialize()).unwrap(); 93 | } 94 | Ok(()) 95 | }(); 96 | 97 | if res.is_err() { 98 | inst.status.inc_failed() 99 | } 100 | } 101 | } else { 102 | // No area; copy whole mapblock. 103 | let data = idb.get_block(key).unwrap(); 104 | if is_valid_generated(&data) { 105 | db.set_block(key, &data).unwrap(); 106 | } 107 | } 108 | } 109 | 110 | inst.status.end_editing(); 111 | } 112 | 113 | 114 | fn get_cached( 115 | db: &mut MapDatabase, 116 | cache: &mut CacheMap>, 117 | key: i64 118 | ) -> Option { 119 | match cache.get(&key) { 120 | Some(data) => data.clone(), 121 | None => { 122 | let block = db.get_block(key).ok() 123 | .filter(|d| is_valid_generated(d)) 124 | .and_then(|d| MapBlock::deserialize(&d).ok()); 125 | cache.insert(key, block.clone()); 126 | block 127 | } 128 | } 129 | } 130 | 131 | 132 | /// Overlay with offset, with or without area. 133 | #[inline] 134 | fn overlay_with_offset(inst: &mut InstBundle) { 135 | let offset = inst.args.offset.unwrap(); 136 | let src_area = inst.args.area; 137 | let dst_area = src_area.map(|a| a + offset); 138 | let idb = inst.idb.as_mut().unwrap(); 139 | 140 | // Get keys from output database. 141 | let dst_keys = query_keys(&mut inst.db, &inst.status, 142 | &[], dst_area, inst.args.invert, true); 143 | 144 | let mut src_block_cache = CacheMap::with_capacity(BLOCK_CACHE_SIZE); 145 | 146 | inst.status.begin_editing(); 147 | for dst_key in dst_keys { 148 | inst.status.inc_done(); 149 | 150 | let dst_pos = Vec3::from_block_key(dst_key); 151 | let dst_data = opt_unwrap_or!( 152 | inst.db.get_block(dst_key).ok().filter(|d| is_valid_generated(d)), 153 | continue 154 | ); 155 | let mut dst_block = unwrap_or!( 156 | MapBlock::deserialize(&dst_data), 157 | { inst.status.inc_failed(); continue; } 158 | ); 159 | 160 | let dst_part_abs = dst_area.map_or( 161 | // If no area is given, the destination part is the whole mapblock. 162 | Area::new(dst_pos * 16, dst_pos * 16 + 15), 163 | |a| a.abs_block_overlap(dst_pos).unwrap() 164 | ); 165 | let src_part_abs = dst_part_abs - offset; 166 | let src_blocks_needed = src_part_abs.to_touching_block_area(); 167 | 168 | for src_pos in &src_blocks_needed { 169 | if !src_pos.is_valid_block_pos() { 170 | continue; 171 | } 172 | let src_key = src_pos.to_block_key(); 173 | let src_block = opt_unwrap_or!( 174 | get_cached(idb, &mut src_block_cache, src_key), 175 | continue 176 | ); 177 | 178 | let src_frag_abs = src_part_abs.abs_block_overlap(src_pos) 179 | .unwrap(); 180 | let src_frag_rel = src_frag_abs - src_pos * 16; 181 | let dst_frag_rel = (src_frag_abs + offset) 182 | .rel_block_overlap(dst_pos).unwrap(); 183 | 184 | merge_blocks(&src_block, &mut dst_block, 185 | src_frag_rel, dst_frag_rel); 186 | merge_metadata(&src_block.metadata, &mut dst_block.metadata, 187 | src_frag_rel, dst_frag_rel); 188 | } 189 | 190 | clean_name_id_map(&mut dst_block); 191 | inst.db.set_block(dst_key, &dst_block.serialize()).unwrap(); 192 | } 193 | 194 | inst.status.end_editing(); 195 | } 196 | 197 | 198 | fn overlay(inst: &mut InstBundle) { 199 | let offset = inst.args.offset.unwrap_or(Vec3::new(0, 0, 0)); 200 | if offset == Vec3::new(0, 0, 0) { 201 | overlay_no_offset(inst); 202 | } else { 203 | overlay_with_offset(inst); 204 | } 205 | } 206 | 207 | 208 | pub fn get_command() -> Command { 209 | Command { 210 | func: overlay, 211 | verify_args: Some(verify_args), 212 | args: vec![ 213 | (ArgType::InputMapPath, "Path to the source map/world"), 214 | (ArgType::Area(false), "Area to copy from. If not specified, \ 215 | everything from the source map will be copied."), 216 | (ArgType::Invert, "Copy everything *outside* the given area."), 217 | (ArgType::Offset(false), "Vector to shift nodes by when copying"), 218 | ], 219 | help: "Copy part or all of a source map into the main map." 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/commands/replace_in_inv.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult}; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::Vec3; 5 | use crate::instance::{ArgType, InstArgs, InstBundle}; 6 | use crate::map_block::MapBlock; 7 | use crate::utils::{query_keys, to_bytes, fmt_big_num}; 8 | 9 | 10 | fn do_replace(inv: &mut Vec, item: &[u8], new_item: &[u8], del_meta: bool) 11 | -> u64 12 | { 13 | const NEWLINE: u8 = b'\n'; 14 | const SPACE: u8 = b' '; 15 | 16 | let delete = new_item.is_empty(); 17 | let mut new_inv = Vec::new(); 18 | let mut mods = 0; 19 | 20 | for line in inv.split(|&x| x == NEWLINE) { 21 | if line.is_empty() { 22 | // Necessary because of newline after final EndInventory 23 | continue; 24 | } 25 | // Max 5 parts: Item 26 | let mut parts = line.splitn(5, |&x| x == SPACE); 27 | if parts.next() == Some(b"Item") && parts.next() == Some(item) { 28 | if delete { 29 | new_inv.extend_from_slice(b"Empty"); 30 | } else { 31 | new_inv.extend_from_slice(b"Item "); 32 | new_inv.extend_from_slice(new_item); 33 | 34 | if del_meta { // Only re-serialize necessary parts 35 | let count = parts.next().unwrap_or(b"1"); 36 | let wear = parts.next().unwrap_or(b"0"); 37 | if count != b"1" || wear != b"0" { 38 | new_inv.push(SPACE); 39 | new_inv.extend_from_slice(count); 40 | } 41 | if wear != b"0" { 42 | new_inv.push(SPACE); 43 | new_inv.extend_from_slice(wear); 44 | } 45 | } else { 46 | for part in parts { 47 | new_inv.push(SPACE); 48 | new_inv.extend_from_slice(part); 49 | } 50 | } 51 | } 52 | mods += 1; 53 | } else { 54 | new_inv.extend_from_slice(line); 55 | } 56 | new_inv.push(NEWLINE); 57 | } 58 | 59 | if mods > 0 { 60 | *inv = new_inv; 61 | } 62 | mods 63 | } 64 | 65 | 66 | fn replace_in_inv(inst: &mut InstBundle) { 67 | let item = to_bytes(inst.args.item.as_ref().unwrap()); 68 | let new_item = inst.args.new_item.as_ref().map(to_bytes) 69 | .unwrap_or(if inst.args.delete { vec![] } else { item.clone() }); 70 | 71 | let nodes: Vec<_> = inst.args.nodes.iter().map(to_bytes).collect(); 72 | let keys = query_keys(&mut inst.db, &mut inst.status, 73 | &nodes, inst.args.area, inst.args.invert, true); 74 | 75 | inst.status.begin_editing(); 76 | let mut item_mods: u64 = 0; 77 | let mut node_mods: u64 = 0; 78 | 79 | for key in keys { 80 | inst.status.inc_done(); 81 | let data = inst.db.get_block(key).unwrap(); 82 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 83 | { inst.status.inc_failed(); continue; }); 84 | 85 | let node_ids: Vec<_> = nodes.iter() 86 | .filter_map(|n| block.nimap.get_id(n)).collect(); 87 | if !nodes.is_empty() && node_ids.is_empty() { 88 | continue; // Block doesn't contain any of the required nodes. 89 | } 90 | 91 | let block_corner = Vec3::from_block_key(key) * 16; 92 | let mut modified = false; 93 | 94 | for (&idx, data) in &mut block.metadata { 95 | let pos = Vec3::from_u16_key(idx); 96 | let abs_pos = pos + block_corner; 97 | if let Some(a) = inst.args.area { 98 | if a.contains(abs_pos) == inst.args.invert { 99 | continue; 100 | } 101 | } 102 | if !node_ids.is_empty() 103 | && !node_ids.contains(&block.node_data.nodes[idx as usize]) 104 | { 105 | continue; 106 | } 107 | 108 | let i_mods = do_replace(&mut data.inv, &item, &new_item, 109 | inst.args.delete_meta); 110 | item_mods += i_mods; 111 | if i_mods > 0 { 112 | node_mods += 1; 113 | modified = true; 114 | } 115 | } 116 | 117 | if modified { 118 | inst.db.set_block(key, &block.serialize()).unwrap(); 119 | } 120 | } 121 | 122 | inst.status.end_editing(); 123 | inst.status.log_info(format!("Replaced {} itemstacks in {} nodes.", 124 | fmt_big_num(item_mods), fmt_big_num(node_mods))); 125 | } 126 | 127 | 128 | fn verify_args(args: &InstArgs) -> ArgResult { 129 | if args.new_item.is_none() && !args.delete && !args.delete_meta { 130 | return ArgResult::error( 131 | "new_item is required unless --delete or --deletemeta is used."); 132 | } else if args.new_item.is_some() && args.delete { 133 | return ArgResult::error( 134 | "Cannot delete items if new_item is specified."); 135 | } else if args.item == args.new_item && !args.delete_meta { 136 | return ArgResult::error("item and new_item cannot be the same."); 137 | } 138 | ArgResult::Ok 139 | } 140 | 141 | 142 | pub fn get_command() -> Command { 143 | Command { 144 | func: replace_in_inv, 145 | verify_args: Some(verify_args), 146 | args: vec![ 147 | (ArgType::Item, "Name of the item to replace/delete"), 148 | (ArgType::NewItem, "Name of the new item, if replacing items."), 149 | (ArgType::Delete, "Delete items instead of replacing them."), 150 | (ArgType::DeleteMeta, "Delete metadata of affected items."), 151 | (ArgType::Nodes, 152 | "Names of one or more nodes to modify inventories of"), 153 | (ArgType::Area(false), "Area in which to modify node inventories"), 154 | (ArgType::Invert, 155 | "Modify node inventories *outside* the given area."), 156 | ], 157 | help: "Replace, delete, or modify items in certain node inventories." 158 | } 159 | } 160 | 161 | 162 | #[cfg(test)] 163 | mod tests { 164 | use super::do_replace; 165 | 166 | #[test] 167 | fn test_replace_in_inv() { 168 | let original = b"\ 169 | List main 10\n\ 170 | Width 5\n\ 171 | Item tools:pickaxe 1 300\n\ 172 | Item test:foo\n\ 173 | Item test:foo 3\n\ 174 | Item test:foo 10 32768\n\ 175 | Empty\n\ 176 | Item test:foo 1 0 \x01some variable\x02some value\x03\n\ 177 | Item test:foo 1 1234 \x01color\x02#FF00FF\x03\n\ 178 | Item test:bar 20\n\ 179 | Item test:foo 42 0 \x01random_number\x02892\x03\n\ 180 | Item test:foo 99 100 \x01description\x02test metadata\x03\n\ 181 | EndInventoryList\n\ 182 | EndInventory\n"; 183 | let replace = b"\ 184 | List main 10\n\ 185 | Width 5\n\ 186 | Item tools:pickaxe 1 300\n\ 187 | Item test:bar\n\ 188 | Item test:bar 3\n\ 189 | Item test:bar 10 32768\n\ 190 | Empty\n\ 191 | Item test:bar 1 0 \x01some variable\x02some value\x03\n\ 192 | Item test:bar 1 1234 \x01color\x02#FF00FF\x03\n\ 193 | Item test:bar 20\n\ 194 | Item test:bar 42 0 \x01random_number\x02892\x03\n\ 195 | Item test:bar 99 100 \x01description\x02test metadata\x03\n\ 196 | EndInventoryList\n\ 197 | EndInventory\n"; 198 | let delete = b"\ 199 | List main 10\n\ 200 | Width 5\n\ 201 | Item tools:pickaxe 1 300\n\ 202 | Empty\n\ 203 | Empty\n\ 204 | Empty\n\ 205 | Empty\n\ 206 | Empty\n\ 207 | Empty\n\ 208 | Item test:bar 20\n\ 209 | Empty\n\ 210 | Empty\n\ 211 | EndInventoryList\n\ 212 | EndInventory\n"; 213 | let replace_delete_meta = b"\ 214 | List main 10\n\ 215 | Width 5\n\ 216 | Item tools:pickaxe 1 300\n\ 217 | Item test:bar\n\ 218 | Item test:bar 3\n\ 219 | Item test:bar 10 32768\n\ 220 | Empty\n\ 221 | Item test:bar\n\ 222 | Item test:bar 1 1234\n\ 223 | Item test:bar 20\n\ 224 | Item test:bar 42\n\ 225 | Item test:bar 99 100\n\ 226 | EndInventoryList\n\ 227 | EndInventory\n"; 228 | 229 | let mut inv = original.to_vec(); 230 | do_replace(&mut inv, b"test:foo", b"test:bar", false); 231 | assert_eq!(&inv, replace); 232 | 233 | let mut inv = original.to_vec(); 234 | do_replace(&mut inv, b"test:foo", b"", false); 235 | assert_eq!(&inv, delete); 236 | 237 | let mut inv = original.to_vec(); 238 | do_replace(&mut inv, b"test:foo", b"test:bar", true); 239 | assert_eq!(&inv, replace_delete_meta); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/commands/replace_nodes.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult}; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::{Vec3, Area, InverseBlockIterator}; 5 | use crate::instance::{ArgType, InstArgs, InstBundle}; 6 | use crate::map_block::MapBlock; 7 | use crate::utils::{query_keys, to_bytes, fmt_big_num}; 8 | 9 | 10 | fn do_replace( 11 | block: &mut MapBlock, 12 | key: i64, 13 | old_id: u16, 14 | new_node: &[u8], 15 | area: Option, 16 | invert: bool 17 | ) -> u64 18 | { 19 | let nodes = &mut block.node_data.nodes; 20 | let block_pos = Vec3::from_block_key(key); 21 | let mut replaced = 0; 22 | 23 | // Replace nodes in a portion of the mapblock. 24 | if area 25 | .filter(|a| a.contains_block(block_pos) != a.touches_block(block_pos)) 26 | .is_some() 27 | { 28 | let node_area = area.unwrap().rel_block_overlap(block_pos).unwrap(); 29 | 30 | let (new_id, new_id_needed) = match block.nimap.get_id(new_node) { 31 | Some(id) => (id, false), 32 | None => (block.nimap.get_max_id().unwrap() + 1, true) 33 | }; 34 | 35 | if invert { 36 | for idx in InverseBlockIterator::new(node_area) { 37 | if nodes[idx] == old_id { 38 | nodes[idx] = new_id; 39 | replaced += 1; 40 | } 41 | } 42 | } else { 43 | for pos in &node_area { 44 | let idx = (pos.x + 16 * (pos.y + 16 * pos.z)) as usize; 45 | if nodes[idx] == old_id { 46 | nodes[idx] = new_id; 47 | replaced += 1; 48 | } 49 | } 50 | } 51 | 52 | // If replacement ID is not in the name-ID map but was used, add it. 53 | if new_id_needed && replaced > 0 { 54 | block.nimap.0.insert(new_id, new_node.to_vec()); 55 | } 56 | 57 | // If all instances of the old ID were replaced, remove the old ID. 58 | if !nodes.contains(&old_id) { 59 | for node in nodes { 60 | *node -= (*node > old_id) as u16; 61 | } 62 | block.nimap.remove_shift(old_id); 63 | } 64 | } 65 | // Replace nodes in whole mapblock. 66 | else { 67 | // Block already contains replacement node, beware! 68 | if let Some(mut new_id) = block.nimap.get_id(new_node) { 69 | // Delete unused ID from name-ID map and shift IDs down. 70 | block.nimap.remove_shift(old_id); 71 | // Shift replacement ID, if necessary. 72 | new_id -= (new_id > old_id) as u16; 73 | 74 | // Map old node IDs to new node IDs. 75 | for id in nodes { 76 | *id = if *id == old_id { 77 | replaced += 1; 78 | new_id 79 | } else { 80 | *id - (*id > old_id) as u16 81 | }; 82 | } 83 | } 84 | // Block does not contain replacement node. 85 | // Simply replace the node name in the name-ID map. 86 | else { 87 | for id in nodes { 88 | replaced += (*id == old_id) as u64; 89 | } 90 | block.nimap.0.insert(old_id, new_node.to_vec()); 91 | } 92 | } 93 | replaced 94 | } 95 | 96 | 97 | fn replace_nodes(inst: &mut InstBundle) { 98 | let old_node = to_bytes(inst.args.node.as_ref().unwrap()); 99 | let new_node = to_bytes(inst.args.new_node.as_ref().unwrap()); 100 | let keys = query_keys(&mut inst.db, &inst.status, 101 | std::slice::from_ref(&old_node), 102 | inst.args.area, inst.args.invert, true); 103 | 104 | inst.status.begin_editing(); 105 | let mut count = 0; 106 | 107 | for key in keys { 108 | let data = inst.db.get_block(key).unwrap(); 109 | 110 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 111 | { inst.status.inc_failed(); continue; }); 112 | 113 | if let Some(old_id) = block.nimap.get_id(&old_node) { 114 | count += do_replace(&mut block, key, old_id, &new_node, 115 | inst.args.area, inst.args.invert); 116 | let new_data = block.serialize(); 117 | inst.db.set_block(key, &new_data).unwrap(); 118 | } 119 | 120 | inst.status.inc_done(); 121 | } 122 | 123 | inst.status.end_editing(); 124 | inst.status.log_info(format!("{} nodes replaced.", fmt_big_num(count))); 125 | } 126 | 127 | 128 | fn verify_args(args: &InstArgs) -> ArgResult { 129 | if args.node == args.new_node { 130 | return ArgResult::error("node and new_node must be different."); 131 | } 132 | 133 | ArgResult::Ok 134 | } 135 | 136 | 137 | pub fn get_command() -> Command { 138 | Command { 139 | func: replace_nodes, 140 | verify_args: Some(verify_args), 141 | args: vec![ 142 | (ArgType::Node(true), "Name of node to replace"), 143 | (ArgType::NewNode, "Name of node to replace with"), 144 | (ArgType::Area(false), "Area in which to replace nodes"), 145 | (ArgType::Invert, "Replace nodes *outside* the given area.") 146 | ], 147 | help: "Replace one node with another node." 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/commands/set_meta_var.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult}; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::Vec3; 5 | use crate::instance::{ArgType, InstArgs, InstBundle}; 6 | use crate::map_block::MapBlock; 7 | use crate::utils::{query_keys, to_bytes, fmt_big_num}; 8 | 9 | 10 | fn verify_args(args: &InstArgs) -> ArgResult { 11 | if args.value.is_none() && !args.delete { 12 | return ArgResult::error( 13 | "value is required unless deleting the variable."); 14 | } else if args.value.is_some() && args.delete { 15 | return ArgResult::error( 16 | "value cannot be used when deleting the variable."); 17 | } else if args.value == Some(String::new()) { 18 | return ArgResult::error("Metadata value cannot be empty."); 19 | } 20 | ArgResult::Ok 21 | } 22 | 23 | 24 | fn set_meta_var(inst: &mut InstBundle) { 25 | // TODO: Bytes input 26 | let key = to_bytes(inst.args.key.as_ref().unwrap()); 27 | let value = to_bytes(inst.args.value.as_ref().unwrap_or(&String::new())); 28 | let nodes: Vec<_> = inst.args.nodes.iter().map(to_bytes).collect(); 29 | 30 | let keys = query_keys(&mut inst.db, &mut inst.status, 31 | &nodes, inst.args.area, inst.args.invert, true); 32 | 33 | inst.status.begin_editing(); 34 | let mut count: u64 = 0; 35 | 36 | for block_key in keys { 37 | inst.status.inc_done(); 38 | let data = inst.db.get_block(block_key).unwrap(); 39 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 40 | { inst.status.inc_failed(); continue; }); 41 | 42 | let node_ids: Vec<_> = nodes.iter() 43 | .filter_map(|n| block.nimap.get_id(n)).collect(); 44 | if !nodes.is_empty() && node_ids.is_empty() { 45 | continue; // Block doesn't contain any of the required nodes. 46 | } 47 | 48 | let block_corner = Vec3::from_block_key(block_key) * 16; 49 | let mut modified = false; 50 | 51 | for (&idx, data) in &mut block.metadata { 52 | let pos = Vec3::from_u16_key(idx); 53 | 54 | if let Some(a) = inst.args.area { 55 | if a.contains(pos + block_corner) == inst.args.invert { 56 | continue; 57 | } 58 | } 59 | if !node_ids.is_empty() 60 | && !node_ids.contains(&block.node_data.nodes[idx as usize]) 61 | { 62 | continue; 63 | } 64 | 65 | if data.vars.contains_key(&key) { 66 | if inst.args.delete { 67 | // Note: serialize() will cull any newly empty metadata. 68 | data.vars.remove(&key); 69 | } else { 70 | data.vars.get_mut(&key).unwrap().0 = value.clone(); 71 | } 72 | modified = true; 73 | count += 1; 74 | } 75 | } 76 | 77 | if modified { 78 | inst.db.set_block(block_key, &block.serialize()).unwrap(); 79 | } 80 | } 81 | 82 | inst.status.end_editing(); 83 | inst.status.log_info( 84 | format!("Set metadata variable of {} nodes.", fmt_big_num(count))); 85 | } 86 | 87 | 88 | pub fn get_command() -> Command { 89 | Command { 90 | func: set_meta_var, 91 | verify_args: Some(verify_args), 92 | args: vec![ 93 | (ArgType::Key, "Name of variable to set/delete"), 94 | (ArgType::Value, "Value to set variable to, if setting a value"), 95 | (ArgType::Delete, "Delete the variable."), 96 | (ArgType::Nodes, 97 | "Names of one or more nodes to modify. If not specified, any \ 98 | node with the given variable will be modified."), 99 | (ArgType::Area(false), 100 | "Area in which to modify node metadata"), 101 | (ArgType::Invert, "Modify node metadata *outside* the given area."), 102 | ], 103 | help: "Set or delete a variable in node metadata of certain nodes." 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/commands/set_param2.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, ArgResult}; 2 | 3 | use crate::unwrap_or; 4 | use crate::spatial::{Vec3, Area, InverseBlockIterator}; 5 | use crate::instance::{ArgType, InstArgs, InstBundle}; 6 | use crate::map_block::MapBlock; 7 | use crate::utils::{query_keys, to_bytes, to_slice, fmt_big_num}; 8 | 9 | 10 | fn set_param2_partial(block: &mut MapBlock, area: Area, invert: bool, 11 | node_id: Option, val: u8) -> u64 12 | { 13 | let nd = &mut block.node_data; 14 | let mut count = 0; 15 | 16 | if invert { 17 | if let Some(id) = node_id { 18 | for idx in InverseBlockIterator::new(area) { 19 | if nd.nodes[idx] == id { 20 | nd.param2[idx] = val; 21 | count += 1; 22 | } 23 | } 24 | } else { 25 | for idx in InverseBlockIterator::new(area) { 26 | nd.param2[idx] = val; 27 | } 28 | count += 4096 - area.volume(); 29 | } 30 | } else { 31 | let no_node = node_id.is_none(); 32 | let id = node_id.unwrap_or(0); 33 | 34 | for z in area.min.z ..= area.max.z { 35 | let z_start = z * 256; 36 | for y in area.min.y ..= area.max.y { 37 | let zy_start = z_start + y * 16; 38 | for x in area.min.x ..= area.max.x { 39 | let i = (zy_start + x) as usize; 40 | if no_node || nd.nodes[i] == id { 41 | nd.param2[i] = val; 42 | count += 1; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | count 50 | } 51 | 52 | 53 | fn set_param2(inst: &mut InstBundle) { 54 | let param2_val = inst.args.param2.unwrap(); 55 | let node = inst.args.node.as_ref().map(to_bytes); 56 | 57 | let keys = query_keys(&mut inst.db, &mut inst.status, 58 | to_slice(&node), inst.args.area, inst.args.invert, true); 59 | 60 | inst.status.begin_editing(); 61 | 62 | let mut count: u64 = 0; 63 | for key in keys { 64 | inst.status.inc_done(); 65 | 66 | let pos = Vec3::from_block_key(key); 67 | let data = inst.db.get_block(key).unwrap(); 68 | let mut block = unwrap_or!(MapBlock::deserialize(&data), 69 | { inst.status.inc_failed(); continue; }); 70 | 71 | let node_id = node.as_ref().and_then(|n| block.nimap.get_id(n)); 72 | if inst.args.node.is_some() && node_id.is_none() { 73 | // Node not found in this mapblock. 74 | continue; 75 | } 76 | 77 | let nd = &mut block.node_data; 78 | if let Some(area) = inst.args.area 79 | .filter(|a| a.contains_block(pos) != a.touches_block(pos)) 80 | { // Modify part of block 81 | let block_part = area.rel_block_overlap(pos).unwrap(); 82 | count += set_param2_partial(&mut block, 83 | block_part, inst.args.invert, node_id, param2_val); 84 | } else { // Modify whole block 85 | if let Some(nid) = node_id { 86 | for i in 0 .. nd.param2.len() { 87 | if nd.nodes[i] == nid { 88 | nd.param2[i] = param2_val; 89 | count += 1; 90 | } 91 | } 92 | } else { 93 | nd.param2.fill(param2_val); 94 | count += nd.param2.len() as u64; 95 | } 96 | } 97 | 98 | inst.db.set_block(key, &block.serialize()).unwrap(); 99 | } 100 | 101 | inst.status.end_editing(); 102 | inst.status.log_info(format!("Set param2 of {} nodes.", 103 | fmt_big_num(count))); 104 | } 105 | 106 | 107 | fn verify_args(args: &InstArgs) -> ArgResult { 108 | if args.area.is_none() && args.node.is_none() { 109 | return ArgResult::error("An area and/or node is required."); 110 | } 111 | 112 | ArgResult::Ok 113 | } 114 | 115 | 116 | pub fn get_command() -> Command { 117 | Command { 118 | func: set_param2, 119 | verify_args: Some(verify_args), 120 | args: vec![ 121 | (ArgType::Node(false), "Name of node to modify"), 122 | (ArgType::Area(false), "Area in which to set param2 values"), 123 | (ArgType::Invert, "Set param2 values *outside* the given area."), 124 | (ArgType::Param2, "New param2 value, between 0 and 255"), 125 | ], 126 | help: "Set param2 values of certain nodes." 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/commands/vacuum.rs: -------------------------------------------------------------------------------- 1 | use super::Command; 2 | 3 | use crate::instance::InstBundle; 4 | 5 | 6 | fn vacuum(inst: &mut InstBundle) { 7 | inst.status.log_info("Starting vacuum."); 8 | 9 | inst.status.set_show_progress(false); // No ETA for vacuum. 10 | inst.status.begin_editing(); 11 | let res = inst.db.vacuum(); 12 | inst.status.end_editing(); 13 | 14 | match res { 15 | Ok(_) => { 16 | inst.status.log_info("Completed vacuum."); 17 | }, 18 | Err(e) => inst.status.log_error(format!("Vacuum failed: {}.", e)) 19 | } 20 | } 21 | 22 | 23 | pub fn get_command() -> Command { 24 | Command { 25 | func: vacuum, 26 | verify_args: None, 27 | args: Vec::new(), 28 | help: "Rebuild the map database to reduce its size." 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/instance.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::{Arc, Mutex}; 3 | use std::sync::mpsc; 4 | 5 | use anyhow::Context; 6 | 7 | use crate::spatial::{Vec3, Area, MAP_LIMIT}; 8 | use crate::map_database::MapDatabase; 9 | use crate::commands; 10 | use crate::commands::ArgResult; 11 | use crate::utils::fmt_big_num; 12 | 13 | 14 | #[derive(Clone)] 15 | pub enum ArgType { 16 | InputMapPath, 17 | Area(bool), 18 | Invert, 19 | Offset(bool), 20 | Node(bool), 21 | Nodes, 22 | NewNode, 23 | Object, 24 | Item, 25 | Items, 26 | NewItem, 27 | Delete, 28 | DeleteMeta, 29 | Key, 30 | Value, 31 | Param2, 32 | } 33 | 34 | 35 | #[derive(Debug)] 36 | pub struct InstArgs { 37 | pub do_confirmation: bool, 38 | pub command: String, 39 | pub map_path: String, 40 | pub input_map_path: Option, 41 | pub area: Option, 42 | pub invert: bool, 43 | pub offset: Option, 44 | pub node: Option, 45 | pub nodes: Vec, 46 | pub new_node: Option, 47 | pub object: Option, 48 | pub item: Option, 49 | pub items: Option>, 50 | pub new_item: Option, 51 | pub delete: bool, 52 | pub delete_meta: bool, 53 | pub key: Option, 54 | pub value: Option, 55 | pub param2: Option, 56 | } 57 | 58 | 59 | /// Used to tell what sort of progress bar/counter should be shown to the user. 60 | #[derive(Clone, Copy, PartialEq)] 61 | pub enum InstState { 62 | Ignore, 63 | Querying, 64 | Editing 65 | } 66 | 67 | 68 | #[derive(Clone)] 69 | pub struct InstStatus { 70 | pub show_progress: bool, 71 | pub blocks_total: usize, 72 | pub blocks_done: usize, 73 | pub blocks_failed: usize, 74 | pub state: InstState 75 | } 76 | 77 | impl InstStatus { 78 | fn new() -> Self { 79 | Self { 80 | show_progress: true, 81 | blocks_total: 0, 82 | blocks_done: 0, 83 | blocks_failed: 0, 84 | state: InstState::Ignore 85 | } 86 | } 87 | } 88 | 89 | 90 | pub enum LogType { 91 | Info, 92 | Warning, 93 | Error 94 | } 95 | 96 | impl std::fmt::Display for LogType { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | match self { 99 | Self::Info => write!(f, "info"), 100 | Self::Warning => write!(f, "warning"), 101 | Self::Error => write!(f, "error") 102 | } 103 | } 104 | } 105 | 106 | 107 | pub enum ServerEvent { 108 | Log(LogType, String), 109 | NewState(InstState), 110 | ConfirmRequest, 111 | } 112 | 113 | 114 | pub enum ClientEvent { 115 | ConfirmResponse(bool), 116 | } 117 | 118 | 119 | pub struct StatusServer { 120 | status: Arc>, 121 | event_tx: mpsc::Sender, 122 | event_rx: mpsc::Receiver, 123 | } 124 | 125 | impl StatusServer { 126 | pub fn get_status(&self) -> InstStatus { 127 | self.status.lock().unwrap().clone() 128 | } 129 | 130 | pub fn set_state(&self, new_state: InstState) { 131 | self.status.lock().unwrap().state = new_state; 132 | self.event_tx.send(ServerEvent::NewState(new_state)).unwrap(); 133 | } 134 | 135 | pub fn set_total(&self, total: usize) { 136 | self.status.lock().unwrap().blocks_total = total; 137 | } 138 | 139 | pub fn inc_done(&self) { 140 | self.status.lock().unwrap().blocks_done += 1; 141 | } 142 | 143 | pub fn inc_failed(&mut self) { 144 | self.status.lock().unwrap().blocks_failed += 1; 145 | } 146 | 147 | pub fn set_show_progress(&self, sp: bool) { 148 | self.status.lock().unwrap().show_progress = sp; 149 | } 150 | 151 | pub fn begin_editing(&self) { 152 | self.set_state(InstState::Editing); 153 | } 154 | 155 | pub fn end_editing(&self) { 156 | self.set_state(InstState::Ignore); 157 | } 158 | 159 | pub fn get_confirmation(&self) -> bool { 160 | self.event_tx.send(ServerEvent::ConfirmRequest).unwrap(); 161 | while let Ok(event) = self.event_rx.recv() { 162 | match event { 163 | ClientEvent::ConfirmResponse(res) => return res 164 | } 165 | } 166 | false 167 | } 168 | 169 | fn log>(&self, lt: LogType, msg: S) { 170 | self.event_tx.send(ServerEvent::Log(lt, msg.as_ref().to_string())) 171 | .unwrap(); 172 | } 173 | 174 | pub fn log_info>(&self, msg: S) { 175 | self.log(LogType::Info, msg); 176 | } 177 | 178 | pub fn log_warning>(&self, msg: S) { 179 | self.log(LogType::Warning, msg); 180 | } 181 | 182 | pub fn log_error>(&self, msg: S) { 183 | self.log(LogType::Error, msg); 184 | } 185 | } 186 | 187 | 188 | pub struct StatusClient { 189 | status: Arc>, 190 | event_tx: mpsc::Sender, 191 | event_rx: mpsc::Receiver, 192 | } 193 | 194 | impl StatusClient { 195 | pub fn get_status(&self) -> InstStatus { 196 | self.status.lock().unwrap().clone() 197 | } 198 | 199 | #[inline] 200 | pub fn receiver(&self) -> &mpsc::Receiver { 201 | &self.event_rx 202 | } 203 | 204 | pub fn send_confirmation(&self, choice: bool) { 205 | self.event_tx.send(ClientEvent::ConfirmResponse(choice)).unwrap(); 206 | } 207 | } 208 | 209 | 210 | fn status_link() -> (StatusServer, StatusClient) { 211 | let status1 = Arc::new(Mutex::new(InstStatus::new())); 212 | let status2 = status1.clone(); 213 | let (s_event_tx, s_event_rx) = mpsc::channel(); 214 | let (c_event_tx, c_event_rx) = mpsc::channel(); 215 | ( 216 | StatusServer { 217 | status: status1, 218 | event_tx: s_event_tx, 219 | event_rx: c_event_rx, 220 | }, 221 | StatusClient { 222 | status: status2, 223 | event_tx: c_event_tx, 224 | event_rx: s_event_rx, 225 | } 226 | ) 227 | } 228 | 229 | 230 | pub struct InstBundle<'a> { 231 | pub args: InstArgs, 232 | pub status: StatusServer, 233 | pub db: MapDatabase<'a>, 234 | pub idb: Option> 235 | } 236 | 237 | 238 | fn verify_args(args: &InstArgs) -> anyhow::Result<()> { 239 | if args.area.is_none() && args.invert { 240 | anyhow::bail!("Cannot invert without a specified area."); 241 | } 242 | if let Some(a) = args.area { 243 | for pos in &[a.min, a.max] { 244 | anyhow::ensure!(pos.is_valid_node_pos(), 245 | "Area corner is outside map bounds: {}.", pos); 246 | } 247 | } 248 | if let Some(offset) = args.offset { 249 | let huge = |n| n < -MAP_LIMIT * 2 || n > MAP_LIMIT * 2; 250 | 251 | if huge(offset.x) || huge(offset.y) || huge(offset.z) { 252 | anyhow::bail!( 253 | "Offset cannot be larger than {} nodes in any direction.", 254 | MAP_LIMIT * 2); 255 | } 256 | } 257 | 258 | fn is_valid_name(name: &str) -> bool { 259 | if name == "air" || name == "ignore" { 260 | true 261 | } else { 262 | let delim = match name.find(':') { 263 | Some(d) => d, 264 | None => return false 265 | }; 266 | 267 | let mod_name = &name[..delim]; 268 | let item_name = &name[delim + 1..]; 269 | 270 | mod_name.chars().all(|c: char| 271 | c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') 272 | && item_name.chars().all(|c: char| 273 | c.is_ascii_alphanumeric() || c == '_') 274 | } 275 | } 276 | 277 | macro_rules! verify_name { 278 | ($name:expr, $msg:literal) => { 279 | if let Some(n) = &$name { 280 | anyhow::ensure!(is_valid_name(n), $msg, n); 281 | } 282 | } 283 | } 284 | 285 | verify_name!(args.node, "Invalid node name: {}"); 286 | for n in &args.nodes { 287 | anyhow::ensure!(is_valid_name(n), "Invalid node name: {}", n); 288 | } 289 | verify_name!(args.new_node, "Invalid node name: {}"); 290 | verify_name!(args.object, "Invalid object name: {}"); 291 | verify_name!(args.item, "Invalid item name: {}"); 292 | if let Some(items) = &args.items { 293 | for i in items { 294 | anyhow::ensure!(is_valid_name(i), "Invalid item name: {}", i); 295 | } 296 | } 297 | verify_name!(args.new_item, "Invalid item name: {}"); 298 | // TODO: Are keys/values escaped? 299 | 300 | Ok(()) 301 | } 302 | 303 | 304 | fn open_map(path: PathBuf, flags: sqlite::OpenFlags) 305 | -> anyhow::Result 306 | { 307 | let new_path = if path.is_file() { 308 | path 309 | } else { 310 | let with_file = path.join("map.sqlite"); 311 | if with_file.is_file() { 312 | with_file 313 | } else { 314 | anyhow::bail!("Could not find the map file."); 315 | } 316 | }; 317 | 318 | Ok(sqlite::Connection::open_with_flags(new_path, flags)?) 319 | } 320 | 321 | 322 | fn compute_thread(args: InstArgs, status: StatusServer) -> anyhow::Result<()> { 323 | verify_args(&args)?; 324 | 325 | let commands = commands::get_commands(); 326 | let mut cmd_warning = None; 327 | if let Some(cmd_verify) = commands[args.command.as_str()].verify_args { 328 | cmd_warning = match cmd_verify(&args) { 329 | ArgResult::Ok => None, 330 | ArgResult::Warning(w) => Some(w), 331 | ArgResult::Error(e) => anyhow::bail!(e) 332 | } 333 | } 334 | 335 | let db_conn = open_map( 336 | PathBuf::from(&args.map_path), 337 | sqlite::OpenFlags::new().set_read_write() 338 | ).context("Failed to open main world/map.")?; 339 | let db = MapDatabase::new(&db_conn) 340 | .context("Main world or map database is invalid.")?; 341 | 342 | let idb_conn = args.input_map_path.as_deref() 343 | .map(|imp| open_map(PathBuf::from(imp), 344 | sqlite::OpenFlags::new().set_read_only())) 345 | .transpose().context("Failed to open input world/map.")?; 346 | let idb = match &idb_conn { 347 | Some(conn) => Some(MapDatabase::new(conn) 348 | .context("Input world or map database is invalid.")?), 349 | None => None 350 | }; 351 | 352 | let func = commands[args.command.as_str()].func; 353 | let mut inst = InstBundle {args, status, db, idb}; 354 | 355 | // Issue warnings and confirmation prompt. 356 | if inst.args.do_confirmation { 357 | inst.status.log_warning( 358 | "This tool can permanently damage your Minetest world.\n\ 359 | Always EXIT Minetest and BACK UP the map database before use."); 360 | } 361 | if let Some(w) = cmd_warning { 362 | inst.status.log_warning(w); 363 | } 364 | if inst.args.do_confirmation && !inst.status.get_confirmation() { 365 | return Ok(()); 366 | } 367 | 368 | func(&mut inst); // The real thing! 369 | 370 | let fails = inst.status.get_status().blocks_failed; 371 | if fails > 0 { 372 | inst.status.log_info(format!( 373 | "Skipped {} invalid/unsupported mapblocks.", 374 | fmt_big_num(fails as u64) 375 | )); 376 | } 377 | 378 | if inst.db.is_in_transaction() { 379 | inst.status.log_info("Committing..."); 380 | inst.db.commit_if_needed()?; 381 | } 382 | inst.status.log_info("Done."); 383 | Ok(()) 384 | } 385 | 386 | 387 | pub fn spawn_compute_thread(args: InstArgs) 388 | -> (std::thread::JoinHandle<()>, StatusClient) 389 | { 390 | let (status_server, status_client) = status_link(); 391 | // Clone within this thread to avoid issue #39364 (hopefully). 392 | let raw_event_tx = status_server.event_tx.clone(); 393 | let h = std::thread::Builder::new() 394 | .name("compute".to_string()) 395 | .spawn(move || { 396 | compute_thread(args, status_server).unwrap_or_else( 397 | // TODO: Find a cleaner way to do this. 398 | |err| raw_event_tx.send( 399 | ServerEvent::Log(LogType::Error, err.to_string())).unwrap() 400 | ); 401 | }) 402 | .unwrap(); 403 | (h, status_client) 404 | } 405 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Uncomment if needed for testing 2 | // mod testing; 3 | mod spatial; 4 | mod utils; 5 | mod map_database; 6 | mod map_block; 7 | mod block_utils; 8 | mod instance; 9 | mod commands; 10 | mod cmd_line; 11 | 12 | 13 | fn main() { 14 | // TODO: Add a GUI. hmm... 15 | cmd_line::run_cmd_line(); 16 | } 17 | -------------------------------------------------------------------------------- /src/map_block/map_block.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use zstd; 3 | 4 | /* 5 | Supported mapblock versions: 6 | 25: In use from 0.4.2-rc1 until 0.4.15. 7 | 26: Only ever sent over the network, not saved. 8 | 27: Existed for around 3 months during 0.4.16 development. 9 | 28: In use from 0.4.16 to 5.4.x. 10 | 29: In use since 5.5.0 (mapblocks are now compressed with zstd instead of zlib). 11 | */ 12 | 13 | const MIN_BLOCK_VER: u8 = 25; 14 | const MAX_BLOCK_VER: u8 = 29; 15 | const SERIALIZE_BUF_SIZE: usize = 2048; 16 | 17 | 18 | pub fn is_valid_generated(src: &[u8]) -> bool { 19 | if src.len() < 2 { 20 | return false; 21 | } 22 | 23 | let mut crs = Cursor::new(src); 24 | let version = crs.read_u8().unwrap(); 25 | if version < MIN_BLOCK_VER || version > MAX_BLOCK_VER { 26 | return false; 27 | } 28 | 29 | let flags = if version >= 29 { 30 | let mut dec = zstd::stream::Decoder::new(crs).unwrap(); 31 | match dec.read_u8() { 32 | Ok(f) => f, 33 | Err(_) => return false 34 | } 35 | } else { 36 | crs.read_u8().unwrap() 37 | }; 38 | 39 | flags & 0x08 == 0 // Bit 3 set if block is not generated. 40 | } 41 | 42 | 43 | #[derive(Clone, Debug)] 44 | pub struct MapBlock { 45 | pub version: u8, 46 | pub flags: u8, 47 | pub lighting_complete: u16, 48 | pub content_width: u8, 49 | pub params_width: u8, 50 | pub node_data: NodeData, 51 | pub metadata: NodeMetadataList, 52 | pub static_objects: StaticObjectList, 53 | pub timestamp: u32, 54 | pub nimap: NameIdMap, 55 | pub node_timers: NodeTimerList 56 | } 57 | 58 | impl MapBlock { 59 | pub fn deserialize(src: &[u8]) -> Result { 60 | let mut raw_crs = Cursor::new(src); 61 | 62 | let version = raw_crs.read_u8()?; 63 | if version < MIN_BLOCK_VER || version > MAX_BLOCK_VER { 64 | return Err(MapBlockError::InvalidBlockVersion); 65 | } 66 | 67 | // TODO: use thread_local buffer for decompressed data. 68 | let decompressed; 69 | let mut crs = 70 | if version >= 29 { 71 | decompressed = zstd::stream::decode_all(raw_crs)?; 72 | Cursor::new(decompressed.as_slice()) 73 | } else { raw_crs }; 74 | 75 | let flags = crs.read_u8()?; 76 | let lighting_complete = 77 | if version >= 27 { crs.read_u16::()? } 78 | else { 0xFFFF }; 79 | 80 | let mut timestamp = 0; 81 | let mut nimap = None; // Use Option to avoid re-initializing the map. 82 | 83 | if version >= 29 { // Timestamp/Name-ID map were moved in v29. 84 | timestamp = crs.read_u32::()?; 85 | nimap = Some(NameIdMap::deserialize(&mut crs)?); 86 | } 87 | 88 | let content_width = crs.read_u8()?; 89 | let params_width = crs.read_u8()?; 90 | // TODO: support content_width == 1? 91 | if content_width != 2 || params_width != 2 { 92 | return Err(MapBlockError::InvalidFeature); 93 | } 94 | 95 | let node_data = 96 | if version >= 29 { 97 | NodeData::deserialize(&mut crs)? 98 | } else { 99 | NodeData::decompress(&mut crs)? 100 | }; 101 | 102 | let metadata = 103 | if version >= 29 { 104 | NodeMetadataList::deserialize(&mut crs)? 105 | } else { 106 | NodeMetadataList::decompress(&mut crs)? 107 | }; 108 | 109 | let static_objects = deserialize_objects(&mut crs)?; 110 | 111 | if version < 29 { 112 | timestamp = crs.read_u32::()?; 113 | nimap = Some(NameIdMap::deserialize(&mut crs)?); 114 | } 115 | 116 | let node_timers = deserialize_timers(&mut crs)?; 117 | 118 | Ok(Self { 119 | version, 120 | flags, 121 | lighting_complete, 122 | content_width, 123 | params_width, 124 | node_data, 125 | metadata, 126 | static_objects, 127 | timestamp, 128 | nimap: nimap.unwrap(), 129 | node_timers 130 | }) 131 | } 132 | 133 | pub fn serialize(&self) -> Vec { 134 | // TODO: Retain compression level used by Minetest? 135 | assert!(MIN_BLOCK_VER <= self.version && self.version <= MAX_BLOCK_VER, 136 | "Invalid mapblock version."); 137 | 138 | // TODO: Use a bigger buffer (unsafe?) to reduce heap allocations. 139 | let mut buf = Vec::with_capacity(SERIALIZE_BUF_SIZE); 140 | let mut crs = Cursor::new(buf); 141 | crs.write_u8(self.version).unwrap(); 142 | 143 | if self.version >= 29 { 144 | let mut enc = zstd::stream::Encoder::new(crs, 0).unwrap(); 145 | 146 | enc.write_u8(self.flags).unwrap(); 147 | enc.write_u16::(self.lighting_complete).unwrap(); 148 | enc.write_u32::(self.timestamp).unwrap(); 149 | self.nimap.serialize(&mut enc); 150 | enc.write_u8(self.content_width).unwrap(); 151 | enc.write_u8(self.params_width).unwrap(); 152 | self.node_data.serialize(&mut enc); 153 | self.metadata.serialize(&mut enc, self.version); 154 | serialize_objects(&self.static_objects, &mut enc); 155 | serialize_timers(&self.node_timers, &mut enc); 156 | 157 | crs = enc.finish().unwrap(); 158 | } else { // version <= 28 159 | crs.write_u8(self.flags).unwrap(); 160 | 161 | if self.version >= 27 { 162 | crs.write_u16::(self.lighting_complete).unwrap(); 163 | } 164 | 165 | crs.write_u8(self.content_width).unwrap(); 166 | crs.write_u8(self.params_width).unwrap(); 167 | self.node_data.compress(&mut crs); 168 | self.metadata.compress(&mut crs, self.version); 169 | serialize_objects(&self.static_objects, &mut crs); 170 | crs.write_u32::(self.timestamp).unwrap(); 171 | self.nimap.serialize(&mut crs); 172 | serialize_timers(&self.node_timers, &mut crs); 173 | } 174 | 175 | buf = crs.into_inner(); 176 | buf.shrink_to_fit(); 177 | buf 178 | } 179 | } 180 | 181 | 182 | #[cfg(test)] 183 | mod tests { 184 | use super::*; 185 | use crate::spatial::Vec3; 186 | use std::path::Path; 187 | 188 | #[test] 189 | fn test_is_valid_generated() { 190 | let ivg = is_valid_generated; 191 | 192 | // Too short 193 | assert_eq!(ivg(b""), false); 194 | assert_eq!(ivg(b"\x18"), false); // v24 195 | assert_eq!(ivg(b"\x1D"), false); // v29 196 | // Invalid version 197 | assert_eq!(ivg(b"\x18\x00\x00\x00"), false); // v24 198 | assert_eq!(ivg(b"\x1E\x00\x00\x00"), false); // v30 199 | // v28, "not generated" flag set 200 | assert_eq!(ivg(b"\x1C\x08"), false); 201 | // v29, zstd compressed data is unreadable 202 | assert_eq!(ivg(b"\x1D\x00\xFF"), false); 203 | // v29, "not generated" flag set 204 | assert_eq!(ivg(b"\x1D\x28\xB5\x2F\xFD\x00\x58\x19\x00\x00\x08\xFF\xFF"), false); 205 | // v28, good 206 | assert_eq!(ivg(b"\x1C\x00"), true); 207 | // v29, good 208 | assert_eq!(ivg(b"\x1D\x28\xB5\x2F\xFD\x00\x58\x19\x00\x00\x00\xFF\xFF"), true); 209 | } 210 | 211 | fn read_test_file(filename: &str) -> anyhow::Result> { 212 | let cargo_path = std::env::var("CARGO_MANIFEST_DIR")?; 213 | let path = Path::new(&cargo_path).join("testing").join(filename); 214 | Ok(std::fs::read(path)?) 215 | } 216 | 217 | #[test] 218 | fn test_mapblock_v29() { 219 | // Original block positioned at (0, 0, 0). 220 | let data1 = read_test_file("mapblock_v29.bin").unwrap(); 221 | let block1 = MapBlock::deserialize(&data1).unwrap(); 222 | // Re-serialize and re-deserialize to test serialization, since 223 | // serialization results can vary. 224 | let data2 = block1.serialize(); 225 | let block2 = MapBlock::deserialize(&data2).unwrap(); 226 | 227 | for block in &[block1, block2] { 228 | /* Ensure that all block data is correct. */ 229 | assert_eq!(block.version, 29); 230 | assert_eq!(block.flags, 0x03); 231 | assert_eq!(block.lighting_complete, 0xFFFF); 232 | assert_eq!(block.content_width, 2); 233 | assert_eq!(block.params_width, 2); 234 | 235 | // Probe a few spots in the node data. 236 | let nd = &block.node_data; 237 | let timer_node_id = block.nimap.get_id(b"test_mod:timer").unwrap(); 238 | let meta_node_id = block.nimap.get_id(b"test_mod:metadata").unwrap(); 239 | let air_id = block.nimap.get_id(b"air").unwrap(); 240 | assert_eq!(nd.nodes[0x000], timer_node_id); 241 | assert!(nd.nodes[0x001..=0xFFE].iter().all(|&n| n == air_id)); 242 | assert_eq!(nd.nodes[0xFFF], meta_node_id); 243 | assert_eq!(nd.param2[0x000], 19); 244 | assert_eq!(nd.param1[0x111], 0x0F); 245 | assert!(nd.param2[0x001..=0xFFF].iter().all(|&n| n == 0)); 246 | 247 | assert_eq!(block.metadata.len(), 1); 248 | let meta = &block.metadata[&4095]; 249 | assert_eq!(meta.vars.len(), 2); 250 | let formspec_var = meta.vars.get(&b"formspec".to_vec()).unwrap(); 251 | assert_eq!(formspec_var.0.len(), 75); 252 | assert_eq!(formspec_var.1, false); 253 | let infotext_var = meta.vars.get(&b"infotext".to_vec()).unwrap(); 254 | assert_eq!(infotext_var.0, b"Test Chest"); 255 | assert_eq!(infotext_var.1, false); 256 | assert_eq!(meta.inv.len(), 70); 257 | 258 | let obj1 = &block.static_objects[0]; 259 | assert_eq!(obj1.obj_type, 7); 260 | assert_eq!(obj1.f_pos, Vec3::new(1, 2, 2) * 10_000); 261 | assert_eq!(obj1.data.len(), 75); 262 | let obj2 = &block.static_objects[1]; 263 | assert_eq!(obj2.obj_type, 7); 264 | assert_eq!(obj2.f_pos, Vec3::new(8, 9, 12) * 10_000); 265 | assert_eq!(obj2.data.len(), 62); 266 | 267 | assert_eq!(block.timestamp, 542); 268 | 269 | assert_eq!(block.nimap.0[&0], b"test_mod:timer"); 270 | assert_eq!(block.nimap.0[&1], b"air"); 271 | 272 | assert_eq!(block.node_timers[0].pos, 0x000); 273 | assert_eq!(block.node_timers[0].timeout, 1337); 274 | assert_eq!(block.node_timers[0].elapsed, 399); 275 | } 276 | } 277 | 278 | #[test] 279 | fn test_mapblock_v28() { 280 | // Original block positioned at (0, 0, 0). 281 | let data1 = read_test_file("mapblock_v28.bin").unwrap(); 282 | let block1 = MapBlock::deserialize(&data1).unwrap(); 283 | let data2 = block1.serialize(); 284 | let block2 = MapBlock::deserialize(&data2).unwrap(); 285 | 286 | for block in &[block1, block2] { 287 | /* Ensure that all block data is correct. */ 288 | assert_eq!(block.version, 28); 289 | assert_eq!(block.flags, 0x03); 290 | assert_eq!(block.lighting_complete, 0xF1C4); 291 | assert_eq!(block.content_width, 2); 292 | assert_eq!(block.params_width, 2); 293 | 294 | // Probe a few spots in the node data. 295 | let nd = &block.node_data; 296 | let test_node_id = block.nimap.get_id(b"test_mod:timer").unwrap(); 297 | let air_id = block.nimap.get_id(b"air").unwrap(); 298 | assert_eq!(nd.nodes[0x000], test_node_id); 299 | assert!(nd.nodes[0x001..=0xFFE].iter().all(|&n| n == air_id)); 300 | assert_eq!(nd.nodes[0xFFF], test_node_id); 301 | assert_eq!(nd.param1[0x111], 0x0F); 302 | assert_eq!(nd.param2[0x000], 4); 303 | assert!(nd.param2[0x001..=0xFFE].iter().all(|&n| n == 0)); 304 | assert_eq!(nd.param2[0xFFF], 16); 305 | 306 | assert!(block.metadata.is_empty()); 307 | 308 | let obj1 = &block.static_objects[0]; 309 | assert_eq!(obj1.obj_type, 7); 310 | assert_eq!(obj1.f_pos, Vec3::new(8, 9, 12) * 10_000); 311 | assert_eq!(obj1.data.len(), 62); 312 | let obj2 = &block.static_objects[1]; 313 | assert_eq!(obj2.obj_type, 7); 314 | assert_eq!(obj2.f_pos, Vec3::new(1, 2, 2) * 10_000); 315 | assert_eq!(obj2.data.len(), 81); 316 | 317 | assert_eq!(block.timestamp, 2756); 318 | 319 | assert_eq!(block.nimap.0[&0], b"test_mod:timer"); 320 | assert_eq!(block.nimap.0[&1], b"air"); 321 | 322 | assert_eq!(block.node_timers[0].pos, 0xFFF); 323 | assert_eq!(block.node_timers[0].timeout, 1337); 324 | assert_eq!(block.node_timers[0].elapsed, 600); 325 | assert_eq!(block.node_timers[1].pos, 0x000); 326 | assert_eq!(block.node_timers[1].timeout, 1337); 327 | assert_eq!(block.node_timers[1].elapsed, 200); 328 | } 329 | } 330 | 331 | #[test] 332 | fn test_mapblock_v25() { 333 | // Original block positioned at (-1, -1, -1). 334 | let data1 = read_test_file("mapblock_v25.bin").unwrap(); 335 | let block1 = MapBlock::deserialize(&data1).unwrap(); 336 | let data2 = block1.serialize(); 337 | let block2 = MapBlock::deserialize(&data2).unwrap(); 338 | 339 | for block in &[block1, block2] { 340 | /* Ensure that all block data is correct. */ 341 | assert_eq!(block.version, 25); 342 | assert_eq!(block.flags, 0x03); 343 | assert_eq!(block.lighting_complete, 0xFFFF); 344 | assert_eq!(block.content_width, 2); 345 | assert_eq!(block.params_width, 2); 346 | 347 | let nd = &block.node_data; 348 | let test_node_id = block.nimap.get_id(b"test_mod:stone").unwrap(); 349 | for z in &[0, 15] { 350 | for y in &[0, 15] { 351 | for x in &[0, 15] { 352 | assert_eq!(nd.nodes[x + 16 * (y + 16 * z)], test_node_id); 353 | } 354 | } 355 | } 356 | assert_eq!(nd.nodes[0x001], block.nimap.get_id(b"air").unwrap()); 357 | assert_eq!(nd.nodes[0x111], 358 | block.nimap.get_id(b"test_mod:timer").unwrap()); 359 | assert_eq!(nd.param2[0x111], 12); 360 | 361 | assert!(block.metadata.is_empty()); 362 | 363 | let obj1 = &block.static_objects[0]; 364 | assert_eq!(obj1.obj_type, 7); 365 | assert_eq!(obj1.f_pos, Vec3::new(-5, -10, -15) * 10_000); 366 | assert_eq!(obj1.data.len(), 72); 367 | 368 | let obj2 = &block.static_objects[1]; 369 | assert_eq!(obj2.obj_type, 7); 370 | assert_eq!(obj2.f_pos, Vec3::new(-14, -12, -10) * 10_000); 371 | assert_eq!(obj2.data.len(), 54); 372 | 373 | assert_eq!(block.timestamp, 2529); 374 | 375 | assert_eq!(block.nimap.0[&0], b"test_mod:stone"); 376 | assert_eq!(block.nimap.0[&1], b"air"); 377 | assert_eq!(block.nimap.0[&2], b"test_mod:timer"); 378 | 379 | assert_eq!(block.node_timers[0].pos, 0x111); 380 | assert_eq!(block.node_timers[0].timeout, 1337); 381 | assert_eq!(block.node_timers[0].elapsed, 0); 382 | } 383 | } 384 | 385 | #[test] 386 | fn test_failures() { 387 | let data = read_test_file("mapblock_v28.bin").unwrap(); 388 | 389 | // Change specific parts of the serialized data and make sure 390 | // MapBlock::deserialize() catches the errors. Something like a hex 391 | // editor is needed to follow along. 392 | 393 | let check_error = 394 | |modder: fn(&mut [u8]), expected_error: MapBlockError| 395 | { 396 | let mut copy = data.clone(); 397 | modder(&mut copy); 398 | assert_eq!(MapBlock::deserialize(©).unwrap_err(), 399 | expected_error); 400 | }; 401 | 402 | // Invalid versions 403 | check_error(|d| d[0x0] = 24, MapBlockError::InvalidBlockVersion); 404 | check_error(|d| d[0x0] = 30, MapBlockError::InvalidBlockVersion); 405 | // Invalid content width 406 | check_error(|d| d[0x4] = 1, MapBlockError::InvalidFeature); 407 | // Invalid parameter width 408 | check_error(|d| d[0x5] = 3, MapBlockError::InvalidFeature); 409 | // Invalid static object version 410 | check_error(|d| d[0xA9] = 1, MapBlockError::InvalidSubVersion); 411 | // Invalid name-ID map version 412 | check_error(|d| d[0x15D] = 1, MapBlockError::InvalidSubVersion); 413 | // Invalid node timer data length 414 | check_error(|d| d[0x179] = 12, MapBlockError::InvalidFeature); 415 | 416 | { // Invalid node data size 417 | let mut block = MapBlock::deserialize(&data).unwrap(); 418 | block.node_data.param1.push(0); 419 | let new_data = block.serialize(); 420 | assert_eq!(MapBlock::deserialize(&new_data).unwrap_err(), 421 | MapBlockError::BadData); 422 | 423 | block.node_data.param1.truncate(4095); 424 | let new_data = block.serialize(); 425 | assert_eq!(MapBlock::deserialize(&new_data).unwrap_err(), 426 | MapBlockError::BadData); 427 | } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/map_block/metadata.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use std::collections::{HashMap, BTreeMap}; 4 | use std::cmp::min; 5 | 6 | use memmem::{Searcher, TwoWaySearcher}; 7 | use flate2::write::ZlibEncoder; 8 | use flate2::read::ZlibDecoder; 9 | use flate2::Compression; 10 | 11 | 12 | const END_STR: &[u8; 13] = b"EndInventory\n"; 13 | 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct NodeMetadata { 17 | pub vars: HashMap, (Vec, bool)>, 18 | pub inv: Vec 19 | } 20 | 21 | impl NodeMetadata { 22 | fn deserialize(src: &mut Cursor<&[u8]>, version: u8) 23 | -> Result 24 | { 25 | let var_count = src.read_u32::()?; 26 | // Avoid allocating huge numbers of variables (bad data handling). 27 | let mut vars = HashMap::with_capacity(min(var_count as usize, 64)); 28 | 29 | for _ in 0..var_count { 30 | let name = read_string16(src)?; 31 | let val = read_string32(src)?; 32 | let private = if version >= 2 { 33 | src.read_u8()? != 0 34 | } else { false }; 35 | vars.insert(name.clone(), (val, private)); 36 | } 37 | 38 | let end_finder = TwoWaySearcher::new(END_STR); 39 | // This should be safe; EndInventory\n cannot appear in item metadata 40 | // since newlines are escaped. 41 | let end = end_finder 42 | .search_in(&src.get_ref()[src.position() as usize ..]) 43 | .ok_or(MapBlockError::BadData)?; 44 | 45 | let mut inv = vec_with_len(end + END_STR.len()); 46 | src.read_exact(&mut inv)?; 47 | 48 | Ok(Self { vars, inv }) 49 | } 50 | 51 | fn serialize(&self, dst: &mut T, version: u8) { 52 | dst.write_u32::(self.vars.len() as u32).unwrap(); 53 | 54 | for (name, (val, private)) in &self.vars { 55 | write_string16(dst, name); 56 | write_string32(dst, &val); 57 | if version >= 2 { 58 | dst.write_u8(*private as u8).unwrap(); 59 | } 60 | } 61 | 62 | dst.write_all(&self.inv).unwrap(); 63 | } 64 | 65 | /// Return `true` if the metadata contains no variables or inventory lists. 66 | fn is_empty(&self) -> bool { 67 | self.vars.is_empty() && self.inv.starts_with(END_STR) 68 | } 69 | } 70 | 71 | 72 | pub trait NodeMetadataListExt { 73 | fn deserialize(src: &mut Cursor<&[u8]>) -> Result 74 | where Self: std::marker::Sized; 75 | fn decompress(src: &mut Cursor<&[u8]>) -> Result 76 | where Self: std::marker::Sized; 77 | fn serialize(&self, dst: &mut T, block_version: u8); 78 | fn compress(&self, dst: &mut T, block_version: u8); 79 | } 80 | 81 | 82 | pub type NodeMetadataList = BTreeMap; 83 | 84 | impl NodeMetadataListExt for NodeMetadataList { 85 | fn deserialize(src: &mut Cursor<&[u8]>) -> Result { 86 | let version = src.read_u8()?; 87 | if version > 2 { 88 | return Err(MapBlockError::InvalidSubVersion) 89 | } 90 | 91 | let count = match version { 92 | 0 => 0, 93 | _ => src.read_u16::()? 94 | }; 95 | 96 | let mut list = BTreeMap::new(); 97 | for _ in 0..count { 98 | let pos = src.read_u16::()?; 99 | let meta = NodeMetadata::deserialize(src, version)?; 100 | list.insert(pos, meta); 101 | } 102 | 103 | Ok(list) 104 | } 105 | 106 | fn decompress(src: &mut Cursor<&[u8]>) -> Result { 107 | let start = src.position(); 108 | let mut decoder = ZlibDecoder::new(src); 109 | let mut buf = Vec::new(); 110 | decoder.read_to_end(&mut buf)?; 111 | 112 | let mut cursor = Cursor::new(buf.as_slice()); 113 | let metadata = Self::deserialize(&mut cursor)?; 114 | 115 | // Fail if there is leftover compressed data. 116 | if decoder.read(&mut [0])? > 0 { 117 | return Err(MapBlockError::BadData); 118 | } 119 | 120 | let total_in = decoder.total_in(); 121 | let src = decoder.into_inner(); 122 | src.set_position(start + total_in); 123 | 124 | Ok(metadata) 125 | } 126 | 127 | fn serialize(&self, dst: &mut T, block_version: u8) { 128 | let count = self.iter().filter(|&(_, m)| !m.is_empty()).count(); 129 | 130 | if count == 0 { 131 | dst.write_u8(0).unwrap(); 132 | } else { 133 | let version = if block_version >= 28 { 2 } else { 1 }; 134 | dst.write_u8(version).unwrap(); 135 | dst.write_u16::(count as u16).unwrap(); 136 | 137 | for (&pos, meta) in self { 138 | if !meta.is_empty() { 139 | dst.write_u16::(pos).unwrap(); 140 | meta.serialize(dst, version); 141 | } 142 | } 143 | } 144 | } 145 | 146 | fn compress(&self, dst: &mut T, block_version: u8) { 147 | let mut encoder = ZlibEncoder::new(dst, Compression::default()); 148 | self.serialize(&mut encoder, block_version); 149 | encoder.finish().unwrap(); 150 | } 151 | } 152 | 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use super::*; 157 | 158 | fn meta_deserialize_slice(src: &[u8]) 159 | -> Result 160 | { 161 | NodeMetadataList::deserialize(&mut Cursor::new(src)) 162 | } 163 | 164 | fn meta_serialize_slice(meta: NodeMetadataList, version: u8) -> Vec { 165 | let mut cursor = Cursor::new(Vec::new()); 166 | meta.serialize(&mut cursor, version); 167 | cursor.into_inner() 168 | } 169 | 170 | #[test] 171 | fn test_meta_serialize() { 172 | // Test empty metadata lists 173 | assert!(meta_deserialize_slice(b"\x00").unwrap().is_empty()); 174 | for &ver in &[25, 29] { 175 | assert_eq!(meta_serialize_slice(NodeMetadataList::new(), ver), b"\x00"); 176 | } 177 | 178 | // Test serialization/deserialization and filtering of empty metadata. 179 | let meta_in = b"\x02\x00\x04\ 180 | \x00\x10\x00\x00\x00\x01\x00\x08formspec\x00\x00\x00\x24size[4,1]\ 181 | list[context;main;0,0;4,1;]\x00List main 4\nWidth 0\nEmpty\n\ 182 | Empty\nItem basenodes:cobble 1 0 \"\\u0001check\\u0002\ 183 | EndInventory\\n\\u0003\"\nEmpty\nEndInventoryList\n\ 184 | EndInventory\n\ 185 | \x0e\x21\x00\x00\x00\x01\x00\x06secret\x00\x00\x00\x0a\x01pa55w0rd\ 186 | \x02\x01EndInventory\n\ 187 | \x03\x23\x00\x00\x00\x00EndInventory\n\ 188 | \x0f\xff\x00\x00\x00\x00List main 1\nWidth 0\nItem basenodes:dirt_\ 189 | with_grass 10\nEndInventoryList\nEndInventory\n"; 190 | 191 | let meta_out = b"\x02\x00\x03\ 192 | \x00\x10\x00\x00\x00\x01\x00\x08formspec\x00\x00\x00\x24size[4,1]\ 193 | list[context;main;0,0;4,1;]\x00List main 4\nWidth 0\nEmpty\n\ 194 | Empty\nItem basenodes:cobble 1 0 \"\\u0001check\\u0002\ 195 | EndInventory\\n\\u0003\"\nEmpty\nEndInventoryList\n\ 196 | EndInventory\n\ 197 | \x0e\x21\x00\x00\x00\x01\x00\x06secret\x00\x00\x00\x0a\x01pa55w0rd\ 198 | \x02\x01EndInventory\n\ 199 | \x0f\xff\x00\x00\x00\x00List main 1\nWidth 0\nItem basenodes:dirt_\ 200 | with_grass 10\nEndInventoryList\nEndInventory\n"; 201 | 202 | let meta_list = meta_deserialize_slice(&meta_in[..]).unwrap(); 203 | assert_eq!(meta_list.len(), 4); 204 | assert_eq!(meta_list[&0x010].vars[&b"formspec"[..]].1, false); 205 | assert_eq!(meta_list[&0xe21].vars[&b"secret"[..]].1, true); 206 | // There is one empty variable which should be deleted. 207 | assert_eq!(meta_serialize_slice(meta_list, 29), meta_out); 208 | 209 | // Test currently unsupported version 210 | let mut meta_future = meta_in.to_vec(); 211 | meta_future[0] = b'\x03'; 212 | assert_eq!( 213 | meta_deserialize_slice(&meta_future[..]).unwrap_err(), 214 | MapBlockError::InvalidSubVersion 215 | ); 216 | 217 | // Test old version 218 | let meta_v1 = b"\x01\x00\x02\ 219 | \x00\x10\x00\x00\x00\x01\x00\x08formspec\x00\x00\x00\x24size[4,1]\ 220 | list[context;main;0,0;4,1;]List main 4\nWidth 0\nEmpty\n\ 221 | Empty\nItem basenodes:cobble\nEmpty\nEndInventoryList\n\ 222 | EndInventory\n\ 223 | \x0d\xb7\x00\x00\x00\x00List main 1\nWidth 0\nItem basenodes:dirt_\ 224 | with_grass 10\nEndInventoryList\nEndInventory\n"; 225 | 226 | let meta_list_v1 = 227 | meta_deserialize_slice(&meta_v1[..]).unwrap(); 228 | assert_eq!(meta_list_v1.len(), 2); 229 | assert_eq!(meta_list_v1[&0x010].vars[&b"formspec"[..]].1, false); 230 | assert_eq!(meta_serialize_slice(meta_list_v1, 25), meta_v1); 231 | 232 | // Test missing inventory 233 | let missing_inv = b"\x02\x00\x02\ 234 | \x01\x23\x00\x00\x00\x01\ 235 | \x00\x03foo\x00\x00\x00\x03bar\x00 236 | \x0f\xed\x00\x00\x00\x01\ 237 | \x00\x0dfake_inv_test\x00\x00\x00\x0cEndInventory\x00"; 238 | assert_eq!(meta_deserialize_slice(missing_inv).unwrap_err(), 239 | MapBlockError::BadData); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/map_block/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::io::Cursor; 3 | use std::convert::TryFrom; 4 | 5 | use byteorder::{ByteOrder, BigEndian, ReadBytesExt, WriteBytesExt}; 6 | 7 | mod map_block; 8 | mod node_data; 9 | mod metadata; 10 | mod static_object; 11 | mod node_timer; 12 | mod name_id_map; 13 | 14 | pub use map_block::{MapBlock, is_valid_generated}; 15 | pub use node_data::NodeData; 16 | pub use metadata::{NodeMetadataList, NodeMetadataListExt}; 17 | pub use static_object::{StaticObject, StaticObjectList, LuaEntityData}; 18 | use static_object::{serialize_objects, deserialize_objects}; 19 | pub use node_timer::{NodeTimer, NodeTimerList}; 20 | use node_timer::{serialize_timers, deserialize_timers}; 21 | pub use name_id_map::NameIdMap; 22 | 23 | 24 | #[derive(Clone, Debug, PartialEq)] 25 | pub enum MapBlockError { 26 | /// Block data is malformed or missing. 27 | BadData, 28 | /// The block version is unsupported. 29 | InvalidBlockVersion, 30 | /// Some data length or other value is unsupported. 31 | InvalidFeature, 32 | /// Some content within the mapblock has an unsupported version. 33 | InvalidSubVersion, 34 | } 35 | 36 | impl From for MapBlockError { 37 | fn from(_: std::io::Error) -> Self { 38 | Self::BadData 39 | } 40 | } 41 | 42 | 43 | fn vec_with_len(len: usize) -> Vec { 44 | let mut v = Vec::with_capacity(len); 45 | unsafe { v.set_len(len) } 46 | v 47 | } 48 | 49 | 50 | /// Return `n` bytes of data from `src`. Will fail safely if there are not 51 | /// enough bytes in `src`. 52 | #[inline(always)] 53 | fn try_read_n(src: &mut Cursor<&[u8]>, n: usize) 54 | -> Result, MapBlockError> 55 | { 56 | if src.get_ref().len() - (src.position() as usize) < n { 57 | // Corrupted length or otherwise not enough bytes to fill buffer. 58 | Err(MapBlockError::BadData) 59 | } else { 60 | let mut bytes = vec_with_len(n); 61 | src.read_exact(&mut bytes)?; 62 | Ok(bytes) 63 | } 64 | } 65 | 66 | 67 | fn read_string16(src: &mut Cursor<&[u8]>) -> Result, MapBlockError> { 68 | let count = src.read_u16::()?; 69 | try_read_n(src, count as usize) 70 | } 71 | 72 | 73 | fn read_string32(src: &mut Cursor<&[u8]>) -> Result, MapBlockError> { 74 | let count = src.read_u32::()?; 75 | try_read_n(src, count as usize) 76 | } 77 | 78 | 79 | fn write_string16(dst: &mut T, data: &[u8]) { 80 | let len = u16::try_from(data.len()).unwrap(); 81 | dst.write_u16::(len).unwrap(); 82 | dst.write(data).unwrap(); 83 | } 84 | 85 | 86 | fn write_string32(dst: &mut T, data: &[u8]) { 87 | let len = u32::try_from(data.len()).unwrap(); 88 | dst.write_u32::(len).unwrap(); 89 | dst.write(data).unwrap(); 90 | } 91 | 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use super::*; 96 | 97 | #[test] 98 | #[should_panic] 99 | fn test_string16_overflow() { 100 | let mut buf = Cursor::new(Vec::new()); 101 | let long = (0..128).collect::>().repeat(512); 102 | write_string16(&mut buf, &long); 103 | } 104 | 105 | #[test] 106 | fn test_string_serialization() { 107 | let mut buf = Cursor::new(Vec::new()); 108 | let long_string = b"lorem ipsum dolor sin amet ".repeat(10); 109 | let huge_string = 110 | b"There are only so many strings that have exactly 64 characters. " 111 | .repeat(1024); 112 | 113 | write_string16(&mut buf, b""); 114 | write_string16(&mut buf, &long_string); 115 | write_string32(&mut buf, b""); 116 | write_string32(&mut buf, &huge_string); 117 | 118 | let mut res = Vec::new(); 119 | res.extend_from_slice(b"\x00\x00"); 120 | res.extend_from_slice(b"\x01\x0E"); 121 | res.extend_from_slice(&long_string); 122 | res.extend_from_slice(b"\x00\x00\x00\x00"); 123 | res.extend_from_slice(b"\x00\x01\x00\x00"); 124 | res.extend_from_slice(&huge_string); 125 | 126 | assert_eq!(buf.into_inner(), res); 127 | } 128 | 129 | #[test] 130 | fn test_string_deserialization() { 131 | let huge_string = 132 | b"Magic purple goats can eat up to 30 kg of purple hay every day. " 133 | .repeat(1024); 134 | 135 | let mut buf = Vec::new(); 136 | buf.extend_from_slice(b"\x00\x00"); 137 | buf.extend_from_slice(b"\x00\x0DHello, world!"); 138 | buf.extend_from_slice(b"\x00\x01\x00\x00"); 139 | buf.extend_from_slice(&huge_string); 140 | buf.extend_from_slice(b"\x00\x00\x00\x00"); 141 | 142 | let mut cursor = Cursor::new(&buf[..]); 143 | 144 | fn contains(res: Result, E>, val: &[u8]) -> bool { 145 | if let Ok(inner) = res { 146 | inner == val 147 | } else { 148 | false 149 | } 150 | } 151 | 152 | assert!(contains(read_string16(&mut cursor), b"")); 153 | assert!(contains(read_string16(&mut cursor), b"Hello, world!")); 154 | assert!(contains(read_string32(&mut cursor), &huge_string)); 155 | assert!(contains(read_string32(&mut cursor), b"")); 156 | 157 | let bad_string16s: &[&[u8]] = &[ 158 | b"", 159 | b"\xFF", 160 | b"\x00\x01", 161 | b"\x00\x2D actual data length < specified data length!", 162 | ]; 163 | for &bad in bad_string16s { 164 | assert_eq!(read_string16(&mut Cursor::new(&bad)), 165 | Err(MapBlockError::BadData)); 166 | } 167 | 168 | let bad_string32s: &[&[u8]] = &[ 169 | b"", 170 | b"\x00\x00", 171 | b"\x00\x00\x00\x01", 172 | b"\xFF\xFF\xFF\xFF", 173 | b"\x00\x00\x00\x2D actual data length < specified data length!", 174 | ]; 175 | for &bad in bad_string32s { 176 | assert_eq!(read_string32(&mut Cursor::new(&bad)), 177 | Err(MapBlockError::BadData)); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/map_block/name_id_map.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::collections::BTreeMap; 3 | 4 | 5 | /// Maps 16-bit node IDs to actual node names. 6 | /// 7 | /// Relevant Minetest source file: /src/nameidmapping.cpp 8 | #[derive(Clone, Debug)] 9 | pub struct NameIdMap(pub BTreeMap>); 10 | 11 | impl NameIdMap { 12 | pub fn deserialize(src: &mut Cursor<&[u8]>) -> Result { 13 | let version = src.read_u8()?; 14 | if version != 0 { 15 | return Err(MapBlockError::InvalidSubVersion); 16 | } 17 | 18 | let count = src.read_u16::()? as usize; 19 | let mut map = BTreeMap::new(); 20 | 21 | for _ in 0..count { 22 | let id = src.read_u16::()?; 23 | let name = read_string16(src)?; 24 | map.insert(id, name); 25 | } 26 | 27 | Ok(Self(map)) 28 | } 29 | 30 | pub fn serialize(&self, dst: &mut T) { 31 | dst.write_u8(0).unwrap(); 32 | dst.write_u16::(self.0.len() as u16).unwrap(); 33 | 34 | for (&id, name) in &self.0 { 35 | dst.write_u16::(id).unwrap(); 36 | write_string16(dst, name); 37 | } 38 | } 39 | 40 | #[inline] 41 | pub fn get_id(&self, name: &[u8]) -> Option { 42 | self.0.iter().find_map(|(&k, v)| 43 | if v.as_slice() == name { Some(k) } else { None } 44 | ) 45 | } 46 | 47 | #[inline] 48 | pub fn get_max_id(&self) -> Option { 49 | self.0.iter().next_back().map(|(&k, _)| k) 50 | } 51 | 52 | /// Remove the name at a given ID and shift down any values above it. 53 | pub fn remove_shift(&mut self, id: u16) { 54 | self.0.remove(&id); 55 | for k in id + 1 ..= self.get_max_id().unwrap_or(0) { 56 | if let Some(name) = self.0.remove(&k) { 57 | self.0.insert(k - 1, name); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/map_block/node_data.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use flate2::write::ZlibEncoder; 4 | use flate2::read::ZlibDecoder; 5 | use flate2::Compression; 6 | 7 | 8 | const BLOCK_SIZE: usize = 16; 9 | const NODE_COUNT: usize = BLOCK_SIZE * BLOCK_SIZE * BLOCK_SIZE; 10 | 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct NodeData { 14 | pub nodes: Vec, 15 | pub param1: Vec, 16 | pub param2: Vec 17 | } 18 | 19 | impl NodeData { 20 | pub fn deserialize(src: &mut T) -> Result { 21 | let mut node_bytes = vec_with_len(NODE_COUNT * 2); 22 | src.read_exact(&mut node_bytes)?; 23 | let mut nodes = vec_with_len(NODE_COUNT); 24 | BigEndian::read_u16_into(&node_bytes, &mut nodes); 25 | 26 | let mut param1 = vec_with_len(NODE_COUNT); 27 | src.read_exact(&mut param1)?; 28 | 29 | let mut param2 = vec_with_len(NODE_COUNT); 30 | src.read_exact(&mut param2)?; 31 | 32 | Ok(Self { 33 | nodes, 34 | param1, 35 | param2 36 | }) 37 | } 38 | 39 | pub fn decompress(src: &mut Cursor<&[u8]>) -> Result { 40 | let start = src.position(); 41 | let mut decoder = ZlibDecoder::new(src); 42 | 43 | let node_data = Self::deserialize(&mut decoder)?; 44 | 45 | // Fail if there is leftover compressed data. 46 | if decoder.read(&mut [0])? > 0 { 47 | return Err(MapBlockError::BadData); 48 | } 49 | 50 | let total_in = decoder.total_in(); 51 | let src = decoder.into_inner(); 52 | src.set_position(start + total_in); 53 | 54 | Ok(node_data) 55 | } 56 | 57 | pub fn serialize(&self, dst: &mut T) { 58 | // This allocation seems slow, but writing u16s iteratively is slower. 59 | let mut node_bytes = vec_with_len(NODE_COUNT * 2); 60 | BigEndian::write_u16_into(&self.nodes, 61 | &mut node_bytes[..NODE_COUNT * 2]); 62 | 63 | dst.write_all(&node_bytes).unwrap(); 64 | dst.write_all(&self.param1).unwrap(); 65 | dst.write_all(&self.param2).unwrap(); 66 | } 67 | 68 | pub fn compress(&self, dst: &mut T) { 69 | let mut encoder = ZlibEncoder::new(dst, Compression::default()); 70 | self.serialize(&mut encoder); 71 | encoder.finish().unwrap(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/map_block/node_timer.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::cmp::min; 3 | 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct NodeTimer { 7 | pub pos: u16, 8 | pub timeout: u32, 9 | pub elapsed: u32 10 | } 11 | 12 | 13 | pub type NodeTimerList = Vec; 14 | 15 | 16 | pub fn deserialize_timers(src: &mut T) 17 | -> Result 18 | { 19 | let data_len = src.read_u8()?; 20 | if data_len != 10 { 21 | return Err(MapBlockError::InvalidFeature); 22 | } 23 | 24 | let count = src.read_u16::()?; 25 | // Limit allocation to number of nodes (bad data handling). 26 | let mut timers = Vec::with_capacity(min(count, 4096) as usize); 27 | 28 | for _ in 0..count { 29 | let pos = src.read_u16::()?; 30 | let timeout = src.read_u32::()?; 31 | let elapsed = src.read_u32::()?; 32 | timers.push(NodeTimer {pos, timeout, elapsed}); 33 | } 34 | 35 | Ok(timers) 36 | } 37 | 38 | 39 | pub fn serialize_timers(timers: &NodeTimerList, dst: &mut T) { 40 | dst.write_u8(10).unwrap(); 41 | dst.write_u16::(timers.len() as u16).unwrap(); 42 | 43 | for t in timers { 44 | dst.write_u16::(t.pos).unwrap(); 45 | dst.write_u32::(t.timeout).unwrap(); 46 | dst.write_u32::(t.elapsed).unwrap(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/map_block/static_object.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::spatial::Vec3; 3 | use std::cmp::min; 4 | 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct StaticObject { 8 | pub obj_type: u8, 9 | pub f_pos: Vec3, 10 | pub data: Vec 11 | } 12 | 13 | impl StaticObject { 14 | fn deserialize(src: &mut Cursor<&[u8]>) -> Result { 15 | let obj_type = src.read_u8()?; 16 | let f_pos = Vec3::new( 17 | src.read_i32::()?, 18 | src.read_i32::()?, 19 | src.read_i32::()? 20 | ); 21 | let data = read_string16(src)?; 22 | Ok(Self {obj_type, f_pos, data}) 23 | } 24 | 25 | fn serialize(&self, dst: &mut T) { 26 | dst.write_u8(self.obj_type).unwrap(); 27 | dst.write_i32::(self.f_pos.x).unwrap(); 28 | dst.write_i32::(self.f_pos.y).unwrap(); 29 | dst.write_i32::(self.f_pos.z).unwrap(); 30 | write_string16(dst, &self.data); 31 | } 32 | } 33 | 34 | 35 | pub type StaticObjectList = Vec; 36 | 37 | 38 | pub fn deserialize_objects(src: &mut Cursor<&[u8]>) 39 | -> Result 40 | { 41 | let version = src.read_u8()?; 42 | if version != 0 { 43 | return Err(MapBlockError::InvalidSubVersion); 44 | } 45 | 46 | let count = src.read_u16::()?; 47 | // Limit allocation to MT's default max object count (bad data handling). 48 | let mut list = Vec::with_capacity(min(count, 64) as usize); 49 | for _ in 0..count { 50 | list.push(StaticObject::deserialize(src)?); 51 | } 52 | 53 | Ok(list) 54 | } 55 | 56 | 57 | pub fn serialize_objects(objects: &StaticObjectList, dst: &mut T) 58 | { 59 | dst.write_u8(0).unwrap(); 60 | dst.write_u16::(objects.len() as u16).unwrap(); 61 | for obj in objects { 62 | obj.serialize(dst); 63 | } 64 | } 65 | 66 | 67 | /// Stores the name and data of a LuaEntity (Minetest's standard entity type). 68 | /// 69 | /// Relevant Minetest source file: src/server/luaentity_sao.cpp 70 | #[derive(Debug)] 71 | pub struct LuaEntityData { 72 | pub name: Vec, 73 | pub data: Vec 74 | } 75 | 76 | impl LuaEntityData { 77 | pub fn deserialize(src: &StaticObject) -> Result { 78 | if src.obj_type != 7 { 79 | return Err(MapBlockError::InvalidFeature); 80 | } 81 | let mut src_data = Cursor::new(src.data.as_slice()); 82 | if src_data.read_u8()? != 1 { 83 | // Unsupported LuaEntity version 84 | return Err(MapBlockError::InvalidSubVersion); 85 | } 86 | 87 | let name = read_string16(&mut src_data)?; 88 | let data = read_string32(&mut src_data)?; 89 | Ok(Self {name, data}) 90 | } 91 | } 92 | 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | 98 | #[test] 99 | fn test_lua_entity() { 100 | let test_obj = StaticObject { 101 | obj_type: 7, 102 | f_pos: Vec3::new(4380, 17279, 32630), 103 | data: b"\x01\x00\x0e__builtin:item\x00\x00\x00\x6e\ 104 | return {[\"age\"] = 0.91899997927248478, \ 105 | [\"itemstring\"] = \"basenodes:cobble 2\", \ 106 | [\"dropped_by\"] = \"singleplayer\"}\ 107 | \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ 108 | \x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00".to_vec() 109 | }; 110 | let entity = LuaEntityData::deserialize(&test_obj).unwrap(); 111 | assert_eq!(entity.name, b"__builtin:item"); 112 | assert_eq!(entity.data, 113 | b"return {[\"age\"] = 0.91899997927248478, \ 114 | [\"itemstring\"] = \"basenodes:cobble 2\", \ 115 | [\"dropped_by\"] = \"singleplayer\"}"); 116 | 117 | let mut wrong_version = test_obj.clone(); 118 | wrong_version.data[0] = 0; 119 | assert_eq!(LuaEntityData::deserialize(&wrong_version).unwrap_err(), 120 | MapBlockError::InvalidSubVersion); 121 | 122 | let wrong_type = StaticObject { obj_type: 6, ..test_obj }; 123 | assert_eq!(LuaEntityData::deserialize(&wrong_type).unwrap_err(), 124 | MapBlockError::InvalidFeature); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/map_database.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum DBError { 3 | #[error("database operation failed")] 4 | DatabaseError, 5 | #[error("database is not a valid map database")] 6 | InvalidDatabase, 7 | #[error("requested data was not found")] 8 | MissingData, 9 | } 10 | 11 | impl From for DBError { 12 | fn from(_: sqlite::Error) -> Self { 13 | Self::DatabaseError 14 | } 15 | } 16 | 17 | 18 | fn verify_database(conn: &sqlite::Connection) -> Result<(), DBError> { 19 | let my_assert = |res: bool| -> Result<(), DBError> { 20 | match res { 21 | true => Ok(()), 22 | false => Err(DBError::InvalidDatabase) 23 | } 24 | }; 25 | 26 | let mut stmt = conn.prepare("PRAGMA table_info(blocks)")?; 27 | 28 | stmt.next()?; 29 | my_assert(stmt.read::(1)? == "pos")?; 30 | my_assert(stmt.read::(2)? == "INT")?; 31 | my_assert(stmt.read::(5)? == 1)?; 32 | stmt.next()?; 33 | my_assert(stmt.read::(1)? == "data")?; 34 | my_assert(stmt.read::(2)? == "BLOB")?; 35 | my_assert(stmt.read::(5)? == 0)?; 36 | 37 | Ok(()) 38 | } 39 | 40 | 41 | pub struct MapDatabaseRows<'a> { 42 | stmt_get: sqlite::Statement<'a> 43 | } 44 | 45 | impl Iterator for MapDatabaseRows<'_> { 46 | type Item = (i64, Vec); 47 | 48 | fn next(&mut self) -> Option { 49 | match self.stmt_get.next().unwrap() { 50 | sqlite::State::Row => { 51 | Some(( 52 | self.stmt_get.read(0).unwrap(), 53 | self.stmt_get.read(1).unwrap() 54 | )) 55 | }, 56 | sqlite::State::Done => None 57 | } 58 | } 59 | } 60 | 61 | 62 | pub struct MapDatabase<'a> { 63 | conn: &'a sqlite::Connection, 64 | stmt_get: sqlite::Statement<'a>, 65 | stmt_set: sqlite::Statement<'a>, 66 | stmt_del: sqlite::Statement<'a>, 67 | in_transaction: bool, 68 | } 69 | 70 | impl<'a> MapDatabase<'a> { 71 | pub fn new(conn: &'a sqlite::Connection) -> Result { 72 | conn.execute("BEGIN")?; 73 | verify_database(conn)?; 74 | 75 | let stmt_get = conn.prepare("SELECT data FROM blocks WHERE pos = ?")?; 76 | let stmt_set = conn.prepare( 77 | "INSERT OR REPLACE INTO blocks (pos, data) VALUES (?, ?)")?; 78 | let stmt_del = conn.prepare("DELETE FROM blocks WHERE pos = ?")?; 79 | 80 | Ok(Self {conn, stmt_get, stmt_set, stmt_del, in_transaction: true}) 81 | } 82 | 83 | pub fn is_in_transaction(&self) -> bool { 84 | self.in_transaction 85 | } 86 | 87 | #[inline] 88 | fn begin_if_needed(&self) -> Result<(), DBError> { 89 | if !self.in_transaction { 90 | self.conn.execute("BEGIN")?; 91 | } 92 | Ok(()) 93 | } 94 | 95 | pub fn commit_if_needed(&mut self) -> Result<(), DBError> { 96 | if self.in_transaction { 97 | self.conn.execute("COMMIT")?; 98 | self.in_transaction = false; 99 | } 100 | Ok(()) 101 | } 102 | 103 | pub fn iter_rows(&self) -> MapDatabaseRows { 104 | self.begin_if_needed().unwrap(); 105 | let stmt = self.conn.prepare("SELECT pos, data FROM blocks").unwrap(); 106 | MapDatabaseRows {stmt_get: stmt} 107 | } 108 | 109 | pub fn get_block(&mut self, map_key: i64) -> Result, DBError> { 110 | self.begin_if_needed()?; 111 | self.stmt_get.bind(1, map_key)?; 112 | 113 | let value = match self.stmt_get.next()? { 114 | sqlite::State::Row => Ok(self.stmt_get.read(0)?), 115 | sqlite::State::Done => Err(DBError::MissingData) 116 | }; 117 | 118 | self.stmt_get.reset()?; 119 | value 120 | } 121 | 122 | pub fn set_block(&mut self, map_key: i64, data: &[u8]) 123 | -> Result<(), DBError> 124 | { 125 | self.begin_if_needed()?; 126 | self.stmt_set.bind(1, map_key)?; 127 | self.stmt_set.bind(2, data)?; 128 | self.stmt_set.next()?; 129 | self.stmt_set.reset()?; 130 | Ok(()) 131 | } 132 | 133 | pub fn delete_block(&mut self, map_key: i64) -> Result<(), DBError> { 134 | self.begin_if_needed()?; 135 | self.stmt_del.bind(1, map_key)?; 136 | self.stmt_del.next()?; 137 | self.stmt_del.reset()?; 138 | Ok(()) 139 | } 140 | 141 | pub fn vacuum(&mut self) -> Result<(), DBError> { 142 | self.commit_if_needed()?; 143 | self.conn.execute("VACUUM")?; 144 | Ok(()) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/spatial/area.rs: -------------------------------------------------------------------------------- 1 | use super::Vec3; 2 | use std::cmp::{min, max}; 3 | 4 | 5 | pub struct AreaIterator { 6 | min: Vec3, 7 | max: Vec3, 8 | pos: Vec3 9 | } 10 | 11 | impl AreaIterator { 12 | #[inline] 13 | pub fn new(min: Vec3, max: Vec3) -> Self { 14 | Self {min, max, pos: min} 15 | } 16 | } 17 | 18 | impl Iterator for AreaIterator { 19 | type Item = Vec3; 20 | 21 | fn next(&mut self) -> Option { 22 | if self.pos.z > self.max.z { 23 | None 24 | } else { 25 | let last_pos = self.pos; 26 | 27 | self.pos.x += 1; 28 | if self.pos.x > self.max.x { 29 | self.pos.x = self.min.x; 30 | self.pos.y += 1; 31 | if self.pos.y > self.max.y { 32 | self.pos.y = self.min.y; 33 | self.pos.z += 1; 34 | } 35 | } 36 | 37 | Some(last_pos) 38 | } 39 | } 40 | } 41 | 42 | 43 | #[derive(Clone, Copy, Debug, PartialEq)] 44 | pub struct Area { 45 | pub min: Vec3, 46 | pub max: Vec3 47 | } 48 | 49 | impl Area { 50 | pub fn is_valid(&self) -> bool { 51 | self.min.x <= self.max.x 52 | && self.min.y <= self.max.y 53 | && self.min.z <= self.max.z 54 | } 55 | 56 | pub fn new(min: Vec3, max: Vec3) -> Self { 57 | let area = Self {min, max}; 58 | assert!(area.is_valid()); 59 | area 60 | } 61 | 62 | pub fn from_unsorted(a: Vec3, b: Vec3) -> Self { 63 | Self { 64 | min: Vec3 { 65 | x: min(a.x, b.x), 66 | y: min(a.y, b.y), 67 | z: min(a.z, b.z) 68 | }, 69 | max: Vec3 { 70 | x: max(a.x, b.x), 71 | y: max(a.y, b.y), 72 | z: max(a.z, b.z) 73 | } 74 | } 75 | } 76 | 77 | pub fn volume(&self) -> u64 { 78 | (self.max.x - self.min.x + 1) as u64 * 79 | (self.max.y - self.min.y + 1) as u64 * 80 | (self.max.z - self.min.z + 1) as u64 81 | } 82 | 83 | pub fn intersection(&self, rhs: Self) -> Option { 84 | let res = Self { 85 | min: Vec3 { 86 | x: max(self.min.x, rhs.min.x), 87 | y: max(self.min.y, rhs.min.y), 88 | z: max(self.min.z, rhs.min.z) 89 | }, 90 | max: Vec3 { 91 | x: min(self.max.x, rhs.max.x), 92 | y: min(self.max.y, rhs.max.y), 93 | z: min(self.max.z, rhs.max.z) 94 | } 95 | }; 96 | Some(res).filter(Self::is_valid) 97 | } 98 | 99 | pub fn contains(&self, pos: Vec3) -> bool { 100 | self.min.x <= pos.x && pos.x <= self.max.x 101 | && self.min.y <= pos.y && pos.y <= self.max.y 102 | && self.min.z <= pos.z && pos.z <= self.max.z 103 | } 104 | 105 | pub fn contains_block(&self, block_pos: Vec3) -> bool { 106 | let corner = block_pos * 16; 107 | self.min.x <= corner.x && corner.x + 15 <= self.max.x 108 | && self.min.y <= corner.y && corner.y + 15 <= self.max.y 109 | && self.min.z <= corner.z && corner.z + 15 <= self.max.z 110 | } 111 | 112 | pub fn touches_block(&self, block_pos: Vec3) -> bool { 113 | let corner = block_pos * 16; 114 | self.min.x <= corner.x + 15 && corner.x <= self.max.x 115 | && self.min.y <= corner.y + 15 && corner.y <= self.max.y 116 | && self.min.z <= corner.z + 15 && corner.z <= self.max.z 117 | } 118 | 119 | pub fn to_contained_block_area(&self) -> Option { 120 | let contained = Self { 121 | min: Vec3 { 122 | x: (self.min.x + 15).div_euclid(16), 123 | y: (self.min.y + 15).div_euclid(16), 124 | z: (self.min.z + 15).div_euclid(16) 125 | }, 126 | max: Vec3 { 127 | x: (self.max.x - 15).div_euclid(16), 128 | y: (self.max.y - 15).div_euclid(16), 129 | z: (self.max.z - 15).div_euclid(16) 130 | } 131 | }; 132 | Some(contained).filter(Self::is_valid) 133 | } 134 | 135 | pub fn to_touching_block_area(&self) -> Self { 136 | Self { 137 | min: Vec3 { 138 | x: self.min.x.div_euclid(16), 139 | y: self.min.y.div_euclid(16), 140 | z: self.min.z.div_euclid(16) 141 | }, 142 | max: Vec3 { 143 | x: self.max.x.div_euclid(16), 144 | y: self.max.y.div_euclid(16), 145 | z: self.max.z.div_euclid(16) 146 | } 147 | } 148 | } 149 | 150 | pub fn abs_block_overlap(&self, block_pos: Vec3) -> Option { 151 | let block_min = block_pos * 16; 152 | let block_max = block_min + 15; 153 | let overlap = Area { 154 | min: Vec3 { 155 | x: max(self.min.x, block_min.x), 156 | y: max(self.min.y, block_min.y), 157 | z: max(self.min.z, block_min.z) 158 | }, 159 | max: Vec3 { 160 | x: min(self.max.x, block_max.x), 161 | y: min(self.max.y, block_max.y), 162 | z: min(self.max.z, block_max.z) 163 | } 164 | }; 165 | Some(overlap).filter(Self::is_valid) 166 | } 167 | 168 | pub fn rel_block_overlap(&self, block_pos: Vec3) -> Option { 169 | let corner = block_pos * 16; 170 | let rel_min = self.min - corner; 171 | let rel_max = self.max - corner; 172 | let overlap = Area { 173 | min: Vec3 { 174 | x: max(rel_min.x, 0), 175 | y: max(rel_min.y, 0), 176 | z: max(rel_min.z, 0) 177 | }, 178 | max: Vec3 { 179 | x: min(rel_max.x, 15), 180 | y: min(rel_max.y, 15), 181 | z: min(rel_max.z, 15) 182 | } 183 | }; 184 | Some(overlap).filter(Self::is_valid) 185 | } 186 | } 187 | 188 | impl IntoIterator for &Area { 189 | type Item = Vec3; 190 | type IntoIter = AreaIterator; 191 | 192 | fn into_iter(self) -> Self::IntoIter { 193 | AreaIterator::new(self.min, self.max) 194 | } 195 | } 196 | 197 | impl std::ops::Add for Area { 198 | type Output = Self; 199 | 200 | fn add(self, rhs: Vec3) -> Self { 201 | Self { 202 | min: self.min + rhs, 203 | max: self.max + rhs 204 | } 205 | } 206 | } 207 | 208 | impl std::ops::Sub for Area { 209 | type Output = Self; 210 | 211 | fn sub(self, rhs: Vec3) -> Self { 212 | Self { 213 | min: self.min - rhs, 214 | max: self.max - rhs 215 | } 216 | } 217 | } 218 | 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use super::*; 223 | 224 | #[test] 225 | fn test_areas() { 226 | assert_eq!(Area {min: Vec3::new(0, 3, 1), max: Vec3::new(-1, 4, -2)} 227 | .is_valid(), false); 228 | assert_eq!( 229 | Area::from_unsorted(Vec3::new(8, 0, -10), Vec3::new(-8, 0, 10)), 230 | Area::new(Vec3::new(-8, 0, -10), Vec3::new(8, 0, 10)) 231 | ); 232 | assert_eq!( 233 | Area::from_unsorted(Vec3::new(10, 80, 42), Vec3::new(10, -50, 99)), 234 | Area::new(Vec3::new(10, -50, 42), Vec3::new(10, 80, 99)) 235 | ); 236 | assert_eq!( 237 | Area::new(Vec3::new(0, 0, 0), Vec3::new(0, 0, 0)).volume(), 1); 238 | assert_eq!( 239 | Area::new( 240 | Vec3::new(1, -3000, 800), 241 | Vec3::new(4000, 999, 4799) 242 | ).volume(), 243 | 4000u64.pow(3) 244 | ); 245 | } 246 | 247 | #[test] 248 | #[should_panic] 249 | fn test_area_validity() { 250 | Area::new(Vec3::new(0, 3, 1), Vec3::new(0, 2, 3)); 251 | } 252 | 253 | #[test] 254 | fn test_area_iteration() { 255 | fn iter_area(a: Area) { 256 | let mut iter = a.into_iter(); 257 | for z in a.min.z..=a.max.z { 258 | for y in a.min.y..=a.max.y { 259 | for x in a.min.x..=a.max.x { 260 | assert_eq!(iter.next(), Some(Vec3::new(x, y, z))) 261 | } 262 | } 263 | } 264 | assert_eq!(iter.next(), None); 265 | } 266 | 267 | iter_area(Area::new(Vec3::new(-1, -1, -1), Vec3::new(-1, -1, -1))); 268 | iter_area(Area::new(Vec3::new(10, -99, 11), Vec3::new(10, -99, 12))); 269 | iter_area(Area::new(Vec3::new(0, -1, -2), Vec3::new(5, 7, 11))); 270 | } 271 | 272 | #[test] 273 | fn test_area_intersection() { 274 | let triples = [ 275 | ( 276 | Area::new(Vec3::new(0, 0, 0), Vec3::new(0, 0, 0)), 277 | Area::new(Vec3::new(1, 1, 0), Vec3::new(1, 1, 0)), 278 | None 279 | ), 280 | ( 281 | Area::new(Vec3::new(-10, -8, -10), Vec3::new(10, 8, 10)), 282 | Area::new(Vec3::new(-12, 0, -2), Vec3::new(-8, 13, 2)), 283 | Some(Area::new(Vec3::new(-10, 0, -2), Vec3::new(-8, 8, 2))) 284 | ), 285 | ( 286 | Area::new(Vec3::new(0, 0, 0), Vec3::new(2, 2, 2)), 287 | Area::new(Vec3::new(0, -1, 3), Vec3::new(2, 1, 5)), 288 | None 289 | ), 290 | ( 291 | Area::new(Vec3::new(0, -10, -10), Vec3::new(30, 30, 30)), 292 | Area::new(Vec3::new(16, 16, -10), Vec3::new(29, 29, 20)), 293 | Some(Area::new(Vec3::new(16, 16, -10), Vec3::new(29, 29, 20))) 294 | ), 295 | ]; 296 | for t in &triples { 297 | assert_eq!(t.0.intersection(t.1), t.2); 298 | assert_eq!(t.1.intersection(t.0), t.2); 299 | } 300 | } 301 | 302 | #[test] 303 | fn test_area_containment() { 304 | let area = Area::new(Vec3::new(-1, -32, 16), Vec3::new(30, -17, 54)); 305 | 306 | assert_eq!(area.contains(Vec3::new(0, -32, 32)), true); 307 | assert_eq!(area.contains(Vec3::new(30, -32, 54)), true); 308 | assert_eq!(area.contains(Vec3::new(30, -17, 55)), false); 309 | assert_eq!(area.contains(Vec3::new(-2, -30, 16)), false); 310 | 311 | let contained = Area::new(Vec3::new(0, -2, 1), Vec3::new(0, -2, 2)); 312 | let touching = Area::new(Vec3::new(-1, -2, 1), Vec3::new(1, -2, 3)); 313 | 314 | assert_eq!(area.to_contained_block_area(), Some(contained)); 315 | assert_eq!(area.to_touching_block_area(), touching); 316 | 317 | for pos in &Area::new(touching.min - 2, touching.max + 2) { 318 | assert_eq!(area.touches_block(pos), touching.contains(pos)); 319 | assert_eq!(area.contains_block(pos), contained.contains(pos)); 320 | } 321 | 322 | assert_eq!( 323 | Area::new(Vec3::new(16, 0, 1), Vec3::new(31, 15, 15)) 324 | .to_contained_block_area(), 325 | None 326 | ); 327 | } 328 | 329 | #[test] 330 | fn test_area_block_overlap() { 331 | let area = Area::new(Vec3::new(-3, -3, -3), Vec3::new(15, 15, 15)); 332 | let pairs = [ 333 | ( 334 | Vec3::new(-1, -1, -1), 335 | Some(Area::new(Vec3::new(-3, -3, -3), Vec3::new(-1, -1, -1))) 336 | ), 337 | ( 338 | Vec3::new(0, 0, 0), 339 | Some(Area::new(Vec3::new(0, 0, 0), Vec3::new(15, 15, 15))) 340 | ), 341 | (Vec3::new(1, 1, 1), None), 342 | ( 343 | Vec3::new(-1, 0, 0), 344 | Some(Area::new(Vec3::new(-3, 0, 0), Vec3::new(-1, 15, 15))) 345 | ), 346 | ]; 347 | for pair in &pairs { 348 | assert_eq!(area.abs_block_overlap(pair.0), pair.1); 349 | assert_eq!( 350 | area.rel_block_overlap(pair.0).map(|a| a + (pair.0 * 16)), 351 | pair.1 352 | ); 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/spatial/mod.rs: -------------------------------------------------------------------------------- 1 | mod vec3; 2 | mod area; 3 | 4 | pub use vec3::{MAP_LIMIT, Vec3}; 5 | pub use area::Area; 6 | 7 | 8 | /// Iterates over all the block indices that are *not* contained within an 9 | /// area, in order. 10 | pub struct InverseBlockIterator { 11 | area: Area, 12 | idx: usize, 13 | can_skip: bool, 14 | skip_pos: Vec3, 15 | skip_idx: usize, 16 | skip_len: usize, 17 | } 18 | 19 | impl InverseBlockIterator { 20 | pub fn new(area: Area) -> Self { 21 | assert!(area.min.x >= 0 && area.max.x < 16 22 | && area.min.y >= 0 && area.max.y < 16 23 | && area.min.z >= 0 && area.max.z < 16); 24 | 25 | Self { 26 | area, 27 | idx: 0, 28 | can_skip: true, 29 | skip_pos: area.min, 30 | skip_idx: 31 | (area.min.x + area.min.y * 16 + area.min.z * 256) as usize, 32 | skip_len: (area.max.x - area.min.x + 1) as usize 33 | } 34 | } 35 | } 36 | 37 | impl Iterator for InverseBlockIterator { 38 | type Item = usize; 39 | 40 | fn next(&mut self) -> Option { 41 | while self.can_skip && self.idx >= self.skip_idx { 42 | self.idx += self.skip_len; 43 | // Increment self.skip_pos, self.skip_idx. 44 | let mut sp = self.skip_pos; 45 | sp.y += 1; 46 | if sp.y > self.area.max.y { 47 | sp.y = self.area.min.y; 48 | sp.z += 1; 49 | if sp.z > self.area.max.z { 50 | // No more skips 51 | self.can_skip = false; 52 | break; 53 | } 54 | } 55 | self.skip_pos = sp; 56 | self.skip_idx = (sp.x + sp.y * 16 + sp.z * 256) as usize; 57 | } 58 | 59 | if self.idx < 4096 { 60 | let idx = self.idx; 61 | self.idx += 1; 62 | Some(idx) 63 | } else { 64 | None 65 | } 66 | } 67 | } 68 | 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use super::*; 73 | 74 | #[test] 75 | fn test_inverse_block_iterator() { 76 | let dim_pairs = [ 77 | (1, 14), // Touching neither end 78 | (1, 2), 79 | (9, 9), 80 | (1, 15), // Touching max end 81 | (11, 15), 82 | (15, 15), 83 | (0, 0), // Touching min end 84 | (0, 1), 85 | (0, 14), 86 | (0, 15), // End-to-end 87 | ]; 88 | 89 | fn test_area(area: Area) { 90 | let mut iter = InverseBlockIterator::new(area); 91 | for pos in &Area::new(Vec3::new(0, 0, 0), Vec3::new(15, 15, 15)) { 92 | if !area.contains(pos) { 93 | let idx = (pos.x + pos.y * 16 + pos.z * 256) as usize; 94 | assert_eq!(iter.next(), Some(idx)); 95 | } 96 | } 97 | assert_eq!(iter.next(), None) 98 | } 99 | 100 | for z_dims in &dim_pairs { 101 | for y_dims in &dim_pairs { 102 | for x_dims in &dim_pairs { 103 | let area = Area::new( 104 | Vec3::new(x_dims.0, y_dims.0, z_dims.0), 105 | Vec3::new(x_dims.1, y_dims.1, z_dims.1) 106 | ); 107 | test_area(area); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/spatial/vec3.rs: -------------------------------------------------------------------------------- 1 | pub const MAP_LIMIT: i32 = 31000; 2 | 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq)] 5 | pub struct Vec3 { 6 | pub x: i32, 7 | pub y: i32, 8 | pub z: i32 9 | } 10 | 11 | impl Vec3 { 12 | #[inline] 13 | pub fn new(x: i32, y: i32, z: i32) -> Self { 14 | Self {x, y, z} 15 | } 16 | 17 | pub fn from_block_key(key: i64) -> Self { 18 | let x = (key + 2048).rem_euclid(4096) - 2048; 19 | let rem = (key - x) / 4096; 20 | let y = (rem + 2048).rem_euclid(4096) - 2048; 21 | let z = (rem - y) / 4096; 22 | Self {x: x as i32, y: y as i32, z: z as i32} 23 | } 24 | 25 | pub fn to_block_key(&self) -> i64 { 26 | // Make sure values are within range. 27 | assert!(-2048 <= self.x && self.x < 2048 28 | && -2048 <= self.y && self.y < 2048 29 | && -2048 <= self.z && self.z < 2048); 30 | 31 | self.x as i64 32 | + self.y as i64 * 4096 33 | + self.z as i64 * 4096 * 4096 34 | } 35 | 36 | pub fn from_u16_key(key: u16) -> Self { 37 | Self { 38 | x: (key & 0xF) as i32, 39 | y: ((key >> 4) & 0xF) as i32, 40 | z: ((key >> 8) & 0xF) as i32 41 | } 42 | } 43 | 44 | pub fn is_valid_block_pos(&self) -> bool { 45 | const LIMIT: i32 = MAP_LIMIT / 16; 46 | 47 | -LIMIT <= self.x && self.x <= LIMIT 48 | && -LIMIT <= self.y && self.y <= LIMIT 49 | && -LIMIT <= self.z && self.z <= LIMIT 50 | } 51 | 52 | pub fn is_valid_node_pos(&self) -> bool { 53 | const LIMIT: i32 = MAP_LIMIT; 54 | 55 | -LIMIT <= self.x && self.x <= LIMIT 56 | && -LIMIT <= self.y && self.y <= LIMIT 57 | && -LIMIT <= self.z && self.z <= LIMIT 58 | } 59 | 60 | pub fn map(&self, func: F) -> Self 61 | where F: Fn(i32) -> i32 62 | { 63 | Self { 64 | x: func(self.x), 65 | y: func(self.y), 66 | z: func(self.z) 67 | } 68 | } 69 | } 70 | 71 | impl std::ops::Add for Vec3 { 72 | type Output = Self; 73 | 74 | fn add(self, rhs: Self) -> Self { 75 | Self { 76 | x: self.x + rhs.x, 77 | y: self.y + rhs.y, 78 | z: self.z + rhs.z 79 | } 80 | } 81 | } 82 | 83 | impl std::ops::Add for Vec3 { 84 | type Output = Self; 85 | 86 | fn add(self, rhs: i32) -> Self { 87 | Self { 88 | x: self.x + rhs, 89 | y: self.y + rhs, 90 | z: self.z + rhs 91 | } 92 | } 93 | } 94 | 95 | impl std::ops::Sub for Vec3 { 96 | type Output = Self; 97 | 98 | fn sub(self, rhs: Self) -> Self { 99 | Self { 100 | x: self.x - rhs.x, 101 | y: self.y - rhs.y, 102 | z: self.z - rhs.z 103 | } 104 | } 105 | } 106 | 107 | impl std::ops::Sub for Vec3 { 108 | type Output = Self; 109 | 110 | fn sub(self, rhs: i32) -> Self { 111 | Self { 112 | x: self.x - rhs, 113 | y: self.y - rhs, 114 | z: self.z - rhs 115 | } 116 | } 117 | } 118 | 119 | impl std::ops::Mul for Vec3 { 120 | type Output = Self; 121 | 122 | fn mul(self, rhs: Self) -> Self { 123 | Self { 124 | x: self.x * rhs.x, 125 | y: self.y * rhs.y, 126 | z: self.z * rhs.z 127 | } 128 | } 129 | } 130 | 131 | impl std::ops::Mul for Vec3 { 132 | type Output = Self; 133 | 134 | fn mul(self, rhs: i32) -> Self { 135 | Self { 136 | x: self.x * rhs, 137 | y: self.y * rhs, 138 | z: self.z * rhs 139 | } 140 | } 141 | } 142 | 143 | impl std::fmt::Display for Vec3 { 144 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 145 | write!(f, "({}, {}, {})", self.x, self.y, self.z) 146 | } 147 | } 148 | 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | 154 | #[test] 155 | fn test_vec3() { 156 | assert_eq!(Vec3::new(42, 0, -6000), Vec3 {x: 42, y: 0, z: -6000}); 157 | 158 | assert_eq!(Vec3::new(-31000, 50, 31000).is_valid_node_pos(), true); 159 | assert_eq!(Vec3::new(-31000, -11, 31001).is_valid_node_pos(), false); 160 | 161 | assert_eq!(Vec3::new(-1937, -5, 1101).is_valid_block_pos(), true); 162 | assert_eq!(Vec3::new(-1937, 1938, -10).is_valid_block_pos(), false); 163 | assert_eq!(Vec3::new(-1938, 4, 1900).is_valid_block_pos(), false); 164 | 165 | let exp = 3; 166 | assert_eq!(Vec3::new(-3, 4, 10).map(|n| n.pow(exp)), 167 | Vec3::new(-27, 64, 1000)); 168 | 169 | assert_eq!(format!("{}", Vec3::new(-1000, 0, 70)), "(-1000, 0, 70)"); 170 | } 171 | 172 | #[test] 173 | fn test_vec3_conversions() { 174 | /* Test block key/vector conversions */ 175 | const Y_FAC: i64 = 0x1_000; 176 | const Z_FAC: i64 = 0x1_000_000; 177 | let bk_pairs = [ 178 | // Basics 179 | (Vec3::new(0, 0, 0), 0), 180 | (Vec3::new(1, 0, 0), 1), 181 | (Vec3::new(0, 1, 0), 1 * Y_FAC), 182 | (Vec3::new(0, 0, 1), 1 * Z_FAC), 183 | // X/Y/Z Boundaries 184 | (Vec3::new(-2048, 0, 0), -2048), 185 | (Vec3::new(2047, 0, 0), 2047), 186 | (Vec3::new(0, -2048, 0), -2048 * Y_FAC), 187 | (Vec3::new(0, 2047, 0), 2047 * Y_FAC), 188 | (Vec3::new(0, 0, -2048), -2048 * Z_FAC), 189 | (Vec3::new(0, 0, 2047), 2047 * Z_FAC), 190 | // Extra spicy boundaries 191 | (Vec3::new(-42, 2047, -99), -42 + 2047 * Y_FAC + -99 * Z_FAC), 192 | (Vec3::new(64, -2048, 22), 64 + -2048 * Y_FAC + 22 * Z_FAC), 193 | (Vec3::new(2047, 555, 35), 2047 + 555 * Y_FAC + 35 * Z_FAC), 194 | (Vec3::new(-2048, 600, -70), -2048 + 600 * Y_FAC + -70 * Z_FAC), 195 | // Multiple boundaries 196 | (Vec3::new(2047, -2048, 16), 2047 + -2048 * Y_FAC + 16 * Z_FAC), 197 | (Vec3::new(-2048, 2047, 50), -2048 + 2047 * Y_FAC + 50 * Z_FAC), 198 | ]; 199 | 200 | for pair in &bk_pairs { 201 | assert_eq!(pair.0.to_block_key(), pair.1); 202 | assert_eq!(pair.0, Vec3::from_block_key(pair.1)); 203 | } 204 | 205 | /* Test u16/vector conversions */ 206 | let u16_pairs = [ 207 | (Vec3::new(0, 0, 0), 0x000), 208 | (Vec3::new(1, 0, 0), 0x001), 209 | (Vec3::new(0, 1, 0), 0x010), 210 | (Vec3::new(0, 0, 1), 0x100), 211 | (Vec3::new(15, 15, 15), 0xFFF), 212 | (Vec3::new(5, 15, 9), 0x9F5) 213 | ]; 214 | 215 | for pair in &u16_pairs { 216 | assert_eq!(pair.0, Vec3::from_u16_key(pair.1)); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/testing.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time::{Instant, Duration}; 3 | 4 | use crate::instance::StatusServer; 5 | 6 | 7 | pub struct Timer<'a> { 8 | parent: &'a mut TimeKeeper, 9 | name: String, 10 | start: Instant 11 | } 12 | 13 | impl<'a> Drop for Timer<'a> { 14 | fn drop(&mut self) { 15 | let elapsed = Instant::now().duration_since(self.start); 16 | self.parent.add_time(&self.name, elapsed); 17 | } 18 | } 19 | 20 | 21 | pub struct TimeKeeper { 22 | times: HashMap 23 | } 24 | 25 | impl TimeKeeper { 26 | pub fn new() -> Self { 27 | Self {times: HashMap::new()} 28 | } 29 | 30 | fn add_time(&mut self, name: &str, elapsed: Duration) { 31 | if let Some(item) = self.times.get_mut(name) { 32 | (*item).0 += elapsed; 33 | (*item).1 += 1; 34 | } else { 35 | self.times.insert(name.to_string(), (elapsed, 1)); 36 | } 37 | } 38 | 39 | pub fn get_timer(&mut self, name: &str) -> Timer { 40 | Timer {parent: self, name: name.to_string(), start: Instant::now()} 41 | } 42 | 43 | pub fn print(&mut self, status: &StatusServer) { 44 | let mut msg = String::new(); 45 | for (name, (duration, count)) in &self.times { 46 | msg += &format!("{}: {} x {:?} each; {:?} total\n", 47 | name, count, *duration / *count, duration); 48 | } 49 | status.log_info(msg); 50 | } 51 | } 52 | 53 | 54 | pub fn debug_bytes(src: &[u8]) -> String { 55 | let mut dst = String::new(); 56 | for &byte in src { 57 | if byte == b'\\' { 58 | dst += "\\\\"; 59 | } else if byte >= 32 && byte < 127 { 60 | dst.push(byte as char); 61 | } else { 62 | dst += &format!("\\x{:0>2x}", byte); 63 | } 64 | } 65 | dst 66 | } 67 | 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use super::*; 72 | 73 | #[test] 74 | fn test_debug_bytes() { 75 | let inp = b"\x00\x0a\x1f~~ Hello \\ World! ~~\x7f\xee\xff"; 76 | let out = r"\x00\x0a\x1f~~ Hello \\ World! ~~\x7f\xee\xff"; 77 | assert_eq!(&debug_bytes(&inp[..]), out); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use std::collections::{HashMap, VecDeque}; 3 | 4 | use memmem::{Searcher, TwoWaySearcher}; 5 | use byteorder::{WriteBytesExt, BigEndian}; 6 | 7 | use crate::instance::{InstState, StatusServer}; 8 | use crate::map_database::MapDatabase; 9 | use crate::spatial::{Area, Vec3}; 10 | 11 | 12 | /// Note: For mapblock version 29 onwards, all block data is compressed, so 13 | /// the `search_strs` argument is ignored. 14 | pub fn query_keys( 15 | db: &mut MapDatabase, 16 | status: &StatusServer, 17 | search_strs: &[Vec], 18 | area: Option, 19 | invert: bool, 20 | include_partial: bool 21 | ) -> Vec { 22 | status.set_state(InstState::Querying); 23 | 24 | // Prepend 16-bit search string length to reduce false positives. 25 | // This will break if the name-ID map format changes. 26 | let string16s: Vec> = search_strs.iter().map(|s| { 27 | let mut res = Vec::new(); 28 | res.write_u16::(s.len() as u16).unwrap(); 29 | res.extend(s); 30 | res 31 | }).collect(); 32 | let data_searchers: Vec = string16s.iter().map(|b| { 33 | TwoWaySearcher::new(b) 34 | }).collect(); 35 | let mut keys = Vec::new(); 36 | 37 | // Area of included block positions. 38 | // If invert == true, the function returns only blocks outside this area. 39 | let block_area = area.map(|a| { 40 | if invert == include_partial { 41 | a.to_contained_block_area() 42 | } else { 43 | Some(a.to_touching_block_area()) 44 | } 45 | }).flatten(); 46 | // True if the given area contains no blocks. 47 | let empty_area = area.is_some() && block_area.is_none(); 48 | 49 | if !empty_area || invert { 50 | for (i, (key, data)) in db.iter_rows().enumerate() { 51 | if !empty_area { 52 | if let Some(a) = &block_area { 53 | let block_pos = Vec3::from_block_key(key); 54 | if a.contains(block_pos) == invert { 55 | continue; 56 | } 57 | } 58 | } 59 | if let Some(&block_version) = data.get(0) { 60 | // If block version <= 28, data must match at least one search 61 | // string. This optimization doesn't work for new mapblocks, as 62 | // all block data is now compressed. 63 | // TODO: Remove this legacy optimization? 64 | if block_version <= 28 && !data_searchers.is_empty() 65 | && !data_searchers.iter().any(|s| s.search_in(&data).is_some()) 66 | { 67 | continue; 68 | } 69 | } 70 | keys.push(key); 71 | 72 | // Update total every 1024 iterations. 73 | if i & 1023 == 0 { 74 | status.set_total(keys.len()) 75 | } 76 | } 77 | } 78 | 79 | status.set_total(keys.len()); 80 | status.set_state(InstState::Ignore); 81 | keys 82 | } 83 | 84 | 85 | pub struct CacheMap { 86 | key_queue: VecDeque, 87 | map: HashMap, 88 | cap: usize, 89 | } 90 | 91 | impl CacheMap { 92 | pub fn with_capacity(cap: usize) -> Self { 93 | Self { 94 | key_queue: VecDeque::with_capacity(cap), 95 | map: HashMap::with_capacity(cap), 96 | cap 97 | } 98 | } 99 | 100 | pub fn insert(&mut self, key: K, value: V) { 101 | if self.key_queue.len() >= self.cap { 102 | if let Some(oldest_key) = self.key_queue.pop_front() { 103 | self.map.remove(&oldest_key); 104 | } 105 | } 106 | self.key_queue.push_back(key.clone()); 107 | self.map.insert(key, value); 108 | } 109 | 110 | #[inline] 111 | pub fn get(&self, key: &K) -> Option<&V> { 112 | self.map.get(key) 113 | } 114 | } 115 | 116 | 117 | pub fn to_bytes(s: &String) -> Vec { 118 | s.as_bytes().to_vec() 119 | } 120 | 121 | 122 | pub fn to_slice(opt: &Option>) -> &[Vec] { 123 | match opt { 124 | Some(x) => std::slice::from_ref(x), 125 | None => &[] 126 | } 127 | } 128 | 129 | 130 | #[macro_export] 131 | macro_rules! unwrap_or { 132 | ($res:expr, $alt:expr) => { 133 | match $res { 134 | Ok(val) => val, 135 | Err(_) => $alt 136 | } 137 | } 138 | } 139 | 140 | 141 | #[macro_export] 142 | macro_rules! opt_unwrap_or { 143 | ($res:expr, $alt:expr) => { 144 | match $res { 145 | Some(val) => val, 146 | None => $alt 147 | } 148 | } 149 | } 150 | 151 | 152 | pub fn fmt_duration(dur: Duration) -> String { 153 | let s = dur.as_secs(); 154 | if s < 3600 { 155 | format!("{:02}:{:02}", s / 60 % 60, s % 60) 156 | } else { 157 | format!("{}:{:02}:{:02}", s / 3600, s / 60 % 60, s % 60) 158 | } 159 | } 160 | 161 | 162 | pub fn fmt_big_num(num: u64) -> String { 163 | let f_num = num as f32; 164 | const ABBREVS: [(&str, f32); 4] = [ 165 | ("T", 1_000_000_000_000.), 166 | ("B", 1_000_000_000.), 167 | ("M", 1_000_000.), 168 | ("k", 1_000.) 169 | ]; 170 | for &(suffix, unit) in &ABBREVS { 171 | if f_num >= unit { 172 | let mantissa = f_num / unit; 173 | let place_vals = 174 | if mantissa >= 100. { 0 } 175 | else if mantissa >= 10. { 1 } 176 | else { 2 }; 177 | return format!("{:.*}{}", place_vals, mantissa, suffix) 178 | } 179 | } 180 | format!("{}", f_num.round()) 181 | } 182 | 183 | 184 | #[cfg(test)] 185 | mod tests { 186 | use super::*; 187 | 188 | #[test] 189 | fn test_nums() { 190 | let pairs = [ 191 | (0, "0"), 192 | (3, "3"), 193 | (42, "42"), 194 | (999, "999"), 195 | (1_000, "1.00k"), 196 | (33_870, "33.9k"), 197 | (470_999, "471k"), 198 | (555_678_000, "556M"), 199 | (1_672_234_000, "1.67B"), 200 | (77_864_672_234_000, "77.9T"), 201 | ]; 202 | for pair in &pairs { 203 | assert_eq!(fmt_big_num(pair.0), pair.1.to_string()); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /testing/mapblock_v25.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/random-geek/MapEditr/d75676679b468eebd31493c8a127e88418af1df8/testing/mapblock_v25.bin -------------------------------------------------------------------------------- /testing/mapblock_v28.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/random-geek/MapEditr/d75676679b468eebd31493c8a127e88418af1df8/testing/mapblock_v28.bin -------------------------------------------------------------------------------- /testing/mapblock_v29.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/random-geek/MapEditr/d75676679b468eebd31493c8a127e88418af1df8/testing/mapblock_v29.bin -------------------------------------------------------------------------------- /testing/test_mod/init.lua: -------------------------------------------------------------------------------- 1 | -- THIS MOD IS NOT USEFUL TO USERS! 2 | 3 | -- test_mod defines a few nodes and entities which may be used to generate test 4 | -- map data for MapEditr. 5 | 6 | 7 | local tex = "test_mod_test.png" 8 | local colors = { 9 | "#FF0000", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF", "#FF00FF" 10 | } 11 | 12 | 13 | minetest.register_node("test_mod:stone", { 14 | description = "test_mod stone", 15 | drawtype = "normal", 16 | tiles = {"default_stone.png^[colorize:#3FFF3F:63"}, 17 | groups = {oddly_breakable_by_hand = 3}, 18 | }) 19 | 20 | 21 | minetest.register_node("test_mod:metadata", { 22 | description = "test_mod metadata", 23 | drawtype = "normal", 24 | tiles = {"default_stone.png^[colorize:#FF3F3F:63"}, 25 | groups = {oddly_breakable_by_hand = 3}, 26 | 27 | on_construct = function(pos) 28 | local meta = minetest.get_meta(pos) 29 | meta:set_string("formspec", 30 | "size[8,5]" .. 31 | "list[current_name;main;0,0;1,1;]" .. 32 | "list[current_player;main;0,1;8,4;]") 33 | meta:set_string("infotext", "Test Chest") 34 | local inv = meta:get_inventory() 35 | inv:set_size("main", 1) 36 | end, 37 | }) 38 | 39 | 40 | minetest.register_node("test_mod:timer", { 41 | description = "test_mod timer", 42 | drawtype = "nodebox", 43 | node_box = { 44 | type = "fixed", 45 | fixed = {-1/4, -1/2, -1/4, 1/4, 1/4, 1/4} 46 | }, 47 | tiles = {tex}, 48 | paramtype = "light", 49 | paramtype2 = "facedir", 50 | groups = {oddly_breakable_by_hand = 3}, 51 | 52 | on_construct = function(pos) 53 | minetest.get_node_timer(pos):start(1.337) 54 | end, 55 | 56 | on_timer = function(pos, elapsed) 57 | local node = minetest.get_node(pos) 58 | node.param2 = (node.param2 + 4) % 24 59 | 60 | minetest.set_node(pos, node) 61 | minetest.get_node_timer(pos):start(1.337) 62 | end, 63 | }) 64 | 65 | 66 | minetest.register_entity("test_mod:color_entity", { 67 | initial_properties = { 68 | visual = "cube", 69 | textures = {tex, tex, tex, tex, tex, tex}, 70 | }, 71 | 72 | on_activate = function(self, staticdata, dtime_s) 73 | if staticdata and staticdata ~= "" then 74 | t = minetest.deserialize(staticdata) 75 | self._color_num = t.color_num 76 | else 77 | self._color_num = math.random(1, #colors) 78 | end 79 | 80 | self.object:settexturemod( 81 | "^[colorize:" .. colors[self._color_num] .. ":127") 82 | end, 83 | 84 | get_staticdata = function(self) 85 | return minetest.serialize({color_num = self._color_num}) 86 | end, 87 | }) 88 | 89 | 90 | minetest.register_entity("test_mod:nametag_entity", { 91 | initial_properties = { 92 | visual = "sprite", 93 | textures = {tex}, 94 | }, 95 | 96 | on_activate = function(self, staticdata, dtime_s) 97 | if staticdata and staticdata ~= "" then 98 | self._text = staticdata 99 | else 100 | self._text = tostring(math.random(0, 999999)) 101 | end 102 | 103 | self.object:set_nametag_attributes({ 104 | text = self._text, 105 | color = "#FFFF00" 106 | }) 107 | end, 108 | 109 | get_staticdata = function(self) 110 | return self._text 111 | end, 112 | }) 113 | -------------------------------------------------------------------------------- /testing/test_mod/textures/test_mod_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/random-geek/MapEditr/d75676679b468eebd31493c8a127e88418af1df8/testing/test_mod/textures/test_mod_test.png --------------------------------------------------------------------------------