├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── demo.png ├── fastanvil ├── Cargo.toml ├── benches │ ├── chunk_parse.rs │ └── complete_chunk.rs ├── examples │ ├── anvil-palette-swap.rs │ ├── chunk-dump-ex.rs │ ├── complete-chunk.rs │ └── dump-mca.rs ├── resources │ ├── 1.12.chunk │ ├── 1.17.0.chunk │ ├── 1.17.1-custom-heights.chunk │ ├── 1.17.1.chunk │ ├── 1.19.4.mca │ ├── 21w44a-test1.nbt │ ├── assets │ │ └── blockstates │ │ │ └── acacia_stairs.json │ ├── chunk.nbt │ ├── etho-empty.chunk │ ├── etho-end-r.-6.-1.c.7.25.nbt │ ├── etho-max-heights.chunk │ ├── etho-old-heightmaps.chunk │ ├── etho-old-in-new.chunk │ ├── etho-old-in-new2.chunk │ ├── etho.chunk │ ├── forge-1.20.1.nbt │ ├── issue99-chunk.nbt │ └── unicode.chunk └── src │ ├── biome.rs │ ├── bits.rs │ ├── complete │ ├── chunk.rs │ ├── mod.rs │ ├── section.rs │ └── section_tower.rs │ ├── dimension.rs │ ├── files.rs │ ├── java │ ├── block.rs │ ├── chunk.rs │ ├── heightmaps.rs │ ├── mod.rs │ ├── pre13 │ │ ├── mod.rs │ │ └── pre13_block_names.rs │ ├── pre18.rs │ ├── section.rs │ ├── section_data.rs │ └── section_tower.rs │ ├── lib.rs │ ├── region.rs │ ├── render.rs │ ├── rendered_palette.rs │ ├── test │ ├── complete_chunk.rs │ ├── mod.rs │ ├── region.rs │ ├── rogue_chunks.rs │ ├── section_data.rs │ ├── standard_chunks.rs │ └── unicode_chunk.rs │ └── tex │ ├── mod.rs │ └── test.rs ├── fastnbt ├── Cargo.toml ├── README.md ├── examples │ ├── avoid-allocation.rs │ ├── change-world-spawn-value.rs │ ├── change-world-spawn.rs │ ├── fastnbt-value.rs │ ├── nbt-dump.rs │ └── simple-deserialize.rs └── src │ ├── arrays.rs │ ├── borrow.rs │ ├── de.rs │ ├── error.rs │ ├── input.rs │ ├── lib.rs │ ├── macros.rs │ ├── ser │ ├── array_serializer.rs │ ├── mod.rs │ ├── name_serializer.rs │ ├── serializer.rs │ └── write_nbt.rs │ ├── stream.rs │ ├── test │ ├── builder.rs │ ├── de.rs │ ├── fuzz.rs │ ├── macros.rs │ ├── minecraft_chunk.rs │ ├── mod.rs │ ├── resources │ │ ├── chunk.nbt │ │ ├── chunk1.14.nbt │ │ └── mod.rs │ ├── ser.rs │ ├── stream.rs │ └── value │ │ ├── de.rs │ │ ├── mod.rs │ │ └── ser.rs │ └── value │ ├── array_serializer.rs │ ├── de.rs │ ├── mod.rs │ └── ser.rs ├── fastsnbt ├── Cargo.toml ├── README.md └── src │ ├── de.rs │ ├── error.rs │ ├── lib.rs │ ├── parser.rs │ ├── ser │ ├── array_serializer.rs │ ├── mod.rs │ └── name_serializer.rs │ └── tests │ ├── de_tests.rs │ ├── mod.rs │ └── ser_tests.rs ├── fuzz ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── fuzz_targets │ ├── deserialize_chunk.rs │ ├── deserialize_value.rs │ ├── read_region.rs │ └── serialize_value.rs ├── palette.tar.gz └── tools ├── Cargo.toml └── src ├── bin ├── anvil-palette-swap.rs ├── anvil-palette.rs ├── anvil.rs ├── flatten-model.rs ├── nbt-dump-gz.rs ├── nbt-dump-raw.rs └── region-dump.rs └── lib.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Build 19 | run: cargo build -p fastnbt -p fastanvil -p fastnbt-tools 20 | - name: Run tests 21 | run: cargo test -p fastnbt -p fastanvil -p fastnbt-tools 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | /tile-server/public/tiles 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | resolver = "2" 4 | members = ["fastnbt", "fastsnbt", "fastanvil", "tools"] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Owen Gage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastnbt project 2 | 3 | ![fastnbt-shield] 4 | [![fastnbt-version-shield]][fastnbt-crates] 5 | [![fastnbt-docs-shield]][fastnbt-docs] 6 | [![build-status-shield]][github-actions] 7 | 8 | ![fastsnbt-shield] 9 | [![fastsnbt-version-shield]][fastsnbt-crates] 10 | [![fastsnbt-docs-shield]][fastsnbt-docs] 11 | 12 | ![fastanvil-shield] 13 | [![fastanvil-version-shield]][fastanvil-crates] 14 | [![fastanvil-docs-shield]][fastanvil-docs] 15 | 16 | 17 | 18 | [fastnbt-shield]: https://img.shields.io/badge/%20-FastNBT-blue 19 | [fastnbt-version-shield]: https://img.shields.io/crates/v/fastnbt.svg 20 | [fastnbt-docs]: https://docs.rs/fastnbt/latest/fastnbt/index.html 21 | [fastnbt-docs-shield]: https://img.shields.io/docsrs/fastnbt 22 | [fastnbt-crates]: https://crates.io/crates/fastnbt 23 | 24 | [fastsnbt-shield]: https://img.shields.io/badge/%20-FastSNBT-blue 25 | [fastsnbt-version-shield]: https://img.shields.io/crates/v/fastsnbt.svg 26 | [fastsnbt-docs]: https://docs.rs/fastsnbt/latest/fastsnbt/index.html 27 | [fastsnbt-docs-shield]: https://img.shields.io/docsrs/fastsnbt 28 | [fastsnbt-crates]: https://crates.io/crates/fastsnbt 29 | 30 | [build-status-shield]: https://img.shields.io/github/actions/workflow/status/owengage/fastnbt/rust.yml?branch=master 31 | [github-actions]: https://github.com/owengage/fastnbt/actions?query=branch%3Amaster 32 | [fastanvil-shield]: https://img.shields.io/badge/%20-FastAnvil-blue 33 | [fastanvil-version-shield]: https://img.shields.io/crates/v/fastanvil.svg 34 | [fastanvil-crates]: https://crates.io/crates/fastanvil 35 | [fastanvil-docs-shield]: https://img.shields.io/docsrs/fastanvil 36 | [fastanvil-docs]: https://docs.rs/fastanvil/latest/fastanvil/index.html 37 | 38 | FastNBT is a [serde](https://serde.rs/) serializer and deserializer for 39 | _Minecraft: Java Edition's_ NBT format, including 40 | [`Value`](https://docs.rs/fastnbt/latest/fastnbt/enum.Value.html) type and 41 | [`nbt!`](https://docs.rs/fastnbt/latest/fastnbt/macro.nbt.html) macro. For 42 | stringified NBT (sNBT) see FastSNBT. 43 | 44 | FastAnvil allows rendering maps of worlds, and a 45 | [`Region`](https://docs.rs/fastanvil/latest/fastanvil/struct.Region.html) for 46 | using the Region file format. Supports 1.20 down to 1.13 inclusive, and slightly 47 | flaky support for 1.12. 48 | 49 | An in-browser Rust-to-WASM powered Minecraft map renderer demo is below. 50 | 51 | # Demos 52 | 53 | Demo of Hermitcraft season 8 and more at [owengage.com/anvil](https://owengage.com/anvil/?world=hermitcraft8) 54 | 55 | ![alt rendered map](demo.png) 56 | 57 | The `anvil` binary from `fastnbt-tools` can render your world leveraging all of 58 | your CPU. 59 | 60 | # Examples 61 | 62 | A bunch of examples can be found in 63 | [`fastnbt/examples`](https://github.com/owengage/fastnbt/tree/master/fastnbt/examples), 64 | [`fastanvil/examples`](https://github.com/owengage/fastnbt/tree/master/fastanvil/examples) and [`tools/src`](https://github.com/owengage/fastnbt/tree/master/tools/src/bin). Some examples are recreated below. 65 | 66 | # Example: editing level.dat 67 | 68 | The following edits the world spawn to 250, 200, 250 (probably not a good 69 | idea!). Full example in fastnbt/examples directory. 70 | 71 | ```rust 72 | #[derive(Serialize, Deserialize)] 73 | struct LevelDat { 74 | #[serde(rename = "Data")] 75 | data: Data, 76 | } 77 | 78 | #[derive(Serialize, Deserialize)] 79 | #[serde(rename_all = "PascalCase")] 80 | struct Data { 81 | spawn_x: i32, 82 | spawn_y: i32, 83 | spawn_z: i32, 84 | 85 | #[serde(flatten)] 86 | other: HashMap, 87 | } 88 | 89 | fn main() { 90 | let args: Vec<_> = std::env::args_os().collect(); 91 | let file = std::fs::File::open(&args[1]).unwrap(); 92 | let mut decoder = GzDecoder::new(file); 93 | let mut bytes = vec![]; 94 | decoder.read_to_end(&mut bytes).unwrap(); 95 | 96 | let mut leveldat: LevelDat = fastnbt::from_bytes(&bytes).unwrap(); 97 | 98 | leveldat.data.spawn_x = 250; 99 | leveldat.data.spawn_y = 200; 100 | leveldat.data.spawn_z = 250; 101 | 102 | let new_bytes = fastnbt::to_bytes(&leveldat).unwrap(); 103 | let outfile = std::fs::File::create("level.dat").unwrap(); 104 | let mut encoder = GzEncoder::new(outfile, Compression::fast()); 105 | encoder.write_all(&new_bytes).unwrap(); 106 | } 107 | ``` 108 | 109 | # Example: print player inventory 110 | 111 | This example demonstrates printing out a players inventory and ender chest contents from the [player dat 112 | files](https://minecraft.wiki/w/Player.dat_format) found in worlds. We 113 | 114 | - use serde's renaming attribute to have rustfmt conformant field names, 115 | - use lifetimes to save on string allocations, and 116 | - use the `Value` type to deserialize a field we don't specify the exact 117 | structure of. 118 | 119 | ```rust 120 | #[derive(Deserialize, Debug)] 121 | #[serde(rename_all = "PascalCase")] 122 | struct PlayerDat<'a> { 123 | data_version: i32, 124 | 125 | #[serde(borrow)] 126 | inventory: Vec>, 127 | ender_items: Vec>, 128 | } 129 | 130 | #[derive(Deserialize, Debug)] 131 | struct InventorySlot<'a> { 132 | id: &'a str, // We avoid allocating a string here. 133 | tag: Option, // Also get the less structured properties of the object. 134 | 135 | // We need to rename fields a lot. 136 | #[serde(rename = "Count")] 137 | count: i8, 138 | } 139 | 140 | fn main() { 141 | let args: Vec<_> = std::env::args().skip(1).collect(); 142 | let file = std::fs::File::open(args[0].clone()).unwrap(); 143 | 144 | // Player dat files are compressed with GZip. 145 | let mut decoder = GzDecoder::new(file); 146 | let mut data = vec![]; 147 | decoder.read_to_end(&mut data).unwrap(); 148 | 149 | let player: Result = from_bytes(data.as_slice()); 150 | 151 | println!("{:#?}", player); 152 | } 153 | ``` 154 | 155 | ## Usage 156 | 157 | For the libraries 158 | 159 | ```toml 160 | [dependencies] 161 | fastnbt = "2" 162 | fastanvil = "0.31" 163 | ``` 164 | 165 | For the `anvil` executable 166 | 167 | ```bash 168 | cargo install fastnbt-tools 169 | ``` 170 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/demo.png -------------------------------------------------------------------------------- /fastanvil/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fastanvil" 3 | description = "Minecraft Anvil library" 4 | repository = "https://github.com/owengage/fastnbt" 5 | version = "0.31.0" 6 | authors = ["Owen Gage "] 7 | edition = "2021" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [dependencies] 11 | fastnbt = { path = "../fastnbt", version = "2" } 12 | flate2 = "1.0" 13 | lz4-java-wrc = "0.2.0" 14 | num_enum = "0.5" 15 | byteorder = "1.3" 16 | bit_field = "0.10" 17 | serde = { version = "1.0", features = ["derive"] } 18 | log = "0.4" 19 | once_cell = "1.9" 20 | 21 | # Deps for render feature. 22 | image = { version = "0.23", default-features = false, optional = true } 23 | tar = { version = "0.4", default-features = false, optional = true } 24 | serde_json = { version = "1.0", optional = true } 25 | 26 | [dev-dependencies] 27 | serde_json = "1.0" 28 | criterion = "0.4" 29 | 30 | [features] 31 | default = ["render"] 32 | render = ["dep:image", "dep:tar", "dep:serde_json"] 33 | 34 | [[bench]] 35 | name = "chunk_parse" 36 | harness = false 37 | 38 | [[bench]] 39 | name = "complete_chunk" 40 | harness = false 41 | -------------------------------------------------------------------------------- /fastanvil/benches/chunk_parse.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use fastanvil::JavaChunk; 3 | 4 | const CHUNK_RAW: &[u8] = include_bytes!("../resources/chunk.nbt"); 5 | 6 | pub fn fastnbt_benchmark(c: &mut Criterion) { 7 | c.bench_function("chunk", |b| { 8 | b.iter(|| { 9 | let chunk = JavaChunk::from_bytes(CHUNK_RAW).unwrap(); 10 | black_box(chunk); 11 | }); 12 | }); 13 | } 14 | 15 | criterion_group!(benches, fastnbt_benchmark); 16 | criterion_main!(benches); 17 | -------------------------------------------------------------------------------- /fastanvil/benches/complete_chunk.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | 3 | use fastanvil::{complete, Region}; 4 | 5 | pub fn create_complete_chunk_by_current(c: &mut Criterion) { 6 | c.bench_function("chunk", |b| { 7 | let file = std::fs::File::open("./resources/1.19.4.mca").unwrap(); 8 | 9 | let mut region = Region::from_stream(file).unwrap(); 10 | let data = ®ion.read_chunk(0, 0).unwrap().unwrap(); 11 | 12 | b.iter(|| { 13 | let chunk = complete::Chunk::from_bytes(data); 14 | black_box(chunk).unwrap(); 15 | }); 16 | }); 17 | } 18 | 19 | criterion_group!(benches, create_complete_chunk_by_current); 20 | criterion_main!(benches); 21 | -------------------------------------------------------------------------------- /fastanvil/examples/anvil-palette-swap.rs: -------------------------------------------------------------------------------- 1 | //! Take a region and produce a copy where a given palette item has been 2 | //! changed to something else. 3 | //! 4 | //! This can for example change all oak_leaves into diamond_blocks. 5 | //! 6 | use std::{collections::HashMap, env, fs::File}; 7 | 8 | use fastanvil::Region; 9 | use fastnbt::Value; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | struct Chunk { 14 | sections: Vec
, 15 | 16 | #[serde(flatten)] 17 | other: HashMap, 18 | } 19 | 20 | #[derive(Serialize, Deserialize)] 21 | struct Section { 22 | block_states: Blockstates, 23 | #[serde(flatten)] 24 | other: HashMap, 25 | } 26 | 27 | #[derive(Serialize, Deserialize)] 28 | struct Blockstates { 29 | palette: Vec, 30 | #[serde(flatten)] 31 | other: HashMap, 32 | } 33 | 34 | #[derive(Serialize, Deserialize)] 35 | struct PaletteItem { 36 | #[serde(rename = "Name")] 37 | name: String, 38 | #[serde(rename = "Properties")] 39 | properties: Option, 40 | } 41 | 42 | fn main() { 43 | let region = env::var_os("REGION").unwrap(); 44 | let out_path = env::var_os("OUT_FILE").unwrap(); 45 | let from = env::var("FROM").unwrap(); 46 | let to = env::var("TO").unwrap(); 47 | 48 | let region = File::open(region).unwrap(); 49 | let mut region = Region::from_stream(region).unwrap(); 50 | 51 | // File needs to readable as well. 52 | let out_file = File::options() 53 | .read(true) 54 | .write(true) 55 | .create_new(true) 56 | .open(out_path) 57 | .unwrap(); 58 | 59 | let mut new_region = Region::create(out_file).unwrap(); 60 | 61 | for z in 0..32 { 62 | for x in 0..32 { 63 | match region.read_chunk(x, z) { 64 | Ok(Some(data)) => { 65 | let mut chunk: Chunk = fastnbt::from_bytes(&data).unwrap(); 66 | for section in chunk.sections.iter_mut() { 67 | let palette: &mut Vec = &mut section.block_states.palette; 68 | for item in palette { 69 | if item.name == from { 70 | item.name = to.to_owned(); 71 | } 72 | } 73 | } 74 | let ser = fastnbt::to_bytes(&chunk).unwrap(); 75 | new_region.write_chunk(x, z, &ser).unwrap(); 76 | } 77 | Ok(None) => {} 78 | Err(e) => eprintln!("{e}"), 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /fastanvil/examples/chunk-dump-ex.rs: -------------------------------------------------------------------------------- 1 | use fastanvil::{CurrentJavaChunk, Region}; 2 | use fastnbt::from_bytes; 3 | 4 | // 5 | // This loads a region file, extracts a chunk from it, and uses serde to 6 | // deserialize it into a `anvil::Chunk` object and print it. 7 | // 8 | 9 | fn main() { 10 | let args: Vec<_> = std::env::args().skip(1).collect(); 11 | let file = std::fs::File::open(args[0].clone()).unwrap(); 12 | 13 | let mut region = Region::from_stream(file).unwrap(); 14 | let data = region.read_chunk(0, 0).unwrap().unwrap(); 15 | 16 | let chunk: CurrentJavaChunk = from_bytes(data.as_slice()).unwrap(); 17 | 18 | println!("{:?}", chunk); 19 | } 20 | -------------------------------------------------------------------------------- /fastanvil/examples/complete-chunk.rs: -------------------------------------------------------------------------------- 1 | use fastanvil::{complete, Chunk, Region}; 2 | 3 | fn main() { 4 | let file = std::fs::File::open("./fastanvil/resources/1.19.4.mca").unwrap(); 5 | 6 | let mut region = Region::from_stream(file).unwrap(); 7 | 8 | let data = region.read_chunk(0, 0).unwrap().unwrap(); 9 | 10 | let complete_chunk = complete::Chunk::from_bytes(&data).unwrap(); 11 | 12 | println!("{}", complete_chunk.status); 13 | 14 | println!("{:?}", complete_chunk.y_range()); 15 | } 16 | -------------------------------------------------------------------------------- /fastanvil/examples/dump-mca.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use fastanvil::Region; 4 | use fastnbt::error::Result; 5 | use fastnbt::Value; 6 | 7 | // 8 | // This loads an MCA file: 9 | // https://minecraft.wiki/w/Anvil_file_format#Further_information 10 | // 11 | // This can be a region, a POI, or an entities MCA file, and dumps the value of it. 12 | // 13 | 14 | fn main() { 15 | let path = std::env::args().nth(1).unwrap(); 16 | let file = File::open(path).unwrap(); 17 | 18 | let mut mca = Region::from_stream(file).unwrap(); 19 | 20 | for chunk in mca.iter().flatten() { 21 | let chunk: Result = fastnbt::from_bytes(&chunk.data); 22 | println!("{:#?}", chunk); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fastanvil/resources/1.12.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/1.12.chunk -------------------------------------------------------------------------------- /fastanvil/resources/1.17.0.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/1.17.0.chunk -------------------------------------------------------------------------------- /fastanvil/resources/1.17.1-custom-heights.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/1.17.1-custom-heights.chunk -------------------------------------------------------------------------------- /fastanvil/resources/1.17.1.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/1.17.1.chunk -------------------------------------------------------------------------------- /fastanvil/resources/1.19.4.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/1.19.4.mca -------------------------------------------------------------------------------- /fastanvil/resources/21w44a-test1.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/21w44a-test1.nbt -------------------------------------------------------------------------------- /fastanvil/resources/assets/blockstates/acacia_stairs.json: -------------------------------------------------------------------------------- 1 | { 2 | "variants": { 3 | "facing=east,half=bottom,shape=inner_left": { 4 | "model": "minecraft:block/acacia_stairs_inner", 5 | "y": 270, 6 | "uvlock": true 7 | }, 8 | "facing=east,half=bottom,shape=inner_right": { 9 | "model": "minecraft:block/acacia_stairs_inner" 10 | }, 11 | "facing=east,half=bottom,shape=outer_left": { 12 | "model": "minecraft:block/acacia_stairs_outer", 13 | "y": 270, 14 | "uvlock": true 15 | }, 16 | "facing=east,half=bottom,shape=outer_right": { 17 | "model": "minecraft:block/acacia_stairs_outer" 18 | }, 19 | "facing=east,half=bottom,shape=straight": { 20 | "model": "minecraft:block/acacia_stairs" 21 | }, 22 | "facing=east,half=top,shape=inner_left": { 23 | "model": "minecraft:block/acacia_stairs_inner", 24 | "x": 180, 25 | "uvlock": true 26 | }, 27 | "facing=east,half=top,shape=inner_right": { 28 | "model": "minecraft:block/acacia_stairs_inner", 29 | "x": 180, 30 | "y": 90, 31 | "uvlock": true 32 | }, 33 | "facing=east,half=top,shape=outer_left": { 34 | "model": "minecraft:block/acacia_stairs_outer", 35 | "x": 180, 36 | "uvlock": true 37 | }, 38 | "facing=east,half=top,shape=outer_right": { 39 | "model": "minecraft:block/acacia_stairs_outer", 40 | "x": 180, 41 | "y": 90, 42 | "uvlock": true 43 | }, 44 | "facing=east,half=top,shape=straight": { 45 | "model": "minecraft:block/acacia_stairs", 46 | "x": 180, 47 | "uvlock": true 48 | }, 49 | "facing=north,half=bottom,shape=inner_left": { 50 | "model": "minecraft:block/acacia_stairs_inner", 51 | "y": 180, 52 | "uvlock": true 53 | }, 54 | "facing=north,half=bottom,shape=inner_right": { 55 | "model": "minecraft:block/acacia_stairs_inner", 56 | "y": 270, 57 | "uvlock": true 58 | }, 59 | "facing=north,half=bottom,shape=outer_left": { 60 | "model": "minecraft:block/acacia_stairs_outer", 61 | "y": 180, 62 | "uvlock": true 63 | }, 64 | "facing=north,half=bottom,shape=outer_right": { 65 | "model": "minecraft:block/acacia_stairs_outer", 66 | "y": 270, 67 | "uvlock": true 68 | }, 69 | "facing=north,half=bottom,shape=straight": { 70 | "model": "minecraft:block/acacia_stairs", 71 | "y": 270, 72 | "uvlock": true 73 | }, 74 | "facing=north,half=top,shape=inner_left": { 75 | "model": "minecraft:block/acacia_stairs_inner", 76 | "x": 180, 77 | "y": 270, 78 | "uvlock": true 79 | }, 80 | "facing=north,half=top,shape=inner_right": { 81 | "model": "minecraft:block/acacia_stairs_inner", 82 | "x": 180, 83 | "uvlock": true 84 | }, 85 | "facing=north,half=top,shape=outer_left": { 86 | "model": "minecraft:block/acacia_stairs_outer", 87 | "x": 180, 88 | "y": 270, 89 | "uvlock": true 90 | }, 91 | "facing=north,half=top,shape=outer_right": { 92 | "model": "minecraft:block/acacia_stairs_outer", 93 | "x": 180, 94 | "uvlock": true 95 | }, 96 | "facing=north,half=top,shape=straight": { 97 | "model": "minecraft:block/acacia_stairs", 98 | "x": 180, 99 | "y": 270, 100 | "uvlock": true 101 | }, 102 | "facing=south,half=bottom,shape=inner_left": { 103 | "model": "minecraft:block/acacia_stairs_inner" 104 | }, 105 | "facing=south,half=bottom,shape=inner_right": { 106 | "model": "minecraft:block/acacia_stairs_inner", 107 | "y": 90, 108 | "uvlock": true 109 | }, 110 | "facing=south,half=bottom,shape=outer_left": { 111 | "model": "minecraft:block/acacia_stairs_outer" 112 | }, 113 | "facing=south,half=bottom,shape=outer_right": { 114 | "model": "minecraft:block/acacia_stairs_outer", 115 | "y": 90, 116 | "uvlock": true 117 | }, 118 | "facing=south,half=bottom,shape=straight": { 119 | "model": "minecraft:block/acacia_stairs", 120 | "y": 90, 121 | "uvlock": true 122 | }, 123 | "facing=south,half=top,shape=inner_left": { 124 | "model": "minecraft:block/acacia_stairs_inner", 125 | "x": 180, 126 | "y": 90, 127 | "uvlock": true 128 | }, 129 | "facing=south,half=top,shape=inner_right": { 130 | "model": "minecraft:block/acacia_stairs_inner", 131 | "x": 180, 132 | "y": 180, 133 | "uvlock": true 134 | }, 135 | "facing=south,half=top,shape=outer_left": { 136 | "model": "minecraft:block/acacia_stairs_outer", 137 | "x": 180, 138 | "y": 90, 139 | "uvlock": true 140 | }, 141 | "facing=south,half=top,shape=outer_right": { 142 | "model": "minecraft:block/acacia_stairs_outer", 143 | "x": 180, 144 | "y": 180, 145 | "uvlock": true 146 | }, 147 | "facing=south,half=top,shape=straight": { 148 | "model": "minecraft:block/acacia_stairs", 149 | "x": 180, 150 | "y": 90, 151 | "uvlock": true 152 | }, 153 | "facing=west,half=bottom,shape=inner_left": { 154 | "model": "minecraft:block/acacia_stairs_inner", 155 | "y": 90, 156 | "uvlock": true 157 | }, 158 | "facing=west,half=bottom,shape=inner_right": { 159 | "model": "minecraft:block/acacia_stairs_inner", 160 | "y": 180, 161 | "uvlock": true 162 | }, 163 | "facing=west,half=bottom,shape=outer_left": { 164 | "model": "minecraft:block/acacia_stairs_outer", 165 | "y": 90, 166 | "uvlock": true 167 | }, 168 | "facing=west,half=bottom,shape=outer_right": { 169 | "model": "minecraft:block/acacia_stairs_outer", 170 | "y": 180, 171 | "uvlock": true 172 | }, 173 | "facing=west,half=bottom,shape=straight": { 174 | "model": "minecraft:block/acacia_stairs", 175 | "y": 180, 176 | "uvlock": true 177 | }, 178 | "facing=west,half=top,shape=inner_left": { 179 | "model": "minecraft:block/acacia_stairs_inner", 180 | "x": 180, 181 | "y": 180, 182 | "uvlock": true 183 | }, 184 | "facing=west,half=top,shape=inner_right": { 185 | "model": "minecraft:block/acacia_stairs_inner", 186 | "x": 180, 187 | "y": 270, 188 | "uvlock": true 189 | }, 190 | "facing=west,half=top,shape=outer_left": { 191 | "model": "minecraft:block/acacia_stairs_outer", 192 | "x": 180, 193 | "y": 180, 194 | "uvlock": true 195 | }, 196 | "facing=west,half=top,shape=outer_right": { 197 | "model": "minecraft:block/acacia_stairs_outer", 198 | "x": 180, 199 | "y": 270, 200 | "uvlock": true 201 | }, 202 | "facing=west,half=top,shape=straight": { 203 | "model": "minecraft:block/acacia_stairs", 204 | "x": 180, 205 | "y": 180, 206 | "uvlock": true 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /fastanvil/resources/chunk.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/chunk.nbt -------------------------------------------------------------------------------- /fastanvil/resources/etho-empty.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/etho-empty.chunk -------------------------------------------------------------------------------- /fastanvil/resources/etho-end-r.-6.-1.c.7.25.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/etho-end-r.-6.-1.c.7.25.nbt -------------------------------------------------------------------------------- /fastanvil/resources/etho-max-heights.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/etho-max-heights.chunk -------------------------------------------------------------------------------- /fastanvil/resources/etho-old-heightmaps.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/etho-old-heightmaps.chunk -------------------------------------------------------------------------------- /fastanvil/resources/etho-old-in-new.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/etho-old-in-new.chunk -------------------------------------------------------------------------------- /fastanvil/resources/etho-old-in-new2.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/etho-old-in-new2.chunk -------------------------------------------------------------------------------- /fastanvil/resources/etho.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/etho.chunk -------------------------------------------------------------------------------- /fastanvil/resources/forge-1.20.1.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/forge-1.20.1.nbt -------------------------------------------------------------------------------- /fastanvil/resources/issue99-chunk.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/issue99-chunk.nbt -------------------------------------------------------------------------------- /fastanvil/resources/unicode.chunk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastanvil/resources/unicode.chunk -------------------------------------------------------------------------------- /fastanvil/src/complete/chunk.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use crate::{ 4 | dimension, pre13, pre18, Block, Chunk as DimensionChunk, CurrentJavaChunk, HeightMode, 5 | JavaChunk, 6 | }; 7 | // Chunk as DimensionChunk need because complete::Chunk and dimension::Chunk are both called Chunk maybe rename one 8 | use crate::biome::Biome; 9 | use crate::complete::section_tower::SectionTower; 10 | 11 | pub struct Chunk { 12 | pub status: String, 13 | 14 | pub sections: SectionTower, 15 | 16 | //todo more | different Heightmaps 17 | pub heightmap: [i16; 256], 18 | } 19 | 20 | impl Chunk { 21 | pub fn from_bytes(data: &[u8]) -> fastnbt::error::Result { 22 | match JavaChunk::from_bytes(data)? { 23 | JavaChunk::Post18(chunk) => Ok(chunk.into()), 24 | JavaChunk::Pre18(chunk) => Ok(chunk.into()), 25 | JavaChunk::Pre13(chunk) => Ok(chunk.into()), 26 | } 27 | } 28 | 29 | pub fn iter_blocks(&self) -> impl Iterator { 30 | self.sections.iter_blocks() 31 | } 32 | 33 | pub fn is_valid_block_cord(&self, x: usize, y: isize, z: usize) -> bool { 34 | self.y_range().contains(&y) && (0..16).contains(&x) && (0..16).contains(&z) 35 | } 36 | } 37 | 38 | impl dimension::Chunk for Chunk { 39 | fn status(&self) -> String { 40 | self.status.clone() 41 | } 42 | 43 | fn surface_height(&self, x: usize, z: usize, mode: HeightMode) -> isize { 44 | match mode { 45 | HeightMode::Trust => self.heightmap[z * 16 + x] as isize, 46 | HeightMode::Calculate => { 47 | todo!() 48 | } 49 | } 50 | } 51 | 52 | fn biome(&self, x: usize, y: isize, z: usize) -> Option { 53 | if !self.is_valid_block_cord(x, y, z) { 54 | return None; 55 | } 56 | 57 | self.sections.biome(x, y, z) 58 | } 59 | 60 | fn block(&self, x: usize, y: isize, z: usize) -> Option<&Block> { 61 | if !self.is_valid_block_cord(x, y, z) { 62 | return None; 63 | } 64 | 65 | self.sections.block(x, y, z) 66 | } 67 | 68 | fn y_range(&self) -> Range { 69 | self.sections.y_range() 70 | } 71 | } 72 | 73 | impl From for Chunk { 74 | fn from(current_java_chunk: CurrentJavaChunk) -> Self { 75 | //probably find better way. maybe always recalculate if need and then cache 76 | current_java_chunk.recalculate_heightmap(HeightMode::Trust); 77 | let heightmap = current_java_chunk.lazy_heightmap.read().unwrap().unwrap(); 78 | 79 | Chunk { 80 | status: current_java_chunk.status.clone(), 81 | sections: current_java_chunk.sections.unwrap().into(), 82 | heightmap, 83 | } 84 | } 85 | } 86 | 87 | impl From for Chunk { 88 | fn from(java_chunk: pre18::JavaChunk) -> Self { 89 | //probably find better way. maybe always recalculate if need and then cache 90 | java_chunk.recalculate_heightmap(HeightMode::Trust); 91 | let heightmap = java_chunk.level.lazy_heightmap.read().unwrap().unwrap(); 92 | 93 | let biomes = create_biome_vec(&java_chunk); 94 | 95 | Chunk { 96 | status: java_chunk.status(), 97 | sections: (java_chunk.level.sections.unwrap(), biomes).into(), 98 | heightmap, 99 | } 100 | } 101 | } 102 | 103 | impl From for Chunk { 104 | fn from(java_chunk: pre13::JavaChunk) -> Self { 105 | //probably find better way. maybe always recalculate if need and then cache 106 | java_chunk.recalculate_heightmap(HeightMode::Trust); 107 | 108 | let heightmap = java_chunk.level.lazy_heightmap.read().unwrap().unwrap(); 109 | 110 | let biomes = create_biome_vec(&java_chunk); 111 | let blocks = create_block_vec(&java_chunk); 112 | 113 | Chunk { 114 | status: java_chunk.status(), 115 | sections: (java_chunk.level.sections.unwrap(), blocks, biomes).into(), 116 | heightmap, 117 | } 118 | } 119 | } 120 | 121 | fn create_biome_vec(java_chunk: &dyn dimension::Chunk) -> Vec { 122 | let mut biomes = vec![]; 123 | 124 | for y in java_chunk.y_range().step_by(4) { 125 | for z in (0..16).step_by(4) { 126 | for x in (0..16).step_by(4) { 127 | biomes.push(java_chunk.biome(x, y, z).unwrap()) 128 | } 129 | } 130 | } 131 | 132 | biomes 133 | } 134 | 135 | fn create_block_vec(java_chunk: &dyn dimension::Chunk) -> Vec { 136 | let mut blocks = vec![]; 137 | 138 | for y in java_chunk.y_range() { 139 | for z in 0..16 { 140 | for x in 0..16 { 141 | blocks.push(java_chunk.block(x, y, z).unwrap().clone()) 142 | } 143 | } 144 | } 145 | 146 | blocks 147 | } 148 | -------------------------------------------------------------------------------- /fastanvil/src/complete/mod.rs: -------------------------------------------------------------------------------- 1 | mod chunk; 2 | mod section; 3 | mod section_tower; 4 | 5 | pub use chunk::Chunk; 6 | -------------------------------------------------------------------------------- /fastanvil/src/complete/section.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | use std::slice::Iter; 3 | 4 | use crate::biome::Biome; 5 | use crate::pre18::Pre18Section; 6 | use crate::{java, Block, AIR}; 7 | 8 | //could remove duplication though another layer similar to BlockData<> | BiomData<> 9 | pub struct Section { 10 | block_palette: Vec, 11 | 12 | //This will increase in x, then z, then y. 13 | //u16 should be enough for all blocks 14 | blocks: Option>, 15 | 16 | biome_palette: Vec, 17 | 18 | //This will increase in x, then z, then y. 19 | //u8 should be enough for all bioms 20 | biomes: Option>, 21 | } 22 | 23 | impl Section { 24 | pub fn block(&self, x: usize, y: usize, z: usize) -> Option<&Block> { 25 | return match &self.blocks { 26 | None => Some(self.block_palette.get(0).unwrap()), 27 | Some(blocks) => { 28 | let index = y * (16 * 16) + z * 16 + x; 29 | 30 | self.block_palette.get(*blocks.get(index).unwrap() as usize) 31 | } 32 | }; 33 | } 34 | 35 | pub fn biome(&self, x: usize, y: usize, z: usize) -> Option { 36 | let x = x / 4; 37 | let y = y / 4; 38 | let z = z / 4; 39 | 40 | return match &self.biomes { 41 | None => Some(*self.biome_palette.get(0).unwrap()), 42 | Some(biomes) => { 43 | let index = y * (4 * 4) + z * 4 + x; 44 | 45 | Some( 46 | *self 47 | .biome_palette 48 | .get(*biomes.get(index).unwrap() as usize) 49 | .unwrap(), 50 | ) 51 | } 52 | }; 53 | } 54 | 55 | pub fn iter_blocks(&self) -> SectionBlockIter { 56 | SectionBlockIter::new(self) 57 | } 58 | } 59 | 60 | impl From for Section { 61 | fn from(current_section: java::Section) -> Self { 62 | let blocks = current_section 63 | .block_states 64 | .try_iter_indices() 65 | .map(|block_iter| block_iter.map(|index| index as u16).collect()); 66 | 67 | let biomes = current_section 68 | .biomes 69 | .try_iter_indices() 70 | .map(|biome_iter| biome_iter.map(|index| index as u8).collect()); 71 | 72 | let mut sec = Section { 73 | block_palette: Vec::from(current_section.block_states.palette()), 74 | blocks, 75 | biome_palette: Vec::from(current_section.biomes.palette()), 76 | biomes, 77 | }; 78 | 79 | // We can find sections with no palettes, but still look like they 80 | // should be valid. We just assume they're full of air. #99. 81 | if sec.block_palette.is_empty() { 82 | sec.block_palette.push(AIR.clone()); 83 | } 84 | 85 | if sec.biome_palette.is_empty() { 86 | sec.biome_palette.push(Biome::Plains); 87 | } 88 | 89 | sec 90 | } 91 | } 92 | 93 | impl From<(Pre18Section, &[Biome])> for Section { 94 | fn from((current_section, current_biomes): (Pre18Section, &[Biome])) -> Self { 95 | let blocks; 96 | let block_pallet; 97 | 98 | match current_section.block_states { 99 | None => { 100 | block_pallet = vec![AIR.clone()]; 101 | blocks = None 102 | } 103 | Some(block_states) => { 104 | block_pallet = current_section.palette; 105 | 106 | blocks = Some( 107 | block_states 108 | .iter_indices(block_pallet.len()) 109 | .map(|index| index as u16) 110 | .collect(), 111 | ); 112 | } 113 | } 114 | 115 | let (biome_palette, biomes) = create_biome_palette(current_biomes); 116 | 117 | Section { 118 | block_palette: block_pallet, 119 | blocks, 120 | biome_palette, 121 | biomes, 122 | } 123 | } 124 | } 125 | 126 | impl From<(&[Block], &[Biome])> for Section { 127 | fn from((current_blocks, current_biomes): (&[Block], &[Biome])) -> Self { 128 | let mut blocks = vec![]; 129 | let mut block_palette = vec![]; 130 | 131 | for block in current_blocks { 132 | match block_palette 133 | .iter() 134 | .position(|check_block: &Block| check_block.name() == block.name()) 135 | { 136 | None => { 137 | block_palette.push(block.clone()); 138 | blocks.push((block_palette.len() - 1) as u16) 139 | } 140 | Some(index) => blocks.push(index as u16), 141 | } 142 | } 143 | 144 | let mut blocks = Some(blocks); 145 | 146 | //optimize if possible 147 | if block_palette.len() == 1 { 148 | blocks = None; 149 | } 150 | 151 | let (biome_palette, biomes) = create_biome_palette(current_biomes); 152 | 153 | Section { 154 | block_palette, 155 | blocks, 156 | biome_palette, 157 | biomes, 158 | } 159 | } 160 | } 161 | 162 | pub struct SectionBlockIter<'a> { 163 | section: &'a Section, 164 | 165 | block_index_iter: Option>, 166 | default_block_iter: Option>, 167 | } 168 | 169 | impl<'a> SectionBlockIter<'a> { 170 | pub fn new(section: &'a Section) -> Self { 171 | let mut iter = Self { 172 | section, 173 | block_index_iter: None, 174 | default_block_iter: None, 175 | }; 176 | 177 | match §ion.blocks { 178 | None => iter.default_block_iter = Some(0..(16 * 16 * 16)), 179 | Some(blocks) => iter.block_index_iter = Some(blocks.iter()), 180 | } 181 | 182 | iter 183 | } 184 | } 185 | 186 | impl<'a> Iterator for SectionBlockIter<'a> { 187 | type Item = &'a Block; 188 | 189 | fn next(&mut self) -> Option { 190 | return if let Some(iter) = self.default_block_iter.as_mut() { 191 | match iter.next() { 192 | None => None, 193 | Some(_) => self.section.block_palette.get(0), 194 | } 195 | } else { 196 | match self.block_index_iter.as_mut().unwrap().next() { 197 | None => None, 198 | Some(index) => self.section.block_palette.get(*index as usize), 199 | } 200 | }; 201 | } 202 | } 203 | 204 | //todo generic so it an also be used for blocks 205 | fn create_biome_palette(all_biomes: &[Biome]) -> (Vec, Option>) { 206 | let mut biomes = vec![]; 207 | let mut biome_palette = vec![]; 208 | 209 | for biome in all_biomes { 210 | match biome_palette 211 | .iter() 212 | .position(|check_biome| check_biome == biome) 213 | { 214 | None => { 215 | biome_palette.push(*biome); 216 | biomes.push((biome_palette.len() - 1) as u8) 217 | } 218 | Some(index) => biomes.push(index as u8), 219 | } 220 | } 221 | 222 | let mut biomes = Some(biomes); 223 | 224 | //optimize if possible 225 | if biome_palette.len() == 1 { 226 | biomes = None; 227 | } 228 | 229 | (biome_palette, biomes) 230 | } 231 | -------------------------------------------------------------------------------- /fastanvil/src/complete/section_tower.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use crate::biome::Biome; 4 | use crate::complete::section::{Section, SectionBlockIter}; 5 | use crate::pre13::Pre13Section; 6 | use crate::pre18::Pre18Section; 7 | use crate::{java, Block}; 8 | 9 | pub struct SectionTower { 10 | sections: Vec
, 11 | 12 | y_min: isize, 13 | y_max: isize, 14 | } 15 | 16 | impl SectionTower { 17 | pub fn block(&self, x: usize, y: isize, z: usize) -> Option<&Block> { 18 | let section_index = self.y_to_index(y); 19 | 20 | let section = self.sections.get(section_index).unwrap(); 21 | 22 | //first compute current section y then sub that from the ask y to get the y in the section 23 | let section_y = y - ((16 * section_index) as isize + self.y_min); 24 | 25 | section.block(x, section_y as usize, z) 26 | } 27 | 28 | pub fn biome(&self, x: usize, y: isize, z: usize) -> Option { 29 | let section_index = self.y_to_index(y); 30 | 31 | let section = self.sections.get(section_index).unwrap(); 32 | 33 | //first compute current section y then sub that from the ask y to get the y in the section 34 | let section_y = y - ((16 * section_index) as isize + self.y_min); 35 | 36 | section.biome(x, section_y as usize, z) 37 | } 38 | 39 | fn y_to_index(&self, y: isize) -> usize { 40 | ((y - self.y_min) / 16) as usize 41 | } 42 | 43 | pub fn y_range(&self) -> Range { 44 | self.y_min..self.y_max 45 | } 46 | 47 | pub fn iter_blocks(&self) -> SectionTowerBlockIter { 48 | SectionTowerBlockIter::new(self) 49 | } 50 | } 51 | 52 | impl From> for SectionTower { 53 | fn from(current_tower: java::SectionTower) -> Self { 54 | let mut tower = SectionTower { 55 | sections: vec![], 56 | y_min: current_tower.y_min(), 57 | y_max: current_tower.y_max(), 58 | }; 59 | 60 | // This looks suspicious, we're not using the map of sections. #99 61 | // This is only a problem because if there can be a terminating section 62 | // in 1.18 worlds? It looks like that's not the case. 63 | for section in current_tower.take_sections() { 64 | tower.sections.push(section.into()) 65 | } 66 | 67 | tower 68 | } 69 | } 70 | 71 | impl From<(java::SectionTower, Vec)> for SectionTower { 72 | fn from( 73 | (current_tower, current_biomes): (java::SectionTower, Vec), 74 | ) -> Self { 75 | let mut tower = SectionTower { 76 | sections: vec![], 77 | y_min: current_tower.y_min(), 78 | y_max: current_tower.y_max(), 79 | }; 80 | 81 | const BIOME_COUNT: usize = 4 * 4 * 4; 82 | 83 | //needed to skip first because it seems like there is a sections to much in the list 84 | // could be connected to java::section_tower.get_section_for_y -> todo 85 | for (index, section) in current_tower 86 | .take_sections() 87 | .into_iter() 88 | .enumerate() 89 | .skip(1) 90 | { 91 | tower.sections.push( 92 | ( 93 | section, 94 | ¤t_biomes[((index - 1) * BIOME_COUNT)..(index * BIOME_COUNT)], 95 | ) 96 | .into(), 97 | ); 98 | } 99 | 100 | tower 101 | } 102 | } 103 | 104 | impl From<(java::SectionTower, Vec, Vec)> for SectionTower { 105 | fn from( 106 | (current_tower, current_blocks, current_biomes): ( 107 | java::SectionTower, 108 | Vec, 109 | Vec, 110 | ), 111 | ) -> Self { 112 | let mut tower = SectionTower { 113 | sections: vec![], 114 | y_min: current_tower.y_min(), 115 | y_max: current_tower.y_max(), 116 | }; 117 | 118 | const BIOME_COUNT: usize = 4 * 4 * 4; 119 | const BLOCK_COUNT: usize = 16 * 16 * 16; 120 | 121 | for (index, _section) in current_tower.take_sections().into_iter().enumerate() { 122 | tower.sections.push( 123 | ( 124 | ¤t_blocks[(index * BLOCK_COUNT)..((index + 1) * BLOCK_COUNT)], 125 | ¤t_biomes[(index * BIOME_COUNT)..((index + 1) * BIOME_COUNT)], 126 | ) 127 | .into(), 128 | ); 129 | } 130 | 131 | tower 132 | } 133 | } 134 | 135 | pub struct SectionTowerBlockIter<'a> { 136 | sections: &'a Vec
, 137 | 138 | section_index_current: usize, 139 | section_iter_current: SectionBlockIter<'a>, 140 | } 141 | 142 | impl<'a> SectionTowerBlockIter<'a> { 143 | pub fn new(section_tower: &'a SectionTower) -> Self { 144 | Self { 145 | sections: §ion_tower.sections, 146 | section_iter_current: section_tower.sections.get(0).unwrap().iter_blocks(), 147 | section_index_current: 0, 148 | } 149 | } 150 | } 151 | 152 | impl<'a> Iterator for SectionTowerBlockIter<'a> { 153 | type Item = &'a Block; 154 | 155 | fn next(&mut self) -> Option { 156 | return match self.section_iter_current.next() { 157 | None => { 158 | //check if it was the last section 159 | if self.section_index_current >= self.sections.len() - 1 { 160 | return None; 161 | } 162 | 163 | self.section_index_current += 1; 164 | self.section_iter_current = self 165 | .sections 166 | .get(self.section_index_current) 167 | .unwrap() 168 | .iter_blocks(); 169 | 170 | self.section_iter_current.next() 171 | } 172 | Some(block) => Some(block), 173 | }; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /fastanvil/src/dimension.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Seek, Write}; 2 | use std::{error::Error, fmt::Display, ops::Range}; 3 | 4 | use crate::Region; 5 | use crate::{biome::Biome, Block}; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 8 | pub struct RCoord(pub isize); 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 11 | pub struct CCoord(pub isize); 12 | 13 | #[derive(Debug, Clone, Copy)] 14 | pub enum HeightMode { 15 | Trust, // trust height maps from chunk data 16 | Calculate, // calculate height maps manually, much slower. 17 | } 18 | 19 | pub trait Chunk: Send + Sync { 20 | // Status of the chunk. Typically anything except 'full' means the chunk 21 | // hasn't been fully generated yet. We use this to skip chunks on map edges 22 | // that haven't been fully generated yet. 23 | fn status(&self) -> String; 24 | 25 | /// Get the height of the first air-like block above something not air-like. 26 | /// Will panic if given x/z coordinates outside of 0..16. 27 | fn surface_height(&self, x: usize, z: usize, mode: HeightMode) -> isize; 28 | 29 | /// Get the biome of the given coordinate. A biome may not exist if the 30 | /// section of the chunk accessed is not present. For example, 31 | /// trying to access the block at height 1234 would return None. 32 | fn biome(&self, x: usize, y: isize, z: usize) -> Option; 33 | 34 | /// Get the block at the given coordinates. A block may not exist if the 35 | /// section of the chunk accessed is not present. For example, 36 | /// trying to access the block at height 1234 would return None. 37 | fn block(&self, x: usize, y: isize, z: usize) -> Option<&Block>; 38 | 39 | /// Get the range of Y values that are valid for this chunk. 40 | fn y_range(&self) -> Range; 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct LoaderError(pub(crate) String); 45 | 46 | pub type LoaderResult = std::result::Result; 47 | 48 | impl Error for LoaderError {} 49 | 50 | impl Display for LoaderError { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | f.write_str(&self.0) 53 | } 54 | } 55 | 56 | /// RegionLoader implmentations provide access to regions. They do not need to 57 | /// be concerned with caching, just providing an object that implements Region. 58 | /// 59 | /// An example implementation could be loading a region file from a local disk, 60 | /// or perhaps a WASM version loading from a file buffer in the browser. 61 | pub trait RegionLoader 62 | where 63 | S: Seek + Read + Write, 64 | { 65 | /// Get a particular region. Returns Ok(None) if region does not exist. 66 | fn region(&self, x: RCoord, z: RCoord) -> LoaderResult>>; 67 | 68 | /// List the regions that this loader can return. Implmentations need to 69 | /// provide this so that callers can efficiently find regions to process. 70 | fn list(&self) -> LoaderResult>; 71 | } 72 | -------------------------------------------------------------------------------- /fastanvil/src/files.rs: -------------------------------------------------------------------------------- 1 | use crate::{JavaChunk, LoaderError}; 2 | use crate::{LoaderResult, Region}; 3 | use crate::{RCoord, RegionLoader}; 4 | use std::fs::File; 5 | use std::io::ErrorKind; 6 | use std::marker::PhantomData; 7 | use std::{ 8 | fs, 9 | path::{Path, PathBuf}, 10 | }; 11 | 12 | pub struct RegionFileLoader { 13 | region_dir: PathBuf, 14 | _d: PhantomData, 15 | } 16 | 17 | impl RegionFileLoader { 18 | pub fn new(region_dir: PathBuf) -> Self { 19 | Self { 20 | region_dir, 21 | _d: PhantomData, 22 | } 23 | } 24 | 25 | pub fn has_region(&self, x: RCoord, z: RCoord) -> bool { 26 | let path = self.region_dir.join(format!("r.{}.{}.mca", x.0, z.0)); 27 | path.exists() 28 | } 29 | } 30 | 31 | impl RegionLoader for RegionFileLoader { 32 | fn region(&self, x: RCoord, z: RCoord) -> LoaderResult>> { 33 | let path = self.region_dir.join(format!("r.{}.{}.mca", x.0, z.0)); 34 | let file = match std::fs::File::open(path) { 35 | Ok(file) => file, 36 | Err(e) => { 37 | if e.kind() == ErrorKind::NotFound { 38 | return Ok(None); 39 | } else { 40 | return Err(LoaderError(e.to_string())); 41 | } 42 | } 43 | }; 44 | let region = Region::from_stream(file).map_err(|e| LoaderError(e.to_string()))?; 45 | 46 | Ok(Some(region)) 47 | } 48 | 49 | fn list(&self) -> LoaderResult> { 50 | let paths = std::fs::read_dir(&self.region_dir).map_err(|e| LoaderError(e.to_string()))?; 51 | 52 | let paths = paths 53 | .into_iter() 54 | .filter_map(|path| path.ok()) 55 | .map(|path| path.path()) 56 | .filter(|path| path.is_file()) 57 | .filter(|path| { 58 | let ext = path.extension(); 59 | ext.is_some() && ext.unwrap() == "mca" 60 | }) 61 | .filter(|path| fs::metadata(path).unwrap().len() > 0) 62 | .filter_map(|p| coords_from_region(&p)) 63 | .collect(); 64 | 65 | Ok(paths) 66 | } 67 | } 68 | 69 | fn coords_from_region(region: &Path) -> Option<(RCoord, RCoord)> { 70 | let filename = region.file_name()?.to_str()?; 71 | let mut parts = filename.split('.').skip(1); 72 | let x = parts.next()?.parse::().ok()?; 73 | let z = parts.next()?.parse::().ok()?; 74 | Some((RCoord(x), RCoord(z))) 75 | } 76 | -------------------------------------------------------------------------------- /fastanvil/src/java/block.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Block { 7 | pub(crate) name: String, 8 | pub(crate) encoded: String, 9 | pub(crate) archetype: BlockArchetype, 10 | } 11 | 12 | #[derive(Debug, PartialEq, Clone)] 13 | pub enum BlockArchetype { 14 | Normal, 15 | Airy, 16 | Watery, 17 | Snowy, 18 | } 19 | 20 | impl Block { 21 | pub fn name(&self) -> &str { 22 | &self.name 23 | } 24 | 25 | pub fn snowy(&self) -> bool { 26 | self.archetype == BlockArchetype::Snowy 27 | } 28 | 29 | /// A string of the format "id|prop1=val1,prop2=val2". The properties are 30 | /// ordered lexigraphically. This somewhat matches the way Minecraft stores 31 | /// variants in blockstates, but with the block ID/name prepended. 32 | pub fn encoded_description(&self) -> &str { 33 | &self.encoded 34 | } 35 | } 36 | 37 | #[derive(Deserialize)] 38 | #[serde(rename_all = "PascalCase")] 39 | struct BlockRaw { 40 | name: String, 41 | 42 | #[serde(default)] 43 | properties: HashMap, 44 | } 45 | 46 | impl<'de> Deserialize<'de> for Block { 47 | fn deserialize(deserializer: D) -> Result 48 | where 49 | D: serde::Deserializer<'de>, 50 | { 51 | let raw: BlockRaw = Deserialize::deserialize(deserializer)?; 52 | let snowy = raw.properties.get("snowy").map(String::as_str) == Some("true"); 53 | 54 | let mut id = raw.name.clone() + "|"; 55 | let mut sep = ""; 56 | 57 | let mut props = raw 58 | .properties 59 | .iter() 60 | .filter(|(k, _)| *k != "waterlogged") // TODO: Handle water logging. See note below 61 | .filter(|(k, _)| *k != "powered") // TODO: Handle power 62 | .collect::>(); 63 | 64 | // need to sort the properties for a consistent ID 65 | props.sort_unstable(); 66 | 67 | for (k, v) in props { 68 | id = id + sep + k + "=" + v; 69 | sep = ","; 70 | } 71 | 72 | let arch = if snowy { 73 | BlockArchetype::Snowy 74 | } else if is_watery(&raw.name) { 75 | BlockArchetype::Watery 76 | } else if is_airy(&raw.name) { 77 | BlockArchetype::Airy 78 | } else { 79 | BlockArchetype::Normal 80 | }; 81 | 82 | Ok(Self { 83 | name: raw.name, 84 | archetype: arch, 85 | encoded: id, 86 | }) 87 | } 88 | } 89 | 90 | /// Blocks that are considered as if they are water when determining colour. 91 | fn is_watery(block: &str) -> bool { 92 | matches!( 93 | block, 94 | "minecraft:water" 95 | | "minecraft:bubble_column" 96 | | "minecraft:kelp" 97 | | "minecraft:kelp_plant" 98 | | "minecraft:sea_grass" 99 | | "minecraft:tall_seagrass" 100 | ) 101 | } 102 | 103 | /// Blocks that are considered as if they are air when determining colour. 104 | fn is_airy(block: &str) -> bool { 105 | matches!(block, "minecraft:air" | "minecraft:cave_air") 106 | } 107 | -------------------------------------------------------------------------------- /fastanvil/src/java/chunk.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | use std::sync::RwLock; 3 | 4 | use serde::Deserialize; 5 | 6 | use crate::{biome::Biome, Block, Chunk, HeightMode}; 7 | use crate::{expand_heightmap, Heightmaps, Section, SectionTower}; 8 | 9 | use super::AIR; 10 | 11 | impl Chunk for CurrentJavaChunk { 12 | fn status(&self) -> String { 13 | self.status.clone() 14 | } 15 | 16 | fn surface_height(&self, x: usize, z: usize, mode: HeightMode) -> isize { 17 | let mut heightmap = self.lazy_heightmap.read().unwrap(); 18 | if heightmap.is_none() { 19 | drop(heightmap); 20 | self.recalculate_heightmap(mode); 21 | heightmap = self.lazy_heightmap.read().unwrap(); 22 | } 23 | heightmap.unwrap()[z * 16 + x] as isize 24 | } 25 | 26 | fn biome(&self, x: usize, y: isize, z: usize) -> Option { 27 | // Going to be awkward because the biomes are now paletted, and so are 28 | // the string not a number. 29 | 30 | let sections = self.sections.as_ref()?; 31 | let sec = sections.get_section_for_y(y)?; 32 | let sec_y = (y - sec.y as isize * 16) as usize; 33 | 34 | sec.biomes.at(x, sec_y, z).cloned() 35 | } 36 | 37 | fn block(&self, x: usize, y: isize, z: usize) -> Option<&Block> { 38 | let sections = self.sections.as_ref()?; 39 | let sec = sections.get_section_for_y(y)?; 40 | let sec_y = (y - sec.y as isize * 16) as usize; 41 | 42 | Some(sec.block_states.at(x, sec_y, z).unwrap_or(&AIR)) 43 | } 44 | 45 | fn y_range(&self) -> Range { 46 | match &self.sections { 47 | Some(sections) => Range { 48 | start: sections.y_min(), 49 | end: sections.y_max(), 50 | }, 51 | None => Range { start: 0, end: 0 }, 52 | } 53 | } 54 | } 55 | 56 | /// A Minecraft chunk. 57 | /// 58 | /// This type is not designed for accessing all data of a chunk. It provides the 59 | /// data necessary to render maps for fastanvil. If you need more complete data 60 | /// you need to write your own chunk struct and implement the serde traits, or 61 | /// use [`fastnbt::Value`]. 62 | #[derive(Deserialize, Debug)] 63 | pub struct CurrentJavaChunk { 64 | #[serde(rename = "DataVersion")] 65 | pub data_version: i32, 66 | 67 | // Maybe put section and heightmaps together and serde flatten? 68 | pub sections: Option>, 69 | 70 | #[serde(rename = "Heightmaps")] 71 | pub heightmaps: Option, 72 | 73 | #[serde(rename = "Status")] 74 | pub status: String, 75 | 76 | #[serde(skip)] 77 | pub(crate) lazy_heightmap: RwLock>, 78 | } 79 | 80 | impl CurrentJavaChunk { 81 | pub fn recalculate_heightmap(&self, mode: HeightMode) { 82 | // TODO: Find top section and start there, pointless checking 320 down 83 | // if its a 1.16 chunk. 84 | 85 | let mut map = [0; 256]; 86 | 87 | match mode { 88 | HeightMode::Trust => { 89 | let updated = self 90 | .heightmaps 91 | .as_ref() 92 | .and_then(|hm| hm.motion_blocking.as_ref()) 93 | .map(|hm| { 94 | let y_min = self.sections.as_ref().unwrap().y_min(); 95 | expand_heightmap(hm, y_min, self.data_version) 96 | }) 97 | .map(|hm| map.copy_from_slice(hm.as_slice())) 98 | .is_some(); 99 | 100 | if updated { 101 | *self.lazy_heightmap.write().unwrap() = Some(map); 102 | return; 103 | } 104 | } 105 | HeightMode::Calculate => {} // fall through to calc mode 106 | } 107 | 108 | let y_range = self.y_range(); 109 | let y_end = y_range.end; 110 | 111 | for z in 0..16 { 112 | for x in 0..16 { 113 | // start at top until we hit a non-air block. 114 | for i in y_range.clone() { 115 | let y = y_end - i; 116 | let block = self.block(x, y - 1, z); 117 | 118 | if block.is_none() { 119 | continue; 120 | } 121 | 122 | if !["minecraft:air", "minecraft:cave_air"] 123 | .as_ref() 124 | .contains(&block.unwrap().name()) 125 | { 126 | map[z * 16 + x] = y as i16; 127 | break; 128 | } 129 | } 130 | } 131 | } 132 | 133 | *self.lazy_heightmap.write().unwrap() = Some(map); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /fastanvil/src/java/heightmaps.rs: -------------------------------------------------------------------------------- 1 | use fastnbt::LongArray; 2 | use serde::Deserialize; 3 | 4 | /// Various heightmaps kept up to date by Minecraft. 5 | #[derive(Deserialize, Debug)] 6 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 7 | pub struct Heightmaps { 8 | pub motion_blocking: Option, 9 | //pub motion_blocking_no_leaves: Option, 10 | //pub ocean_floor: Option, 11 | //pub world_surface: Option, 12 | } 13 | -------------------------------------------------------------------------------- /fastanvil/src/java/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use fastnbt::{error::Result, from_bytes}; 4 | /// 1.2 to 1.12 5 | pub mod pre13; 6 | /// 1.13 to 1.17 7 | pub mod pre18; 8 | 9 | mod block; 10 | mod chunk; 11 | mod heightmaps; 12 | mod section; 13 | mod section_data; 14 | mod section_tower; 15 | 16 | pub use block::*; 17 | pub use chunk::*; 18 | pub use heightmaps::*; 19 | pub use section::*; 20 | pub use section_data::*; 21 | pub use section_tower::*; 22 | 23 | use once_cell::sync::Lazy; 24 | 25 | use crate::{biome::Biome, Chunk, HeightMode}; 26 | 27 | pub static AIR: Lazy = Lazy::new(|| Block { 28 | name: "minecraft:air".to_owned(), 29 | encoded: "minecraft:air|".to_owned(), 30 | archetype: BlockArchetype::Airy, 31 | }); 32 | pub static SNOW_BLOCK: Lazy = Lazy::new(|| Block { 33 | name: "minecraft:snow_block".to_owned(), 34 | encoded: "minecraft:snow_block|".to_owned(), 35 | archetype: BlockArchetype::Snowy, 36 | }); 37 | 38 | /// A Minecraft chunk. 39 | #[derive(Debug)] 40 | pub enum JavaChunk { 41 | Post18(CurrentJavaChunk), 42 | Pre18(pre18::JavaChunk), 43 | Pre13(pre13::JavaChunk), 44 | } 45 | 46 | impl JavaChunk { 47 | pub fn from_bytes(data: &[u8]) -> Result { 48 | let chunk: Result = from_bytes(data); 49 | 50 | match chunk { 51 | Ok(chunk) => Ok(Self::Post18(chunk)), 52 | Err(_) => match from_bytes::(data) { 53 | Ok(chunk) => Ok(Self::Pre18(chunk)), 54 | Err(_) => Ok(Self::Pre13(from_bytes::(data)?)), 55 | }, 56 | } 57 | } 58 | } 59 | 60 | // TODO: Find a better way to dispatch these methods. 61 | impl Chunk for JavaChunk { 62 | fn status(&self) -> String { 63 | match self { 64 | JavaChunk::Post18(c) => c.status(), 65 | JavaChunk::Pre18(c) => c.status(), 66 | JavaChunk::Pre13(c) => c.status(), 67 | } 68 | } 69 | 70 | fn surface_height(&self, x: usize, z: usize, mode: HeightMode) -> isize { 71 | match self { 72 | JavaChunk::Post18(c) => c.surface_height(x, z, mode), 73 | JavaChunk::Pre18(c) => c.surface_height(x, z, mode), 74 | JavaChunk::Pre13(c) => c.surface_height(x, z, mode), 75 | } 76 | } 77 | 78 | fn biome(&self, x: usize, y: isize, z: usize) -> Option { 79 | match self { 80 | JavaChunk::Post18(c) => c.biome(x, y, z), 81 | JavaChunk::Pre18(c) => c.biome(x, y, z), 82 | JavaChunk::Pre13(c) => c.biome(x, y, z), 83 | } 84 | } 85 | 86 | fn block(&self, x: usize, y: isize, z: usize) -> Option<&Block> { 87 | match self { 88 | JavaChunk::Post18(c) => c.block(x, y, z), 89 | JavaChunk::Pre18(c) => c.block(x, y, z), 90 | JavaChunk::Pre13(c) => c.block(x, y, z), 91 | } 92 | } 93 | 94 | fn y_range(&self) -> Range { 95 | match self { 96 | JavaChunk::Post18(c) => c.y_range(), 97 | JavaChunk::Pre18(c) => c.y_range(), 98 | JavaChunk::Pre13(c) => c.y_range(), 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /fastanvil/src/java/pre18.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::ops::Range; 3 | use std::sync::RwLock; 4 | 5 | use fastnbt::IntArray; 6 | use once_cell::sync::OnceCell; 7 | use serde::Deserialize; 8 | 9 | use crate::java::AIR; 10 | use crate::{biome::Biome, Block, Chunk, HeightMode}; 11 | use crate::{bits_per_block, expand_heightmap, Heightmaps, PackedBits, SectionLike, SectionTower}; 12 | 13 | /// A Minecraft chunk. 14 | #[derive(Deserialize, Debug)] 15 | #[serde(rename_all = "PascalCase")] 16 | pub struct JavaChunk { 17 | pub data_version: i32, 18 | pub level: Level, 19 | } 20 | 21 | impl Chunk for JavaChunk { 22 | fn status(&self) -> String { 23 | self.level.status.clone() 24 | } 25 | 26 | fn surface_height(&self, x: usize, z: usize, mode: HeightMode) -> isize { 27 | let mut heightmap = self.level.lazy_heightmap.read().unwrap(); 28 | if heightmap.is_none() { 29 | drop(heightmap); 30 | self.recalculate_heightmap(mode); 31 | heightmap = self.level.lazy_heightmap.read().unwrap(); 32 | } 33 | heightmap.unwrap()[z * 16 + x] as isize 34 | } 35 | 36 | fn biome(&self, x: usize, y: isize, z: usize) -> Option { 37 | let biomes = self.level.biomes.as_ref()?; 38 | 39 | // After 1.15 Each biome in i32, biomes split into 4-wide cubes, so 40 | // 4x4x4 per section. 41 | 42 | // v1.15 was only x/z, i32 per column. 43 | const V1_15: usize = 16 * 16; 44 | 45 | match biomes.len() { 46 | V1_15 => { 47 | // 1x1 columns stored z then x. 48 | let i = z * 16 + x; 49 | let biome = biomes[i]; 50 | Biome::try_from(biome).ok() 51 | } 52 | _ => { 53 | // Assume latest 54 | let range = self.y_range(); 55 | if range.is_empty() { 56 | // Sometimes biomes can be specified, but there's actually 57 | // no blocks. 58 | return None; 59 | } 60 | let y_shifted = (y.clamp(range.start, range.end - 1) - range.start) as usize; 61 | let i = (z / 4) * 4 + (x / 4) + (y_shifted / 4) * 16; 62 | 63 | let biome = *biomes.get(i)?; 64 | 65 | Biome::try_from(biome).ok() 66 | } 67 | } 68 | } 69 | 70 | fn block(&self, x: usize, y: isize, z: usize) -> Option<&Block> { 71 | let sec = self.level.sections.as_ref()?.get_section_for_y(y)?; 72 | 73 | // If a section is entirely air, then the block states are missing 74 | // entirely, presumably to save space. 75 | match &sec.block_states { 76 | None => Some(&AIR), 77 | Some(blockstates) => { 78 | let sec_y = (y - sec.y as isize * 16) as usize; 79 | let pal_index = blockstates.state(x, sec_y, z, sec.palette.len()); 80 | sec.palette.get(pal_index) 81 | } 82 | } 83 | } 84 | 85 | fn y_range(&self) -> std::ops::Range { 86 | match &self.level.sections { 87 | Some(sections) => Range { 88 | start: sections.y_min(), 89 | end: sections.y_max(), 90 | }, 91 | None => Range { start: 0, end: 0 }, 92 | } 93 | } 94 | } 95 | 96 | /// A level describes the contents of the chunk in the world. 97 | #[derive(Deserialize, Debug)] 98 | #[serde(rename_all = "PascalCase")] 99 | pub struct Level { 100 | #[serde(rename = "xPos")] 101 | pub x_pos: i32, 102 | 103 | #[serde(rename = "zPos")] 104 | pub z_pos: i32, 105 | 106 | pub biomes: Option, 107 | 108 | /// Can be empty if the chunk hasn't been generated properly yet. 109 | pub sections: Option>, 110 | 111 | pub heightmaps: Option, 112 | 113 | // Status of the chunk. Typically anything except 'full' means the chunk 114 | // hasn't been fully generated yet. We use this to skip chunks on map edges 115 | // that haven't been fully generated yet. 116 | pub status: String, 117 | 118 | #[serde(skip)] 119 | pub(crate) lazy_heightmap: RwLock>, 120 | } 121 | 122 | impl JavaChunk { 123 | pub fn recalculate_heightmap(&self, mode: HeightMode) { 124 | // TODO: Find top section and start there, pointless checking 320 down 125 | // if its a 1.16 chunk. 126 | 127 | let mut map = [0; 256]; 128 | 129 | match mode { 130 | HeightMode::Trust => { 131 | let updated = self 132 | .level 133 | .heightmaps 134 | .as_ref() 135 | .and_then(|hm| hm.motion_blocking.as_ref()) 136 | .map(|hm| { 137 | // unwrap, if heightmaps exists, sections should... 🤞 138 | let y_min = self.level.sections.as_ref().unwrap().y_min(); 139 | expand_heightmap(hm, y_min, self.data_version) 140 | }) 141 | .map(|hm| map.copy_from_slice(hm.as_slice())) 142 | .is_some(); 143 | 144 | if updated { 145 | *self.level.lazy_heightmap.write().unwrap() = Some(map); 146 | return; 147 | } 148 | } 149 | HeightMode::Calculate => {} // fall through to calc mode 150 | } 151 | 152 | let y_range = self.y_range(); 153 | let y_end = y_range.end; 154 | 155 | for z in 0..16 { 156 | for x in 0..16 { 157 | // start at top until we hit a non-air block. 158 | for i in y_range.clone() { 159 | let y = y_end - i; 160 | let block = self.block(x, y - 1, z); 161 | 162 | if block.is_none() { 163 | continue; 164 | } 165 | 166 | if !["minecraft:air", "minecraft:cave_air"] 167 | .as_ref() 168 | .contains(&block.unwrap().name()) 169 | { 170 | map[z * 16 + x] = y as i16; 171 | break; 172 | } 173 | } 174 | } 175 | } 176 | 177 | *self.level.lazy_heightmap.write().unwrap() = Some(map); 178 | } 179 | } 180 | 181 | /// A vertical section of a chunk (ie a 16x16x16 block cube), for before 1.18. 182 | #[derive(Deserialize, Debug)] 183 | #[serde(rename_all = "PascalCase")] 184 | pub struct Pre18Section { 185 | pub y: i8, 186 | 187 | pub block_states: Option, 188 | 189 | #[serde(default)] 190 | pub palette: Vec, 191 | } 192 | 193 | impl SectionLike for Pre18Section { 194 | fn is_terminator(&self) -> bool { 195 | self.palette.is_empty() && self.block_states.is_none() 196 | } 197 | 198 | fn y(&self) -> i8 { 199 | self.y 200 | } 201 | } 202 | 203 | #[derive(Debug)] 204 | pub struct Pre18Blockstates { 205 | unpacked: OnceCell<[u16; 16 * 16 * 16]>, 206 | packed: PackedBits, 207 | } 208 | 209 | impl Pre18Blockstates { 210 | /// Get the state for the given block at x,y,z, where x, y, and z are 211 | /// relative to the section ie 0..16 212 | #[inline(always)] 213 | pub fn state(&self, x: usize, sec_y: usize, z: usize, pal_len: usize) -> usize { 214 | let unpacked = self.unpacked.get_or_init(|| { 215 | let bits_per_item = bits_per_block(pal_len); 216 | let mut buf = [0u16; 16 * 16 * 16]; 217 | self.packed.unpack_blockstates(bits_per_item, &mut buf); 218 | buf 219 | }); 220 | 221 | let state_index = (sec_y * 16 * 16) + z * 16 + x; 222 | unpacked[state_index] as usize 223 | } 224 | 225 | /// Get iterator for the state indicies. This will increase in x, then z, 226 | /// then y. These indicies are used with the relevant palette to get the 227 | /// data for that block. 228 | /// 229 | /// The pal_len must be the length of the palette corresponding to these 230 | /// blockstates. 231 | /// 232 | /// You can recover the coordinate be enumerating the iterator: 233 | /// 234 | /// ```no_run 235 | /// # use fastanvil::pre18::Pre18Blockstates; 236 | /// # fn main() { 237 | /// # let states: Pre18Blockstates = todo!(); 238 | /// for (i, block_index) in states.iter_indices(10).enumerate() { 239 | /// let x = i & 0x000F; 240 | /// let y = (i & 0x0F00) >> 8; 241 | /// let z = (i & 0x00F0) >> 4; 242 | /// } 243 | /// # } 244 | /// ``` 245 | pub fn iter_indices(&self, pal_len: usize) -> impl Iterator + '_ { 246 | let unpacked = self.unpacked.get_or_init(|| { 247 | let bits_per_item = bits_per_block(pal_len); 248 | let mut buf = [0u16; 16 * 16 * 16]; 249 | self.packed.unpack_blockstates(bits_per_item, &mut buf); 250 | buf 251 | }); 252 | 253 | unpacked.iter().map(|&i| i as usize) 254 | } 255 | } 256 | 257 | impl<'de> Deserialize<'de> for Pre18Blockstates { 258 | fn deserialize(d: D) -> Result 259 | where 260 | D: serde::Deserializer<'de>, 261 | { 262 | let packed: PackedBits = Deserialize::deserialize(d)?; 263 | Ok(Self { 264 | packed, 265 | unpacked: OnceCell::new(), 266 | }) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /fastanvil/src/java/section.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::{biome::Biome, BiomeData, Block, BlockData}; 4 | 5 | pub trait SectionLike { 6 | fn is_terminator(&self) -> bool; 7 | fn y(&self) -> i8; 8 | } 9 | 10 | /// A vertical section of a chunk (ie a 16x16x16 block cube) 11 | #[derive(Deserialize, Debug)] 12 | pub struct Section { 13 | #[serde(rename = "Y")] 14 | pub y: i8, 15 | 16 | #[serde(default)] 17 | pub block_states: BlockData, 18 | 19 | #[serde(default)] 20 | pub biomes: BiomeData, 21 | } 22 | 23 | impl SectionLike for Section { 24 | fn is_terminator(&self) -> bool { 25 | // Seems impossible to determine for post 1.18 sections, as sections can 26 | // be saved with no block states or palette but still be a legitimate 27 | // part of the world. 28 | false 29 | } 30 | 31 | fn y(&self) -> i8 { 32 | self.y 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fastanvil/src/java/section_data.rs: -------------------------------------------------------------------------------- 1 | use bit_field::BitField; 2 | use fastnbt::LongArray; 3 | 4 | use serde::Deserialize; 5 | use std::fmt::Debug; 6 | 7 | #[derive(Deserialize, Debug)] 8 | #[serde(transparent)] 9 | pub struct BlockData { 10 | inner: DataInner, 11 | } 12 | 13 | #[derive(Deserialize, Debug)] 14 | #[serde(transparent)] 15 | pub struct BiomeData { 16 | inner: DataInner, 17 | } 18 | 19 | impl BlockData { 20 | /// Get the block data for the block at x,y,z, where x,y,z are relative to 21 | /// the section ie 0..16. 22 | pub fn at(&self, x: usize, sec_y: usize, z: usize) -> Option<&T> { 23 | let state_index = (sec_y * 16 * 16) + z * 16 + x; 24 | self.inner.at( 25 | state_index, 26 | blockstates_bits_per_block(self.inner.palette.len()), 27 | ) 28 | } 29 | 30 | /// Get iterator for the state indicies. This will increase in x, then z, 31 | /// then y. These indicies are used with the relevant palette to get the 32 | /// data for that block. 33 | /// 34 | /// This returns None if no block states were present. This typically means 35 | /// the section was empty (ie filled with air). 36 | /// 37 | /// You can recover the coordinate be enumerating the iterator: 38 | /// 39 | /// ```no_run 40 | /// # use fastanvil::BlockData; 41 | /// # let states: BlockData = todo!(); 42 | /// for (i, block_index) in states.try_iter_indices().unwrap().enumerate() { 43 | /// let x = i & 0x000F; 44 | /// let y = (i & 0x0F00) >> 8; 45 | /// let z = (i & 0x00F0) >> 4; 46 | /// } 47 | /// ``` 48 | pub fn try_iter_indices(&self) -> Option { 49 | if let Some(data) = &self.inner.data { 50 | let bits = blockstates_bits_per_block(self.inner.palette.len()); 51 | Some(StatesIter::new(bits, 16 * 16 * 16, data)) 52 | } else { 53 | None 54 | } 55 | } 56 | 57 | /// Get the palette for this block data. Indicies into this palette can be 58 | /// obtained via [`try_iter_indices`][`BlockData::try_iter_indices`]. 59 | pub fn palette(&self) -> &[T] { 60 | self.inner.palette.as_slice() 61 | } 62 | } 63 | 64 | impl BiomeData { 65 | pub fn at(&self, x: usize, sec_y: usize, z: usize) -> Option<&T> { 66 | // Caution: int division, so lops of remainder of 4, so you can't just 67 | // remove a *4 and /4 and get the same results. 68 | let x = x / 4; 69 | let y = sec_y / 4; 70 | let z = z / 4; 71 | 72 | let state_index = (y * 4 * 4) + z * 4 + x; 73 | self.inner 74 | .at(state_index, biomes_bits_per_block(self.inner.palette.len())) 75 | } 76 | 77 | pub fn try_iter_indices(&self) -> Option { 78 | if let Some(data) = &self.inner.data { 79 | let bits = biomes_bits_per_block(self.inner.palette.len()); 80 | Some(StatesIter::new(bits, 4 * 4 * 4, data)) 81 | } else { 82 | None 83 | } 84 | } 85 | 86 | pub fn palette(&self) -> &[T] { 87 | self.inner.palette.as_slice() 88 | } 89 | } 90 | 91 | #[derive(Deserialize, Debug)] 92 | struct DataInner { 93 | data: Option, 94 | palette: Vec, 95 | } 96 | 97 | impl DataInner { 98 | pub fn at(&self, index: usize, bits_per_item: usize) -> Option<&T> { 99 | if self.data.is_none() && self.palette.len() == 1 { 100 | return self.palette.get(0); 101 | } 102 | 103 | let data = self.data.as_ref()?; 104 | 105 | let values_per_64bits = 64 / bits_per_item; 106 | 107 | let long_index = index / values_per_64bits; 108 | let inter_index = index % values_per_64bits; 109 | let range = inter_index * bits_per_item..(inter_index + 1) * bits_per_item; 110 | 111 | // Super important line: treat the i64 as an u64. 112 | // Bug 1: Kept i64 and the get_bits interprets as signed. 113 | // Bug 2: Went to usize, worked on 64bit platforms broke on 32 bit like WASM. 114 | let long = data[long_index] as u64; 115 | 116 | let palette_index = long.get_bits(range); 117 | 118 | self.palette.get(palette_index as usize) 119 | } 120 | } 121 | 122 | // Block states at the least can be missing from the world data. This typically 123 | // just means that it's a big block of air. We default the DataInner and let the 124 | // fact data is None to also return none. Rather than have BlockData be optional 125 | // in the chunk struct. 126 | impl Default for DataInner { 127 | fn default() -> Self { 128 | Self { 129 | data: Default::default(), 130 | palette: Default::default(), 131 | } 132 | } 133 | } 134 | 135 | impl Default for BlockData { 136 | fn default() -> Self { 137 | Self { 138 | inner: Default::default(), 139 | } 140 | } 141 | } 142 | 143 | impl Default for BiomeData { 144 | fn default() -> Self { 145 | Self { 146 | inner: Default::default(), 147 | } 148 | } 149 | } 150 | 151 | /// Number of bits that will be used per block in block_states data for blocks. 152 | fn blockstates_bits_per_block(palette_len: usize) -> usize { 153 | std::cmp::max(4, min_bits_for_n_states(palette_len)) 154 | // std::cmp::max((palette_len as f64).log2().ceil() as usize, 4) 155 | } 156 | 157 | /// Number of bits that will be used per block in block_states data for biomes. 158 | fn biomes_bits_per_block(palette_len: usize) -> usize { 159 | std::cmp::max(1, min_bits_for_n_states(palette_len)) 160 | // std::cmp::max((palette_len as f64).log2().ceil() as usize, 1) 161 | } 162 | 163 | pub(crate) fn min_bits_for_n_states(palette_len: usize) -> usize { 164 | (usize::BITS - (palette_len - 1).leading_zeros()) as usize 165 | } 166 | 167 | /// Iterator over block state data. Each value is the index into the relevant palette. 168 | pub struct StatesIter<'a> { 169 | inner: &'a [i64], // remaining data to iterate. 170 | stride: usize, // stride in bits we're iterating. 171 | pos: usize, // bit position the next value starts at. 172 | // need to track the 'remaining' because some of the last long might be padding. 173 | remaining: usize, 174 | } 175 | 176 | impl<'a> StatesIter<'a> { 177 | pub(crate) fn new(stride: usize, len: usize, inner: &'a [i64]) -> Self { 178 | Self { 179 | inner, 180 | stride, 181 | remaining: len, 182 | pos: 0, 183 | } 184 | } 185 | } 186 | 187 | impl<'a> Iterator for StatesIter<'a> { 188 | type Item = usize; 189 | 190 | fn next(&mut self) -> Option { 191 | if self.remaining == 0 { 192 | return None; 193 | } 194 | 195 | let start = self.pos; 196 | self.pos += self.stride; 197 | let end = self.pos; 198 | let datum = *(self.inner.first()?) as u64; 199 | 200 | let value = datum.get_bits(start..end) as usize; 201 | self.remaining -= 1; 202 | 203 | if self.pos + self.stride > 64 { 204 | self.pos = 0; 205 | self.inner = &self.inner[1..]; 206 | } 207 | 208 | Some(value) 209 | } 210 | 211 | fn size_hint(&self) -> (usize, Option) { 212 | (self.remaining, Some(self.remaining)) 213 | } 214 | } 215 | 216 | impl<'a> ExactSizeIterator for StatesIter<'a> {} 217 | -------------------------------------------------------------------------------- /fastanvil/src/java/section_tower.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::SectionLike; 4 | 5 | /// SectionTower represents the set of sections that make up a Minecraft chunk. 6 | /// It has a custom deserialization in order to more efficiently lay out the 7 | /// sections for quick access. 8 | #[derive(Debug)] 9 | pub struct SectionTower { 10 | sections: Vec, 11 | map: Vec>, 12 | y_min: isize, 13 | y_max: isize, 14 | } 15 | 16 | impl SectionTower { 17 | pub fn sections(&self) -> &[S] { 18 | &self.sections 19 | } 20 | 21 | pub(crate) fn take_sections(self) -> Vec { 22 | self.sections 23 | } 24 | 25 | pub fn get_section_for_y(&self, y: isize) -> Option<&S> { 26 | if y >= self.y_max || y < self.y_min { 27 | // TODO: This occurs a lot in hermitcraft season 7. Probably some 28 | // form of bug? 29 | return None; 30 | } 31 | 32 | let lookup_index = y_to_index(y, self.y_min); 33 | 34 | let section_index = *self.map.get(lookup_index as usize)?; 35 | self.sections.get(section_index?) 36 | } 37 | 38 | pub fn y_min(&self) -> isize { 39 | self.y_min 40 | } 41 | 42 | pub fn y_max(&self) -> isize { 43 | self.y_max 44 | } 45 | } 46 | 47 | impl<'de, S: SectionLike + Deserialize<'de>> Deserialize<'de> for SectionTower { 48 | fn deserialize(deserializer: D) -> Result 49 | where 50 | D: serde::Deserializer<'de>, 51 | { 52 | let sections: Vec = Deserialize::deserialize(deserializer)?; 53 | if sections.is_empty() { 54 | return Ok(Self { 55 | sections, 56 | map: vec![], 57 | y_min: 0, 58 | y_max: 0, 59 | }); 60 | } 61 | 62 | // We need to figure out how deep the world goes. Since 1.17 the depth 63 | // of worlds can be customized. Each section in the chunk will have a 64 | // 'y' value. We need to find the minimum value here, and that will tell 65 | // us the minimum world y. 66 | let lowest_section = sections 67 | .iter() 68 | .min_by_key(|s| s.y()) 69 | .expect("checked no empty above"); 70 | 71 | // Sometimes the lowest section is a 'null' section. This isn't actually 72 | // part of the world, it just indicates there no more sections below. 73 | // You can tell if it's 'null terminated' by the palette and 74 | // blockstates. 75 | let null_term = lowest_section.is_terminator(); 76 | 77 | let min = if null_term { 78 | lowest_section.y() as isize + 1 79 | } else { 80 | lowest_section.y() as isize 81 | }; 82 | let max = sections 83 | .iter() 84 | .max_by_key(|s| s.y()) 85 | .map(|s| s.y()) 86 | .unwrap() as isize; 87 | 88 | let mut sparse_sections = vec![None; (1 + max - min) as usize]; 89 | 90 | for (i, sec) in sections.iter().enumerate() { 91 | // Don't bother adding the null section. 92 | if sec.y() == lowest_section.y() && null_term { 93 | continue; 94 | } 95 | 96 | let sec_index = (sec.y() as isize - min) as usize; 97 | 98 | sparse_sections[sec_index] = Some(i); 99 | } 100 | 101 | Ok(Self { 102 | sections, 103 | map: sparse_sections, 104 | y_min: 16 * min, 105 | y_max: 16 * (max + 1), 106 | }) 107 | } 108 | } 109 | 110 | const fn y_to_index(y: isize, y_min: isize) -> u8 { 111 | ((y - y_min) >> 4) as u8 112 | } 113 | -------------------------------------------------------------------------------- /fastanvil/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! For handling Minecraft's region format, Anvil. This crate is mostly to 2 | //! support creating maps of Minecraft worlds and is not stable (per-1.0). The 3 | //! [`Region`] struct is probably the most generally useful part in this crate. 4 | //! 5 | //! This crate also contains a [`JavaChunk`] that allows deserializing 1.18 6 | //! down to about 1.15 chunks into some structs. This doesn't record all 7 | //! information from a chunk however, eg entities are lost. It is not suitable 8 | //! for serializing back into a region. 9 | //! 10 | //! You can create your own chunk structures to (de)serialize using [`fastnbt`]. 11 | //! 12 | //! [`Region`] can be given a `Read`, `Write` and `Seek` type eg a file in 13 | //! order to read and write chunk data. 14 | //! 15 | //! # Crate features 16 | //! 17 | //! * **render** - 18 | //! This feature is enabled by default and encapsulates all world-rendering related functionality. 19 | 20 | pub mod biome; 21 | pub mod tex; 22 | pub mod complete; 23 | 24 | mod bits; 25 | mod dimension; 26 | mod files; 27 | mod java; 28 | mod region; 29 | #[cfg(feature = "render")] 30 | mod render; 31 | #[cfg(feature = "render")] 32 | mod rendered_palette; 33 | 34 | pub use bits::*; 35 | pub use dimension::*; 36 | pub use files::*; 37 | pub use java::*; 38 | pub use region::*; 39 | #[cfg(feature = "render")] 40 | pub use render::*; 41 | #[cfg(feature = "render")] 42 | pub use rendered_palette::*; 43 | 44 | #[cfg(test)] 45 | mod test; 46 | 47 | #[derive(Debug)] 48 | pub enum Error { 49 | IO(std::io::Error), 50 | InvalidOffset(isize, isize), 51 | UnknownCompression(u8), 52 | ChunkTooLarge, 53 | } 54 | 55 | impl From for Error { 56 | fn from(err: std::io::Error) -> Error { 57 | Error::IO(err) 58 | } 59 | } 60 | 61 | pub type Result = std::result::Result; 62 | 63 | impl std::fmt::Display for Error { 64 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 65 | match self { 66 | Error::IO(e) => f.write_fmt(format_args!("io error: {e:?}")), 67 | Error::InvalidOffset(x, z) => { 68 | f.write_fmt(format_args!("invalid offset: x = {x}, z = {z}")) 69 | } 70 | Error::UnknownCompression(scheme) => f.write_fmt(format_args!( 71 | "compression scheme ({scheme}) was not recognised for chunk" 72 | )), 73 | Error::ChunkTooLarge => f.write_str("chunk too large to store"), 74 | } 75 | } 76 | } 77 | 78 | impl std::error::Error for Error {} 79 | -------------------------------------------------------------------------------- /fastanvil/src/rendered_palette.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::Display, 4 | io::{BufReader, Read}, 5 | }; 6 | 7 | use flate2::bufread::GzDecoder; 8 | use log::debug; 9 | 10 | use crate::{biome::Biome, Block, Palette, Rgba, SNOW_BLOCK}; 11 | 12 | pub struct RenderedPalette { 13 | pub blockstates: std::collections::HashMap, 14 | pub grass: image::RgbaImage, 15 | pub foliage: image::RgbaImage, 16 | } 17 | 18 | impl RenderedPalette { 19 | fn pick_grass(&self, b: Option) -> Rgba { 20 | b.map(|b| { 21 | let climate = b.climate(); 22 | let t = climate.temperature.clamp(0., 1.); 23 | let r = climate.rainfall.clamp(0., 1.) * t; 24 | 25 | let t = 255 - (t * 255.).ceil() as u32; 26 | let r = 255 - (r * 255.).ceil() as u32; 27 | 28 | self.grass.get_pixel(t, r).0 29 | }) 30 | .unwrap_or([255, 0, 0, 0]) 31 | } 32 | 33 | fn pick_foliage(&self, b: Option) -> Rgba { 34 | b.map(|b| { 35 | let climate = b.climate(); 36 | let t = climate.temperature.clamp(0., 1.); 37 | let r = climate.rainfall.clamp(0., 1.) * t; 38 | 39 | let t = 255 - (t * 255.).ceil() as u32; 40 | let r = 255 - (r * 255.).ceil() as u32; 41 | 42 | self.foliage.get_pixel(t, r).0 43 | }) 44 | .unwrap_or([255, 0, 0, 0]) 45 | } 46 | 47 | fn pick_water(&self, b: Option) -> Rgba { 48 | use Biome::*; 49 | b.map(|b| match b { 50 | Swamp => [0x61, 0x7B, 0x64, 255], 51 | River => [0x3F, 0x76, 0xE4, 255], 52 | Ocean => [0x3F, 0x76, 0xE4, 255], 53 | LukewarmOcean => [0x45, 0xAD, 0xF2, 255], 54 | WarmOcean => [0x43, 0xD5, 0xEE, 255], 55 | ColdOcean => [0x3D, 0x57, 0xD6, 255], 56 | FrozenRiver => [0x39, 0x38, 0xC9, 255], 57 | FrozenOcean => [0x39, 0x38, 0xC9, 255], 58 | _ => [0x3f, 0x76, 0xe4, 255], 59 | }) 60 | .unwrap_or([0x3f, 0x76, 0xe4, 255]) 61 | } 62 | } 63 | 64 | impl Palette for RenderedPalette { 65 | fn pick(&self, block: &Block, biome: Option) -> Rgba { 66 | let missing_colour = [255, 0, 255, 255]; 67 | 68 | // A bunch of blocks in the game seem to be special cased outside of the 69 | // blockstate/model mechanism. For example leaves get coloured based on 70 | // the tree type and the biome type, but this is not encoded in the 71 | // blockstate or the model. 72 | // 73 | // This means we have to do a bunch of complex conditional logic in one 74 | // of the most called functions. Yuck. 75 | if let Some(id) = block.name().strip_prefix("minecraft:") { 76 | match id { 77 | "grass" | "tall_grass" | "vine" | "fern" | "large_fern" | "short_grass" => { 78 | return self.pick_grass(biome); 79 | } 80 | "grass_block" => { 81 | if block.snowy() { 82 | return self.pick(&SNOW_BLOCK, biome); 83 | } else { 84 | return self.pick_grass(biome); 85 | }; 86 | } 87 | "water" | "bubble_column" => return self.pick_water(biome), 88 | "oak_leaves" | "jungle_leaves" | "acacia_leaves" | "dark_oak_leaves" 89 | | "mangrove_leaves" => return self.pick_foliage(biome), 90 | "birch_leaves" => { 91 | return [0x80, 0xa7, 0x55, 255]; // game hardcodes this 92 | } 93 | "spruce_leaves" => { 94 | return [0x61, 0x99, 0x61, 255]; // game hardcodes this 95 | } 96 | // Kelp and seagrass don't look like much from the top as 97 | // they're flat. Maybe in future hard code a green tint to make 98 | // it show up? 99 | "kelp" | "kelp_plant" | "seagrass" | "tall_seagrass" => { 100 | return self.pick_water(biome); 101 | } 102 | "snow" => { 103 | return self.pick(&SNOW_BLOCK, biome); 104 | } 105 | // Occurs a lot for the end, as layer 0 will be air in the void. 106 | // Rendering it black makes sense in the end, but might look 107 | // weird if it ends up elsewhere. 108 | "air" => { 109 | return [0, 0, 0, 255]; 110 | } 111 | "cave_air" => { 112 | return [255, 0, 0, 255]; // when does this happen?? 113 | } 114 | // Otherwise fall through to the general mechanism. 115 | _ => {} 116 | } 117 | } 118 | 119 | let col = self 120 | .blockstates 121 | .get(block.encoded_description()) 122 | .or_else(|| self.blockstates.get(block.name())); 123 | 124 | match col { 125 | Some(c) => *c, 126 | None => { 127 | debug!("could not draw {}", block.encoded_description()); 128 | missing_colour 129 | } 130 | } 131 | } 132 | } 133 | 134 | #[derive(Debug)] 135 | pub struct PaletteError(String); 136 | 137 | impl PaletteError { 138 | fn new(err: impl Error) -> PaletteError { 139 | Self(err.to_string()) 140 | } 141 | } 142 | 143 | impl Display for PaletteError { 144 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 145 | f.write_str(&self.0) 146 | } 147 | } 148 | 149 | impl Error for PaletteError {} 150 | 151 | /** 152 | * Load a prepared rendered palette. This is for use with the palette.tar.gz 153 | * alongside the fastnbt project repository. 154 | */ 155 | pub fn load_rendered_palette( 156 | palette: impl Read, 157 | ) -> std::result::Result { 158 | let f = GzDecoder::new(BufReader::new(palette)); 159 | let mut ar = tar::Archive::new(f); 160 | let mut grass = Err(PaletteError("no grass colour map".to_owned())); 161 | let mut foliage = Err(PaletteError("no foliage colour map".to_owned())); 162 | let mut blockstates = Err(PaletteError("no blockstate palette".to_owned())); 163 | 164 | for file in ar.entries().map_err(PaletteError::new)? { 165 | let mut file = file.map_err(PaletteError::new)?; 166 | match file 167 | .path() 168 | .map_err(PaletteError::new)? 169 | .to_str() 170 | .ok_or(PaletteError("invalid path".to_owned()))? 171 | { 172 | "grass-colourmap.png" => { 173 | let mut buf = vec![]; 174 | file.read_to_end(&mut buf).map_err(PaletteError::new)?; 175 | 176 | grass = Ok( 177 | image::load(std::io::Cursor::new(buf), image::ImageFormat::Png) 178 | .map_err(PaletteError::new)? 179 | .into_rgba8(), 180 | ); 181 | } 182 | "foliage-colourmap.png" => { 183 | let mut buf = vec![]; 184 | file.read_to_end(&mut buf).map_err(PaletteError::new)?; 185 | 186 | foliage = Ok( 187 | image::load(std::io::Cursor::new(buf), image::ImageFormat::Png) 188 | .map_err(PaletteError::new)? 189 | .into_rgba8(), 190 | ); 191 | } 192 | "blockstates.json" => { 193 | let json: std::collections::HashMap = 194 | serde_json::from_reader(file).map_err(PaletteError::new)?; 195 | blockstates = Ok(json); 196 | } 197 | _ => {} 198 | } 199 | } 200 | 201 | Ok(RenderedPalette { 202 | blockstates: blockstates?, 203 | grass: grass?, 204 | foliage: foliage?, 205 | }) 206 | } 207 | -------------------------------------------------------------------------------- /fastanvil/src/test/complete_chunk.rs: -------------------------------------------------------------------------------- 1 | use crate::{complete, Chunk, HeightMode, JavaChunk, Region}; 2 | 3 | fn get_test_chunk() -> Vec<(JavaChunk, complete::Chunk)> { 4 | //todo better test region (different bioms) 5 | let file = std::fs::File::open("./resources/1.19.4.mca").unwrap(); 6 | 7 | let mut region = Region::from_stream(file).unwrap(); 8 | let data = region.read_chunk(0, 0).unwrap().unwrap(); 9 | 10 | let current_java_chunk = JavaChunk::from_bytes(data.as_slice()).unwrap(); 11 | let complete_chunk_current = complete::Chunk::from_bytes(data.as_slice()).unwrap(); 12 | 13 | let chunk_pre18 = 14 | JavaChunk::from_bytes(include_bytes!("../../resources/1.17.1.chunk")).unwrap(); 15 | 16 | let complete_chunk_pre18 = 17 | complete::Chunk::from_bytes(include_bytes!("../../resources/1.17.1.chunk")).unwrap(); 18 | 19 | let chunk_pre13 = 20 | JavaChunk::from_bytes(include_bytes!("../../resources/1.12.chunk")).unwrap(); 21 | 22 | let complete_chunk_pre13 = 23 | complete::Chunk::from_bytes(include_bytes!("../../resources/1.12.chunk")).unwrap(); 24 | 25 | vec![ 26 | (current_java_chunk, complete_chunk_current), 27 | (chunk_pre18, complete_chunk_pre18), 28 | (chunk_pre13, complete_chunk_pre13), 29 | ] 30 | } 31 | 32 | #[test] 33 | fn block_returns_same_as_current_java_chunk() { 34 | let chunks = get_test_chunk(); 35 | 36 | for (java_chunk, complete_chunk) in chunks { 37 | for x in 0..16 { 38 | for z in 0..16 { 39 | for y in complete_chunk.y_range() { 40 | let actual = complete_chunk.block(x, y, z).unwrap().name(); 41 | let expected = java_chunk.block(x, y, z).unwrap().name(); 42 | 43 | assert!( 44 | actual.eq(expected), 45 | "Block at {} {} {} was {} but should be {}", 46 | x, 47 | y, 48 | z, 49 | actual, 50 | expected 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | #[test] 59 | fn iter_block_returns_same_as_current_java_chunk() { 60 | let chunks = get_test_chunk(); 61 | 62 | for (java_chunk, complete_chunk) in chunks { 63 | for (index, block) in complete_chunk.iter_blocks().enumerate() { 64 | let x = index % 16; 65 | let z = (index / 16) % 16; 66 | 67 | //- y_range() because chunk does not begin at y = 0 68 | let y = index as isize / (16 * 16) + complete_chunk.y_range().start; 69 | 70 | let actual = block.name(); 71 | let expected = java_chunk.block(x, y, z).unwrap().name(); 72 | 73 | assert!( 74 | actual.eq(expected), 75 | "Block at {} {} {} was {} but should be {}", 76 | x, 77 | y, 78 | z, 79 | actual, 80 | expected 81 | ) 82 | } 83 | } 84 | } 85 | 86 | #[test] 87 | fn biome_returns_same_as_current_java_chunk() { 88 | let chunks = get_test_chunk(); 89 | 90 | for (java_chunk, complete_chunk) in chunks { 91 | for x in 0..16 { 92 | for z in 0..16 { 93 | for y in complete_chunk.y_range() { 94 | assert_eq!( 95 | complete_chunk.biome(x, y, z).unwrap(), 96 | java_chunk.biome(x, y, z).unwrap(), 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | #[test] 105 | fn surface_height_returns_same_as_current_java_chunk() { 106 | let chunks = get_test_chunk(); 107 | 108 | for (java_chunk, complete_chunk) in chunks { 109 | for x in 0..16 { 110 | for z in 0..16 { 111 | assert_eq!( 112 | complete_chunk.surface_height(x, z, HeightMode::Trust), 113 | java_chunk.surface_height(x, z, HeightMode::Trust) 114 | ) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /fastanvil/src/test/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "render")] 2 | use crate::{biome::Biome, Block, Palette, Rgba}; 3 | #[cfg(feature = "render")] 4 | use std::hash::Hash; 5 | #[cfg(feature = "render")] 6 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; 7 | 8 | use fastnbt::{nbt, LongArray, Value}; 9 | 10 | mod region; 11 | mod rogue_chunks; 12 | mod section_data; 13 | mod complete_chunk; 14 | #[cfg(feature = "render")] 15 | mod standard_chunks; 16 | mod unicode_chunk; 17 | 18 | #[test] 19 | fn nbt_macro_use() { 20 | // this checks that the fastnbt macro is accessible from an other crate. 21 | let val = nbt!([L;1,2,3]); 22 | assert_eq!(val, Value::LongArray(LongArray::new(vec![1, 2, 3]))); 23 | } 24 | 25 | /// A palette that colours blocks based on the hash of their full description. 26 | /// Will produce gibberish looking maps but is great for testing rendering isn't 27 | /// changing. 28 | #[cfg(feature = "render")] 29 | pub struct HashPalette; 30 | 31 | #[cfg(feature = "render")] 32 | impl Palette for HashPalette { 33 | fn pick(&self, block: &Block, _: Option) -> Rgba { 34 | // Call methods just to exercise all the code. 35 | block.name(); 36 | block.snowy(); 37 | let hash = calculate_hash(block.encoded_description()); 38 | let bytes = hash.to_be_bytes(); 39 | [bytes[0], bytes[1], bytes[2], 255] 40 | } 41 | } 42 | 43 | #[cfg(feature = "render")] 44 | fn calculate_hash(t: &T) -> u64 { 45 | let mut s = DefaultHasher::new(); 46 | t.hash(&mut s); 47 | s.finish() 48 | } 49 | -------------------------------------------------------------------------------- /fastanvil/src/test/region.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Read, Seek, Write}; 2 | 3 | use crate::{ 4 | ChunkLocation, CompressionScheme::Uncompressed, Error, Region, CHUNK_HEADER_SIZE, 5 | REGION_HEADER_SIZE, SECTOR_SIZE, 6 | }; 7 | 8 | fn new_empty() -> Region>> { 9 | Region::create(Cursor::new(vec![])).unwrap() 10 | } 11 | 12 | fn assert_location(r: &mut Region, x: usize, z: usize, offset: u64, size: u64) 13 | where 14 | S: Read + Write + Seek, 15 | { 16 | let ChunkLocation { 17 | offset: found_offset, 18 | sectors: found_size, 19 | } = r.location(x, z).unwrap().unwrap(); 20 | 21 | assert_eq!(offset, found_offset); 22 | assert_eq!(size, found_size); 23 | } 24 | 25 | fn n_sector_chunk(n: usize) -> Vec { 26 | assert!(n > 0); 27 | vec![0; (n * SECTOR_SIZE) - CHUNK_HEADER_SIZE] 28 | } 29 | 30 | #[test] 31 | fn new_region_should_be_empty() { 32 | let mut r = new_empty(); 33 | 34 | for x in 0..32 { 35 | for z in 0..32 { 36 | let chunk = r.read_chunk(x, z); 37 | assert!(matches!(chunk, Ok(None))) 38 | } 39 | } 40 | } 41 | 42 | #[test] 43 | fn blank_write_chunk() { 44 | let mut r = new_empty(); 45 | r.write_compressed_chunk(0, 0, Uncompressed, &[1, 2, 3]) 46 | .unwrap(); 47 | assert_location(&mut r, 0, 0, 2, 1); 48 | } 49 | 50 | #[test] 51 | fn write_invalid_offset_errors() { 52 | let mut r = new_empty(); 53 | assert!(matches!( 54 | r.write_compressed_chunk(32, 0, Uncompressed, &[1, 2, 3]), 55 | Err(Error::InvalidOffset(..)) 56 | )); 57 | assert!(matches!( 58 | r.write_compressed_chunk(0, 32, Uncompressed, &[1, 2, 3]), 59 | Err(Error::InvalidOffset(..)) 60 | )); 61 | } 62 | 63 | #[test] 64 | fn exact_sector_size_chunk_takes_one_sector() { 65 | let mut r = new_empty(); 66 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(1)) 67 | .unwrap(); 68 | assert_location(&mut r, 0, 0, 2, 1); 69 | } 70 | 71 | #[test] 72 | fn over_one_sector_size_chunk_takes_two_sectors() { 73 | let mut r = new_empty(); 74 | r.write_compressed_chunk( 75 | 0, 76 | 0, 77 | Uncompressed, 78 | &[0; SECTOR_SIZE - CHUNK_HEADER_SIZE + 1], 79 | ) 80 | .unwrap(); 81 | assert_location(&mut r, 0, 0, 2, 2); 82 | } 83 | 84 | #[test] 85 | fn several_sector_chunk_takes_correct_size() { 86 | let mut r = new_empty(); 87 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(5)) 88 | .unwrap(); 89 | assert_location(&mut r, 0, 0, 2, 5); 90 | } 91 | 92 | #[test] 93 | fn oversized_chunk_fails() { 94 | let mut r = new_empty(); 95 | let res = r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(256)); 96 | assert!(matches!(res, Err(Error::ChunkTooLarge))) 97 | } 98 | 99 | #[test] 100 | fn write_several_chunks() { 101 | let mut r = new_empty(); 102 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(2)) 103 | .unwrap(); 104 | r.write_compressed_chunk(0, 1, Uncompressed, &n_sector_chunk(3)) 105 | .unwrap(); 106 | 107 | assert_location(&mut r, 0, 0, 2, 2); 108 | assert_location(&mut r, 0, 1, 4, 3); 109 | } 110 | 111 | #[test] 112 | fn write_and_get_chunk() { 113 | let mut r = new_empty(); 114 | r.write_compressed_chunk(0, 0, Uncompressed, &[1, 2, 3]) 115 | .unwrap(); 116 | let c = r.read_chunk(0, 0).unwrap().unwrap(); 117 | assert_eq!(c, &[1, 2, 3]); 118 | } 119 | 120 | #[test] 121 | fn getting_other_chunks_404s() { 122 | let mut r = new_empty(); 123 | r.write_compressed_chunk(1, 1, Uncompressed, &[1, 2, 3]) 124 | .unwrap(); 125 | assert!(matches!(r.read_chunk(0, 0), Ok(None))); 126 | assert!(matches!(r.read_chunk(1, 0), Ok(None))); 127 | assert!(matches!(r.read_chunk(1, 1), Ok(Some(_)))); 128 | } 129 | 130 | #[test] 131 | fn overwrite_with_smaller_chunk() { 132 | let mut r = new_empty(); 133 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(2)) 134 | .unwrap(); 135 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(1)) 136 | .unwrap(); 137 | 138 | assert_location(&mut r, 0, 0, 2, 1); 139 | } 140 | 141 | #[test] 142 | fn overwrite_with_larger_chunk() { 143 | let mut r = new_empty(); 144 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(2)) 145 | .unwrap(); 146 | 147 | // this chunk will be offset 4 size 1. 148 | r.write_compressed_chunk(0, 1, Uncompressed, &n_sector_chunk(1)) 149 | .unwrap(); 150 | 151 | // overwrite chunk at offset 2 to be 3 large, which would overwrite the 152 | // above chunk if done in-place. 153 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(3)) 154 | .unwrap(); 155 | 156 | // sectors now look like [H,H,??,??,01,00,00,00] 157 | assert_location(&mut r, 0, 0, 5, 3); 158 | } 159 | 160 | #[test] 161 | fn chunk_can_fill_gap_left_by_moved_chunk_after_it() { 162 | let mut r = new_empty(); 163 | // HH000111222---- - starting point, chunks 0,1,2 all 3 sectors 164 | // HH000---2221111 - chunk 1 grows beyond capcacity, moves to end. 165 | // HH0000002221111 - chunk 0 can grow to 6 sectors. 166 | 167 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(3)) 168 | .unwrap(); 169 | r.write_compressed_chunk(0, 1, Uncompressed, &n_sector_chunk(3)) 170 | .unwrap(); 171 | r.write_compressed_chunk(0, 2, Uncompressed, &n_sector_chunk(3)) 172 | .unwrap(); 173 | 174 | // chunk 0,1 gets moved to the end 175 | r.write_compressed_chunk(0, 1, Uncompressed, &n_sector_chunk(4)) 176 | .unwrap(); 177 | 178 | // chunk 0,0 can grow to 6 without moving. 179 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(6)) 180 | .unwrap(); 181 | 182 | // HH0000002221111 183 | assert_location(&mut r, 0, 0, 2, 6); 184 | assert_location(&mut r, 0, 1, 11, 4); 185 | assert_location(&mut r, 0, 2, 8, 3); 186 | } 187 | 188 | #[test] 189 | fn load_from_existing_buffer() { 190 | let mut r = new_empty(); 191 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(1)) 192 | .unwrap(); 193 | r.write_compressed_chunk(0, 1, Uncompressed, &n_sector_chunk(2)) 194 | .unwrap(); 195 | 196 | let buf = r.into_inner().unwrap(); 197 | 198 | // reload the region 199 | let mut r = Region::from_stream(buf).unwrap(); 200 | assert_location(&mut r, 0, 0, 2, 1); 201 | assert_location(&mut r, 0, 1, 3, 2); 202 | } 203 | 204 | #[test] 205 | fn deleted_chunk_doenst_exist() { 206 | let mut r = new_empty(); 207 | 208 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(3)) 209 | .unwrap(); 210 | r.write_compressed_chunk(0, 1, Uncompressed, &n_sector_chunk(3)) 211 | .unwrap(); 212 | r.write_compressed_chunk(0, 2, Uncompressed, &n_sector_chunk(3)) 213 | .unwrap(); 214 | 215 | r.remove_chunk(0, 1).unwrap(); 216 | 217 | assert!(matches!(r.read_chunk(0, 0), Ok(Some(_)))); 218 | assert!(matches!(r.read_chunk(0, 1), Ok(None))); 219 | assert!(matches!(r.read_chunk(0, 2), Ok(Some(_)))); 220 | } 221 | 222 | #[test] 223 | fn deleting_non_existing_chunk_works() { 224 | let mut r = new_empty(); 225 | 226 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(3)) 227 | .unwrap(); 228 | r.write_compressed_chunk(0, 2, Uncompressed, &n_sector_chunk(3)) 229 | .unwrap(); 230 | 231 | r.remove_chunk(0, 1).unwrap(); 232 | 233 | assert!(matches!(r.read_chunk(0, 0), Ok(Some(_)))); 234 | assert!(matches!(r.read_chunk(0, 1), Ok(None))); 235 | assert!(matches!(r.read_chunk(0, 2), Ok(Some(_)))); 236 | } 237 | 238 | #[test] 239 | fn into_inner_rewinds_to_correct_position() { 240 | let mut r = new_empty(); 241 | 242 | r.write_compressed_chunk(0, 0, Uncompressed, &n_sector_chunk(3)) 243 | .unwrap(); 244 | r.write_compressed_chunk(0, 1, Uncompressed, &n_sector_chunk(3)) 245 | .unwrap(); 246 | r.write_compressed_chunk(0, 2, Uncompressed, &n_sector_chunk(3)) 247 | .unwrap(); 248 | 249 | let expected_position = REGION_HEADER_SIZE + SECTOR_SIZE * 3 * 3; 250 | 251 | let inner = r.into_inner().unwrap(); 252 | assert_eq!(inner.position(), expected_position as u64); 253 | } 254 | 255 | #[test] 256 | fn into_inner_rewinds_behind_header_if_empty_region() { 257 | let r = new_empty(); 258 | 259 | let inner = r.into_inner().unwrap(); 260 | assert_eq!(inner.position(), REGION_HEADER_SIZE as u64); 261 | } 262 | 263 | // TODO: Should we always zero out space? Would likely be good for compression. 264 | // TODO: defrag? 265 | 266 | // TODO: Worry about atomicity of underlying buffer? The Read+Write+Seek can't 267 | // really provide us with atomicity, we'd probably need some highly level 268 | // abstraction on top of this providing this. Something that copies a region and 269 | // only write the to copy until done, then atomically moves the file over the 270 | // old region. 271 | -------------------------------------------------------------------------------- /fastanvil/src/test/section_data.rs: -------------------------------------------------------------------------------- 1 | use crate::{min_bits_for_n_states, StatesIter}; 2 | 3 | #[test] 4 | fn iter_zeroes() { 5 | let actual: Vec<_> = StatesIter::new(4, 16, &[0]).collect(); 6 | let expected = [0; 16]; 7 | assert_eq!(expected[..], actual); 8 | } 9 | 10 | #[test] 11 | fn iter_one() { 12 | let actual: Vec<_> = StatesIter::new(4, 16, &[1]).collect(); 13 | let expected = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 14 | assert_eq!(expected[..], actual); 15 | } 16 | 17 | #[test] 18 | fn iter_capped() { 19 | let actual: Vec<_> = StatesIter::new(4, 2, &[1]).collect(); 20 | let expected = [1, 0]; 21 | assert_eq!(expected[..], actual); 22 | } 23 | 24 | #[test] 25 | fn iter_skips_padding() { 26 | let actual: Vec<_> = StatesIter::new(60, 3, &[1, 2, 3]).collect(); 27 | let expected = [1, 2, 3]; 28 | assert_eq!(expected[..], actual); 29 | } 30 | 31 | #[test] 32 | fn min_bits() { 33 | let ideal = |n: usize| (n as f64).log2().ceil() as usize; 34 | 35 | assert_eq!(0, min_bits_for_n_states(1)); 36 | assert_eq!(1, min_bits_for_n_states(2)); 37 | assert_eq!(2, min_bits_for_n_states(3)); 38 | assert_eq!(2, min_bits_for_n_states(4)); 39 | assert_eq!(3, min_bits_for_n_states(5)); 40 | assert_eq!(3, min_bits_for_n_states(5)); 41 | 42 | for i in 1..16 * 16 * 16 { 43 | assert_eq!(ideal(i), min_bits_for_n_states(i)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fastanvil/src/test/unicode_chunk.rs: -------------------------------------------------------------------------------- 1 | use crate::{complete, JavaChunk}; 2 | 3 | const UNICODE_CHUNK: &[u8] = include_bytes!("../../resources/unicode.chunk"); 4 | 5 | #[test] 6 | fn unicode_chunk() { 7 | // This chunk contains unicode that isn't on the basic multilingual plane. 8 | // Characters off this plane are encoded in a modified form of cesu8 which 9 | // is an encoding for unicode. Rust uses utf-8 for strings so there can be 10 | // conflicts if not deserialized properly. 11 | let c = JavaChunk::from_bytes(UNICODE_CHUNK); 12 | assert!(c.is_ok()); 13 | let c = complete::Chunk::from_bytes(UNICODE_CHUNK); 14 | assert!(c.is_ok()); 15 | } 16 | -------------------------------------------------------------------------------- /fastnbt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fastnbt" 3 | description = "Serde deserializer for Minecraft's NBT format" 4 | repository = "https://github.com/owengage/fastnbt" 5 | readme = "README.md" 6 | version = "2.5.0" 7 | authors = ["Owen Gage "] 8 | edition = "2021" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["minecraft", "nbt", "serde"] 11 | categories = ["parser-implementations"] 12 | 13 | [dependencies] 14 | arbitrary = { version = "1", optional = true, features = ["derive"] } 15 | byteorder = "1" 16 | cesu8 = "1.1" 17 | serde = { version = "1", features = ["derive"] } 18 | serde_bytes = "0.11.5" 19 | 20 | [features] 21 | arbitrary1 = ["arbitrary"] 22 | 23 | [dev-dependencies] 24 | flate2 = "1" 25 | serde_json = "1" 26 | -------------------------------------------------------------------------------- /fastnbt/README.md: -------------------------------------------------------------------------------- 1 | # fastnbt crate 2 | 3 | Documentation: [docs.rs](https://docs.rs/crate/fastnbt) 4 | 5 | Fast serde deserializer and serializer for *Minecraft: Java Edition*'s NBT format. 6 | 7 | Zero-copy is supported where possible through `from_bytes`. The 8 | `borrow` module contains more types for avoiding allocations. 9 | 10 | Includes a `Value` type for serializing or deserializing any NBT. `Value` 11 | correctly preserves the exact NBT structure. The `nbt!` macro allows easy 12 | creation of these values. 13 | 14 | To support NBT's arrays, there are dedicated `ByteArray`, `IntArray` and 15 | `LongArray` types. 16 | 17 | [See the documentation](https://docs.rs/crate/fastnbt) for more information. 18 | 19 | ```toml 20 | [dependencies] 21 | fastnbt = "2" 22 | ``` 23 | 24 | `fastnbt` follows Semver, some things that this project does *not* count as a 25 | breaking change are: 26 | 27 | * Minimum Rust version change. Outside of corporate environments this should not 28 | be too difficult, and I don't see much need for NBT in those environments. 29 | * Improving the (de)serializer such that valid NBT that did not (de)serialize, then 30 | (de)serializes. Any of these cases I consider a bug. 31 | * Data format when serializing types from fastnbt/fastanvil to other formats. 32 | Types in fastnbt implement `serde::Serialize` to enable spitting out to other 33 | data formats, but may change structure in future. 34 | 35 | Changes that make `fastnbt` incompatible with WebAssembly *are* considered 36 | breaking changes. 37 | 38 | # Other NBT crates 39 | 40 | There appears to be a few crates that support serde (de)serialization, the main 41 | ones I found were: 42 | 43 | * [`hematite_nbt`](https://github.com/PistonDevelopers/hematite_nbt) 44 | * [`quartz_nbt`](https://github.com/Rusty-Quartz/quartz_nbt) 45 | 46 | There are likely others! There are definitely more without serde support. 47 | 48 | * All these crates support serialization and deserialization with 49 | serde. 50 | * They are not interoperable with each other due to requiring custom handling of 51 | NBT Array types. 52 | * They all handle Minecraft's (actually Java's) specialized Unicode. 53 | * quartz and fastnbt support borrowing from the underlying bytes being deserialized. 54 | * fastnbt's `Value` type can round-trip deserialize-serialize NBT arrays. The 55 | other crates have value types as well, they may also round-trip correctly. 56 | 57 | Honestly, they all seem like good options! -------------------------------------------------------------------------------- /fastnbt/examples/avoid-allocation.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use fastnbt::error::Result; 3 | use fastnbt::{from_bytes, Value}; 4 | use flate2::read::GzDecoder; 5 | use serde::Deserialize; 6 | use std::io::Read; 7 | 8 | // This example show retrieving a players inventory from the palyer.dat file 9 | // found in the world/playerdata directory. 10 | // 11 | // In particular we are avoiding allocating a new string for every inventory 12 | // slot by instead having a &str with a lifetime tied to the input data. 13 | 14 | #[derive(Deserialize, Debug)] 15 | #[serde(rename_all = "PascalCase")] 16 | struct PlayerDat<'a> { 17 | data_version: i32, 18 | 19 | #[serde(borrow)] 20 | inventory: Vec>, 21 | ender_items: Vec>, 22 | } 23 | 24 | #[derive(Deserialize, Debug)] 25 | struct InventorySlot<'a> { 26 | id: &'a str, // We avoid allocating a string here. 27 | tag: Option, // Also get the less structured properties of the object. 28 | 29 | // We need to rename fields a lot. 30 | #[serde(rename = "Count")] 31 | count: i8, 32 | } 33 | 34 | fn main() { 35 | let args: Vec<_> = std::env::args().skip(1).collect(); 36 | let file = std::fs::File::open(args[0].clone()).unwrap(); 37 | 38 | // Player dat files are compressed with GZip. 39 | let mut decoder = GzDecoder::new(file); 40 | let mut data = vec![]; 41 | decoder.read_to_end(&mut data).unwrap(); 42 | 43 | let player: Result = from_bytes(data.as_slice()); 44 | 45 | println!("{:#?}", player); 46 | } 47 | -------------------------------------------------------------------------------- /fastnbt/examples/change-world-spawn-value.rs: -------------------------------------------------------------------------------- 1 | //! This executable takes a path to a level.dat file for a world, and spits out 2 | //! a new level.dat file in the current directory. The data is changed so that 3 | //! the world spawn is set to 0,0. 4 | //! 5 | //! This uses fastnbt::Value which isn't easy to use due to it's dynamic nature. 6 | //! The change-world-spawn example uses an actual LevelDat struct. 7 | 8 | use fastnbt::Value; 9 | use flate2::{read::GzDecoder, write::GzEncoder, Compression}; 10 | use std::io::{Read, Write}; 11 | 12 | fn main() { 13 | let args: Vec<_> = std::env::args_os().collect(); 14 | let file = std::fs::File::open(&args[1]).unwrap(); 15 | let mut decoder = GzDecoder::new(file); 16 | let mut bytes = vec![]; 17 | decoder.read_to_end(&mut bytes).unwrap(); 18 | 19 | let mut leveldat: Value = fastnbt::from_bytes(&bytes).unwrap(); 20 | 21 | match &mut leveldat { 22 | Value::Compound(level) => { 23 | let data = level.get_mut("Data").unwrap(); 24 | match data { 25 | Value::Compound(data) => { 26 | *data.get_mut("SpawnX").unwrap() = Value::Int(0); 27 | *data.get_mut("SpawnY").unwrap() = Value::Int(100); 28 | *data.get_mut("SpawnZ").unwrap() = Value::Int(0); 29 | } 30 | _ => panic!(), 31 | } 32 | } 33 | _ => panic!(), 34 | } 35 | 36 | let new_bytes = fastnbt::to_bytes(&leveldat).unwrap(); 37 | let outfile = std::fs::File::create("level.dat").unwrap(); 38 | let mut encoder = GzEncoder::new(outfile, Compression::fast()); 39 | encoder.write_all(&new_bytes).unwrap(); 40 | } 41 | -------------------------------------------------------------------------------- /fastnbt/examples/change-world-spawn.rs: -------------------------------------------------------------------------------- 1 | //! This executable takes a path to a level.dat file for a world, and spits out 2 | //! a new level.dat file in the current directory. The data is changed so that 3 | //! the world spawn is set to 0,0. 4 | 5 | use fastnbt::Value; 6 | use flate2::{read::GzDecoder, write::GzEncoder, Compression}; 7 | use serde::{Deserialize, Serialize}; 8 | use std::{ 9 | collections::HashMap, 10 | io::{Read, Write}, 11 | }; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | struct LevelDat { 15 | #[serde(rename = "Data")] 16 | data: Data, 17 | } 18 | 19 | #[derive(Serialize, Deserialize)] 20 | #[serde(rename_all = "PascalCase")] 21 | struct Data { 22 | spawn_x: i32, 23 | spawn_y: i32, 24 | spawn_z: i32, 25 | 26 | #[serde(flatten)] 27 | other: HashMap, 28 | } 29 | 30 | fn main() { 31 | let args: Vec<_> = std::env::args_os().collect(); 32 | let file = std::fs::File::open(&args[1]).unwrap(); 33 | let mut decoder = GzDecoder::new(file); 34 | let mut bytes = vec![]; 35 | decoder.read_to_end(&mut bytes).unwrap(); 36 | 37 | let mut leveldat: LevelDat = fastnbt::from_bytes(&bytes).unwrap(); 38 | 39 | leveldat.data.spawn_x = 250; 40 | leveldat.data.spawn_y = 200; 41 | leveldat.data.spawn_z = 250; 42 | 43 | let new_bytes = fastnbt::to_bytes(&leveldat).unwrap(); 44 | let outfile = std::fs::File::create("level.dat").unwrap(); 45 | let mut encoder = GzEncoder::new(outfile, Compression::fast()); 46 | encoder.write_all(&new_bytes).unwrap(); 47 | } 48 | -------------------------------------------------------------------------------- /fastnbt/examples/fastnbt-value.rs: -------------------------------------------------------------------------------- 1 | use fastnbt::Value; 2 | use flate2::read::GzDecoder; 3 | use std::io; 4 | use std::io::Read; 5 | 6 | fn main() { 7 | let stdin = io::stdin(); 8 | let mut decoder = GzDecoder::new(stdin); 9 | let mut buf = vec![]; 10 | decoder.read_to_end(&mut buf).unwrap(); 11 | 12 | let val: Value = fastnbt::from_bytes(buf.as_slice()).unwrap(); 13 | 14 | println!("{:#?}", val); 15 | } 16 | -------------------------------------------------------------------------------- /fastnbt/examples/nbt-dump.rs: -------------------------------------------------------------------------------- 1 | use fastnbt::stream::{Parser, Value}; 2 | use flate2::read::GzDecoder; 3 | use std::io; 4 | 5 | // 6 | // This example uses the streaming parser to simply dump NBT from stdin with GZip compression. 7 | // 8 | 9 | fn main() { 10 | let stdin = io::stdin(); 11 | let decoder = GzDecoder::new(stdin); 12 | 13 | let mut parser = Parser::new(decoder); 14 | let mut indent = 0; 15 | 16 | loop { 17 | match parser.next() { 18 | Err(e) => { 19 | println!("{:?}", e); 20 | break; 21 | } 22 | Ok(value) => { 23 | match value { 24 | Value::CompoundEnd => indent -= 4, 25 | Value::ListEnd => indent -= 4, 26 | _ => {} 27 | } 28 | 29 | println!("{:indent$}{:?}", "", value, indent = indent); 30 | 31 | match value { 32 | Value::Compound(_) => indent += 4, 33 | Value::List(_, _, _) => indent += 4, 34 | _ => {} 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /fastnbt/examples/simple-deserialize.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use fastnbt::error::Result; 3 | use fastnbt::from_bytes; 4 | use flate2::read::GzDecoder; 5 | use serde::Deserialize; 6 | use std::io::Read; 7 | 8 | // This example show retrieving a players inventory from the palyer.dat file 9 | // found in the world/playerdata directory. 10 | 11 | #[derive(Deserialize, Debug)] 12 | #[serde(rename_all = "PascalCase")] 13 | struct PlayerDat { 14 | data_version: i32, 15 | inventory: Vec, 16 | ender_items: Vec, 17 | } 18 | 19 | #[derive(Deserialize, Debug)] 20 | struct InventorySlot { 21 | id: String, 22 | } 23 | 24 | fn main() { 25 | let args: Vec<_> = std::env::args().skip(1).collect(); 26 | let file = std::fs::File::open(args[0].clone()).unwrap(); 27 | 28 | // Player dat files are compressed with GZip. 29 | let mut decoder = GzDecoder::new(file); 30 | let mut data = vec![]; 31 | decoder.read_to_end(&mut data).unwrap(); 32 | 33 | let player: Result = from_bytes(data.as_slice()); 34 | 35 | println!("{:?}", player); 36 | } 37 | -------------------------------------------------------------------------------- /fastnbt/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Contains the Error and Result type used by the deserializer. 2 | use std::fmt::Display; 3 | 4 | /// Various errors that can occur during deserialization. 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct Error(String); 7 | 8 | /// Convenience type for Result. 9 | pub type Result = std::result::Result; 10 | 11 | impl std::error::Error for Error {} 12 | 13 | impl std::fmt::Display for Error { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | f.write_str(&self.0) 16 | } 17 | } 18 | 19 | impl serde::de::Error for Error { 20 | fn custom(msg: T) -> Self { 21 | Error(msg.to_string()) 22 | } 23 | } 24 | 25 | // TODO: Separate error types for ser and de? 26 | impl serde::ser::Error for Error { 27 | fn custom(msg: T) -> Self 28 | where 29 | T: Display, 30 | { 31 | Error(msg.to_string()) 32 | } 33 | } 34 | 35 | impl From for Error { 36 | fn from(e: std::io::Error) -> Self { 37 | Error(format!("io error: {}", e)) 38 | } 39 | } 40 | 41 | impl Error { 42 | pub(crate) fn invalid_tag(tag: u8) -> Error { 43 | Error(format!("invalid nbt tag value: {}", tag)) 44 | } 45 | 46 | pub(crate) fn no_root_compound() -> Error { 47 | Error("invalid nbt: no root compound".to_owned()) 48 | } 49 | 50 | pub(crate) fn nonunicode_string(data: &[u8]) -> Error { 51 | Error(format!( 52 | "invalid nbt string: nonunicode: {}", 53 | String::from_utf8_lossy(data) 54 | )) 55 | } 56 | 57 | pub(crate) fn unexpected_eof() -> Error { 58 | Error("eof: unexpectedly ran out of input".to_owned()) 59 | } 60 | 61 | pub(crate) fn array_as_seq() -> Error { 62 | Error("expected NBT Array, found seq: use ByteArray, IntArray or LongArray types".into()) 63 | } 64 | 65 | pub(crate) fn array_as_other() -> Error { 66 | Error("expected NBT Array: use ByteArray, IntArray or LongArray types".into()) 67 | } 68 | 69 | pub(crate) fn bespoke(msg: String) -> Error { 70 | Error(msg) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /fastnbt/src/ser/array_serializer.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use serde::ser::Impossible; 4 | 5 | use crate::{error::Error, error::Result, Tag}; 6 | 7 | use super::{serializer::Serializer, write_nbt::WriteNbt}; 8 | 9 | /// ArraySerializer is for serializing the NBT Arrays ie ByteArray, IntArray and 10 | /// LongArray. 11 | pub(crate) struct ArraySerializer<'a, W: Write> { 12 | pub(crate) ser: &'a mut Serializer, 13 | pub(crate) tag: Tag, 14 | } 15 | 16 | macro_rules! only_bytes { 17 | ($v:ident($($t:ty),* $(,)?) -> $r:ty) => { 18 | fn $v(self, $(_: $t),*) -> Result<$r> { 19 | Err(Error::array_as_other()) 20 | } 21 | }; 22 | 23 | ($v:ident($($t:ty),* $(,)?)) => { 24 | fn $v(self, $(_: $t),*) -> Result { 25 | Err(Error::array_as_other()) 26 | } 27 | }; 28 | 29 | ($v:ident($($t:ty),* $(,)?)) => { 30 | only_bytes!{$v($($t,)*) -> ()} 31 | }; 32 | } 33 | 34 | impl<'a, W: Write> serde::Serializer for ArraySerializer<'a, W> { 35 | type Ok = (); 36 | type Error = Error; 37 | type SerializeSeq = Impossible<(), Error>; 38 | type SerializeTuple = Impossible<(), Error>; 39 | type SerializeTupleStruct = Impossible<(), Error>; 40 | type SerializeTupleVariant = Impossible<(), Error>; 41 | type SerializeMap = Impossible<(), Error>; 42 | type SerializeStruct = Impossible<(), Error>; 43 | type SerializeStructVariant = Impossible<(), Error>; 44 | 45 | fn serialize_bytes(self, v: &[u8]) -> Result { 46 | let stride = match self.tag { 47 | Tag::ByteArray => 1, 48 | Tag::IntArray => 4, 49 | Tag::LongArray => 8, 50 | _ => panic!(), 51 | }; 52 | let len = v.len() / stride; 53 | self.ser.writer.write_len(len)?; 54 | self.ser.writer.write_all(v)?; 55 | Ok(()) 56 | } 57 | 58 | only_bytes!(serialize_bool(bool)); 59 | only_bytes!(serialize_i8(i8)); 60 | only_bytes!(serialize_i16(i16)); 61 | only_bytes!(serialize_i32(i32)); 62 | only_bytes!(serialize_i64(i64)); 63 | only_bytes!(serialize_i128(i128)); 64 | only_bytes!(serialize_u8(u8)); 65 | only_bytes!(serialize_u16(u16)); 66 | only_bytes!(serialize_u32(u32)); 67 | only_bytes!(serialize_u64(u64)); 68 | only_bytes!(serialize_u128(u128)); 69 | only_bytes!(serialize_f32(f32)); 70 | only_bytes!(serialize_f64(f64)); 71 | only_bytes!(serialize_char(char)); 72 | only_bytes!(serialize_str(&str)); 73 | only_bytes!(serialize_none()); 74 | only_bytes!(serialize_some(&T)); 75 | only_bytes!(serialize_unit()); 76 | only_bytes!(serialize_unit_struct(&'static str)); 77 | only_bytes!(serialize_unit_variant(&'static str, u32, &'static str)); 78 | only_bytes!(serialize_newtype_struct(&'static str, &T)); 79 | only_bytes!(serialize_newtype_variant(&'static str, u32, &'static str, &T)); 80 | only_bytes!(serialize_seq(Option) -> Self::SerializeSeq); 81 | only_bytes!(serialize_tuple(usize) -> Self::SerializeSeq); 82 | only_bytes!(serialize_map(Option) -> Self::SerializeMap); 83 | only_bytes!(serialize_tuple_struct(&'static str, usize) -> Self::SerializeTupleStruct); 84 | only_bytes!(serialize_struct(&'static str, usize) -> Self::SerializeStruct); 85 | 86 | only_bytes!( 87 | serialize_tuple_variant( 88 | &'static str, 89 | u32, 90 | &'static str, 91 | usize, 92 | ) -> Self::SerializeTupleVariant 93 | ); 94 | 95 | only_bytes!( 96 | serialize_struct_variant( 97 | &'static str, 98 | u32, 99 | &'static str, 100 | usize, 101 | ) -> Self::SerializeStructVariant 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /fastnbt/src/ser/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a serde serializer for NBT data. This should be able to 2 | //! serialize most structures to NBT. Use [`to_bytes`][`crate::to_bytes`] or 3 | //! [`to_writer`][`crate::to_writer`]. There are 'with opts' functions for more 4 | //! serialization control. 5 | //! 6 | //! Some Rust structures have no sensible mapping to NBT data. These cases will 7 | //! result in an error (not a panic). If you find a case where you think there 8 | //! is a valid way to serialize it, please open an issue. 9 | //! 10 | //! The examples directory contains some examples. The [`de`][`crate::de`] 11 | //! module contains more information about (de)serialization. 12 | //! 13 | //! # 128 bit integers and UUIDs 14 | //! 15 | //! UUIDs tend to be stored in NBT using 4-long IntArrays. When serializing 16 | //! `i128` or `u128`, an IntArray of length 4 will be produced. This is stored 17 | //! as big endian i.e. the most significant bit (and int) is first. 18 | //! 19 | //! # Root compound name 20 | //! 21 | //! A valid NBT compound must have a name, including the root compound. For most 22 | //! Minecraft data this is simply the empty string. If you need to control the 23 | //! name of this root compound you can use 24 | //! [`to_bytes_with_opts`][`crate::to_bytes_with_opts`] and 25 | //! [`to_writer_with_opts`][`crate::to_writer_with_opts`]. For example the 26 | //! [unofficial schematic 27 | //! format](https://minecraft.wiki/w/Schematic_file_format): 28 | //! 29 | //! ```no_run 30 | //! use serde::{Serialize, Deserialize}; 31 | //! use fastnbt::{Value, ByteArray, SerOpts}; 32 | //! 33 | //! #[derive(Serialize, Deserialize, Debug)] 34 | //! #[serde(rename_all = "PascalCase")] 35 | //! pub struct Schematic { 36 | //! blocks: ByteArray, 37 | //! data: ByteArray, 38 | //! tile_entities: Vec, 39 | //! entities: Vec, 40 | //! width: i16, 41 | //! height: i16, 42 | //! length: i16, 43 | //! materials: String, 44 | //! } 45 | //! 46 | //! let structure = todo!(); // make schematic 47 | //! let bytes = fastnbt::to_bytes_with_opts(&structure, SerOpts::new().root_name("Schematic")).unwrap(); 48 | //! ``` 49 | mod array_serializer; 50 | mod name_serializer; 51 | mod serializer; 52 | mod write_nbt; 53 | 54 | pub use serializer::*; 55 | -------------------------------------------------------------------------------- /fastnbt/src/ser/name_serializer.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use serde::{ser::Impossible, Serializer}; 4 | 5 | use crate::error::{Error, Result}; 6 | 7 | pub(crate) struct NameSerializer { 8 | pub(crate) name: W, 9 | } 10 | 11 | macro_rules! bespoke_error { 12 | ($name:literal) => { 13 | Err(Error::bespoke(format!( 14 | "field must be string-like, found {}", 15 | $name 16 | ))) 17 | }; 18 | } 19 | 20 | macro_rules! must_be_stringy { 21 | ($name:literal: $ser:ident($($t:ty),*) -> $res:ty) => { 22 | fn $ser(self, $(_: $t),*) -> Result<$res> { 23 | bespoke_error!($name) 24 | } 25 | }; 26 | 27 | ($name:literal: $ser:ident($($t:ty),*) -> $res:ty) => { 28 | fn $ser(self, $(_: $t),*) -> Result<$res> { 29 | bespoke_error!($name) 30 | } 31 | }; 32 | 33 | ($name:literal: $ser:ident($($t:ty),*)) => { 34 | must_be_stringy!($name: $ser($($t),*) -> ()); 35 | }; 36 | 37 | ($name:literal: $ser:ident($($t:ty),*)) => { 38 | must_be_stringy!($name: $ser($($t),*) -> ()); 39 | }; 40 | } 41 | 42 | /// NameSerializer is all about serializing the name of a field. It does not 43 | /// write the length or the tag. We typically need to write this to a different 44 | /// buffer than the main one we're writing to, because we need to write out the 45 | /// field in tag, name, value order. In order the write the tag we need to know 46 | /// what value we're serializing, which we can only do when we serialize the 47 | /// value. So we save the name start serializing the value, which serializes the 48 | /// tag, then the saved name, then the value. 49 | impl Serializer for &mut NameSerializer { 50 | type Ok = (); 51 | type Error = Error; 52 | type SerializeSeq = Impossible<(), Error>; 53 | type SerializeTuple = Impossible<(), Error>; 54 | type SerializeTupleStruct = Impossible<(), Error>; 55 | type SerializeTupleVariant = Impossible<(), Error>; 56 | type SerializeMap = Impossible<(), Error>; 57 | type SerializeStruct = Impossible<(), Error>; 58 | type SerializeStructVariant = Impossible<(), Error>; 59 | 60 | fn serialize_bytes(self, v: &[u8]) -> Result { 61 | self.name.write_all(v)?; 62 | Ok(()) 63 | } 64 | 65 | fn serialize_char(self, c: char) -> Result { 66 | self.name.write_all(&cesu8::to_java_cesu8(&c.to_string()))?; 67 | Ok(()) 68 | } 69 | 70 | fn serialize_str(self, v: &str) -> Result { 71 | self.name.write_all(&cesu8::to_java_cesu8(v))?; 72 | Ok(()) 73 | } 74 | 75 | must_be_stringy!("bool": serialize_bool(bool)); 76 | must_be_stringy!("i8": serialize_i8(i8)); 77 | must_be_stringy!("i16": serialize_i16(i16)); 78 | must_be_stringy!("i32": serialize_i32(i32)); 79 | must_be_stringy!("i64": serialize_i64(i64)); 80 | must_be_stringy!("u8": serialize_u8(u8)); 81 | must_be_stringy!("u16": serialize_u16(u16)); 82 | must_be_stringy!("u32": serialize_u32(u32)); 83 | must_be_stringy!("u64": serialize_u64(u64)); 84 | must_be_stringy!("f32": serialize_f32(f32)); 85 | must_be_stringy!("f64": serialize_f64(f64)); 86 | must_be_stringy!("none": serialize_none()); 87 | must_be_stringy!("some": serialize_some(&T)); 88 | must_be_stringy!("unit": serialize_unit()); 89 | must_be_stringy!("unit_struct": serialize_unit_struct(&'static str)); 90 | must_be_stringy!("unit_variant": serialize_unit_variant(&'static str, u32, &'static str)); 91 | must_be_stringy!("newtype_struct": serialize_newtype_struct(&'static str, &T)); 92 | must_be_stringy!("newtype_variant": serialize_newtype_variant(&'static str, u32, &'static str, &T)); 93 | must_be_stringy!("seq": serialize_seq(Option) -> Self::SerializeSeq); 94 | must_be_stringy!("tuple": serialize_tuple(usize) -> Self::SerializeTuple); 95 | must_be_stringy!("tuple_struct": serialize_tuple_struct(&'static str, usize) -> Self::SerializeTupleStruct); 96 | must_be_stringy!("tuple_variant": serialize_tuple_variant(&'static str, u32, &'static str, usize) -> Self::SerializeTupleVariant); 97 | must_be_stringy!("map": serialize_map(Option) -> Self::SerializeMap); 98 | must_be_stringy!("struct": serialize_struct(&'static str, usize) -> Self::SerializeStruct); 99 | must_be_stringy!("struct_variant": serialize_struct_variant(&'static str, u32, &'static str, usize) -> Self::SerializeStructVariant); 100 | } 101 | -------------------------------------------------------------------------------- /fastnbt/src/ser/write_nbt.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::io::Write; 3 | 4 | use byteorder::{BigEndian, WriteBytesExt}; 5 | 6 | use crate::error::{Error, Result}; 7 | use crate::Tag; 8 | 9 | pub(crate) trait WriteNbt: Write { 10 | fn write_tag(&mut self, tag: Tag) -> Result<()> { 11 | self.write_u8(tag as u8)?; 12 | Ok(()) 13 | } 14 | 15 | fn write_size_prefixed_str(&mut self, key: &str) -> Result<()> { 16 | let key = cesu8::to_java_cesu8(key); 17 | let len_bytes = key.len() as u16; 18 | self.write_u16::(len_bytes)?; 19 | self.write_all(&key)?; 20 | Ok(()) 21 | } 22 | 23 | fn write_len(&mut self, len: usize) -> Result<()> { 24 | self.write_u32::( 25 | len.try_into() 26 | .map_err(|_| Error::bespoke("len too large".to_owned()))?, 27 | )?; 28 | 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl WriteNbt for T where T: Write {} 34 | -------------------------------------------------------------------------------- /fastnbt/src/test/builder.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use super::super::*; 4 | 5 | /// Builder for NBT data. This is to create test data. It specifically does 6 | /// *not* guarantee the resulting data is valid NBT. Creating invalid NBT is 7 | /// useful for testing. 8 | pub struct Builder { 9 | payload: Vec, 10 | } 11 | 12 | impl Builder { 13 | pub fn new() -> Self { 14 | Builder { 15 | payload: Vec::new(), 16 | } 17 | } 18 | 19 | pub fn tag(mut self, t: Tag) -> Self { 20 | self.payload.push(t as u8); 21 | self 22 | } 23 | 24 | pub fn name(mut self, name: &str) -> Self { 25 | let name = cesu8::to_java_cesu8(name); 26 | let len_bytes = &(name.len() as u16).to_be_bytes()[..]; 27 | self.payload.extend_from_slice(len_bytes); 28 | self.payload.extend_from_slice(&name); 29 | self 30 | } 31 | 32 | pub fn start_compound(self, name: &str) -> Self { 33 | self.tag(Tag::Compound).name(name) 34 | } 35 | 36 | pub fn end_compound(self) -> Self { 37 | self.tag(Tag::End) 38 | } 39 | 40 | pub fn end_anon_compound(self) -> Self { 41 | self.tag(Tag::End) 42 | } 43 | 44 | pub fn start_list(self, name: &str, element_tag: Tag, size: i32) -> Self { 45 | self.tag(Tag::List) 46 | .name(name) 47 | .tag(element_tag) 48 | .int_payload(size) 49 | } 50 | 51 | pub fn start_anon_list(self, element_tag: Tag, size: i32) -> Self { 52 | self.tag(element_tag).int_payload(size) 53 | } 54 | 55 | pub fn byte(self, name: &str, b: i8) -> Self { 56 | self.tag(Tag::Byte).name(name).byte_payload(b) 57 | } 58 | 59 | pub fn short(self, name: &str, b: i16) -> Self { 60 | self.tag(Tag::Short).name(name).short_payload(b) 61 | } 62 | 63 | pub fn int(self, name: &str, b: i32) -> Self { 64 | self.tag(Tag::Int).name(name).int_payload(b) 65 | } 66 | 67 | pub fn long(self, name: &str, b: i64) -> Self { 68 | self.tag(Tag::Long).name(name).long_payload(b) 69 | } 70 | 71 | pub fn string(self, name: &str, s: &str) -> Self { 72 | self.tag(Tag::String).name(name).string_payload(s) 73 | } 74 | 75 | pub fn float(self, name: &str, n: f32) -> Self { 76 | self.tag(Tag::Float).name(name).float_payload(n) 77 | } 78 | 79 | pub fn double(self, name: &str, n: f64) -> Self { 80 | self.tag(Tag::Double).name(name).double_payload(n) 81 | } 82 | 83 | pub fn byte_array(self, name: &str, bs: &[i8]) -> Self { 84 | self.tag(Tag::ByteArray) 85 | .name(name) 86 | .int_payload(bs.len().try_into().unwrap()) 87 | .byte_array_payload(bs) 88 | } 89 | 90 | pub fn int_array(self, name: &str, arr: &[i32]) -> Self { 91 | self.tag(Tag::IntArray) 92 | .name(name) 93 | .int_payload(arr.len().try_into().unwrap()) 94 | .int_array_payload(arr) 95 | } 96 | 97 | pub fn long_array(self, name: &str, arr: &[i64]) -> Self { 98 | self.tag(Tag::LongArray) 99 | .name(name) 100 | .int_payload(arr.len().try_into().unwrap()) 101 | .long_array_payload(arr) 102 | } 103 | 104 | pub fn string_payload(self, s: &str) -> Self { 105 | self.name(s) 106 | } 107 | 108 | pub fn byte_payload(mut self, b: i8) -> Self { 109 | self.payload.push(b as u8); 110 | self 111 | } 112 | 113 | pub fn byte_array_payload(mut self, bs: &[i8]) -> Self { 114 | for b in bs { 115 | self.payload.push(*b as u8); 116 | } 117 | self 118 | } 119 | 120 | pub fn short_payload(mut self, i: i16) -> Self { 121 | self.payload.extend_from_slice(&i.to_be_bytes()[..]); 122 | self 123 | } 124 | 125 | pub fn int_payload(mut self, i: i32) -> Self { 126 | self.payload.extend_from_slice(&i.to_be_bytes()[..]); 127 | self 128 | } 129 | 130 | pub fn int_array_payload(mut self, is: &[i32]) -> Self { 131 | for i in is { 132 | self = self.int_payload(*i); 133 | } 134 | self 135 | } 136 | 137 | pub fn long_payload(mut self, i: i64) -> Self { 138 | self.payload.extend_from_slice(&i.to_be_bytes()[..]); 139 | self 140 | } 141 | 142 | pub fn long_array_payload(mut self, is: &[i64]) -> Self { 143 | for i in is { 144 | self = self.long_payload(*i); 145 | } 146 | self 147 | } 148 | 149 | pub fn float_payload(mut self, f: f32) -> Self { 150 | self.payload.extend_from_slice(&f.to_be_bytes()[..]); 151 | self 152 | } 153 | 154 | pub fn double_payload(mut self, f: f64) -> Self { 155 | self.payload.extend_from_slice(&f.to_be_bytes()[..]); 156 | self 157 | } 158 | 159 | pub fn raw_str_len(mut self, len: usize) -> Self { 160 | let len: u16 = len.try_into().expect("test given length beyond u16"); 161 | let len_bytes = &len.to_be_bytes(); 162 | self.payload.extend_from_slice(len_bytes); 163 | self 164 | } 165 | /// Straight up add some bytes to the payload. For very corner-case tests 166 | /// that are not worth a specific builder method. 167 | pub fn raw_bytes(mut self, bs: &[u8]) -> Self { 168 | for b in bs { 169 | self.payload.push(*b); 170 | } 171 | self 172 | } 173 | 174 | /// This is a no-op, but can make code clearer by showing the points where a 175 | /// compound in a list has logically started. 176 | pub fn start_anon_compound(self) -> Self { 177 | self 178 | } 179 | 180 | pub fn build(self) -> Vec { 181 | self.payload 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /fastnbt/src/test/fuzz.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, iter::FromIterator}; 2 | 3 | use crate::{error::Result, from_bytes, test::builder::Builder, Tag, Value}; 4 | 5 | /// Bugs found via cargo-fuzz. 6 | 7 | #[test] 8 | fn partial_input_causes_panic_if_in_string() { 9 | let input = Builder::new().start_compound("some long name").build(); 10 | let v: Result = from_bytes(&input[0..3]); 11 | assert!(v.is_err()); 12 | } 13 | 14 | #[test] 15 | fn list_of_end() { 16 | let input = Builder::new() 17 | .start_compound("") 18 | .start_list("", Tag::End, 1) 19 | .tag(Tag::End) 20 | .end_compound() 21 | .build(); 22 | 23 | let v: Result = from_bytes(&input); 24 | assert!(v.is_err()); 25 | } 26 | 27 | #[test] 28 | fn float_double() { 29 | // C name f name ............ end compound 30 | let input = [10, 0, 0, 5, 0, 0, 0, 0, 0, 10, 0]; 31 | let v: Value = from_bytes(&input).unwrap(); 32 | let expected = Value::Compound(HashMap::from_iter([( 33 | "".to_string(), 34 | Value::Float(1.4e-44), 35 | )])); 36 | 37 | assert_eq!(expected, v); 38 | } 39 | 40 | #[test] 41 | fn overflow_len() { 42 | // This was a overflow caused by a multiply. 43 | let data = &[ 44 | 10, 0, 0, 12, 0, 19, 95, 95, 102, 97, 115, 116, 110, 98, 116, 95, 105, 110, 116, 95, 97, 45 | 114, 114, 97, 121, 254, 255, 255, 241, 46 | ]; 47 | assert!(from_bytes::(data).is_err()); 48 | } 49 | 50 | #[test] 51 | fn compound_using_nbt_token_key() { 52 | // Payload is a compound that has a key containing one of our NBT token 53 | // keys. We could in theory deserialize this, but instead we're going to 54 | // make the deserializer detect this and error. 55 | let data = &[ 56 | 10, 0, 0, 10, 0, 0, 10, 0, 0, 0, 10, 0, 0, 10, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 9, 0, 19, 57 | 95, 95, 102, 97, 115, 116, 110, 98, 116, 95, 105, 110, 116, 95, 97, 114, 114, 97, 121, 2, 58 | 0, 0, 0, 0, 9, 0, 10, 0, 110, 110, 98, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 59 | 0, 0, 10, 0, 0, 10, 0, 0, 9, 0, 10, 0, 110, 60 | ]; 61 | assert!(from_bytes::(data).is_err()); 62 | } 63 | -------------------------------------------------------------------------------- /fastnbt/src/test/macros.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ByteArray, IntArray, LongArray, Value}; 4 | 5 | #[test] 6 | fn nbt() { 7 | assert_eq!(nbt!(1_i8), Value::Byte(1)); 8 | assert_eq!(nbt!(1_u8), Value::Byte(1)); 9 | assert_eq!(nbt!(1_i16), Value::Short(1)); 10 | assert_eq!(nbt!(1_u16), Value::Short(1)); 11 | assert_eq!(nbt!(1), Value::Int(1)); 12 | assert_eq!(nbt!(1_u32), Value::Int(1)); 13 | assert_eq!(nbt!(1_i64), Value::Long(1)); 14 | assert_eq!(nbt!(1_u64), Value::Long(1)); 15 | assert_eq!(nbt!(1_f32), Value::Float(1.0)); 16 | assert_eq!(nbt!(1.0), Value::Double(1.0)); 17 | assert_eq!(nbt!(true), Value::Byte(1)); 18 | assert_eq!(nbt!(false), Value::Byte(0)); 19 | 20 | assert_eq!(nbt!("string"), Value::String("string".to_owned())); 21 | assert_eq!( 22 | nbt!("string".to_owned()), 23 | Value::String("string".to_owned()) 24 | ); 25 | 26 | assert_eq!(nbt!([]), Value::List(vec![])); 27 | assert_eq!( 28 | nbt!([1, 3]), 29 | Value::List(vec![Value::Int(1), Value::Int(3)]) 30 | ); 31 | assert_eq!( 32 | nbt!([ 33 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 34 | "Duis mattis massa metus, vel consequat lacus tincidunt ut.", 35 | "Nam in lobortis quam, vel vehicula magna.", 36 | "Cras massa turpis, facilisis non volutpat vitae, elementum.", 37 | ]), 38 | Value::List(vec![ 39 | Value::String("Lorem ipsum dolor sit amet, consectetur adipiscing elit.".to_owned()), 40 | Value::String("Duis mattis massa metus, vel consequat lacus tincidunt ut.".to_owned()), 41 | Value::String("Nam in lobortis quam, vel vehicula magna.".to_owned()), 42 | Value::String("Cras massa turpis, facilisis non volutpat vitae, elementum.".to_owned()), 43 | ]) 44 | ); 45 | 46 | assert_eq!(nbt!({}), Value::Compound(HashMap::new())); 47 | assert_eq!( 48 | nbt!({ "key": "value" }), 49 | Value::Compound(HashMap::from([( 50 | "key".to_owned(), 51 | Value::String("value".to_owned()) 52 | ),])) 53 | ); 54 | assert_eq!( 55 | nbt!({ 56 | "key1": "value1", 57 | "key2": 42, 58 | "key3": [4, 2], 59 | }), 60 | Value::Compound(HashMap::from([ 61 | ("key1".to_owned(), Value::String("value1".to_owned())), 62 | ("key2".to_owned(), Value::Int(42)), 63 | ( 64 | "key3".to_owned(), 65 | Value::List(vec![Value::Int(4), Value::Int(2)]) 66 | ), 67 | ])) 68 | ); 69 | 70 | assert_eq!(nbt!([B;]), Value::ByteArray(ByteArray::new(vec![]))); 71 | assert_eq!(nbt!([I;]), Value::IntArray(IntArray::new(vec![]))); 72 | assert_eq!(nbt!([L;]), Value::LongArray(LongArray::new(vec![]))); 73 | assert_eq!( 74 | nbt!([B; 1, 2, 3]), 75 | Value::ByteArray(ByteArray::new(vec![1, 2, 3])) 76 | ); 77 | assert_eq!( 78 | nbt!([I;1,2,3]), 79 | Value::IntArray(IntArray::new(vec![1, 2, 3])) 80 | ); 81 | assert_eq!( 82 | nbt!([L; 1, 2, 3,]), 83 | Value::LongArray(LongArray::new(vec![1, 2, 3])) 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /fastnbt/src/test/minecraft_chunk.rs: -------------------------------------------------------------------------------- 1 | use super::resources::{CHUNK_RAW, CHUNK_RAW_WITH_ENTITIES}; 2 | use crate::{from_bytes, Value}; 3 | use serde::Deserialize; 4 | 5 | #[test] 6 | fn unit_variant_enum_for_chunk_status() { 7 | // From https://minecraft.wiki/w/Chunk_format 8 | // 9 | // Status: Defines the world generation status of this chunk. It is always 10 | // one of the following: empty, structure_starts, structure_references, 11 | // biomes, noise, surface, carvers, liquid_carvers, features, light, spawn, 12 | // or heightmaps.[1] 13 | #[derive(Deserialize)] 14 | struct Chunk { 15 | #[serde(rename = "Level")] 16 | level: Level, 17 | } 18 | 19 | #[derive(Deserialize)] 20 | struct Level { 21 | #[serde(rename = "Status")] 22 | status: Status, 23 | } 24 | 25 | #[derive(Deserialize, PartialEq, Debug)] 26 | #[serde(rename_all = "snake_case")] 27 | enum Status { 28 | Empty, 29 | StructureStarts, 30 | StructureReferences, 31 | Biomes, 32 | Noise, 33 | Surface, 34 | Carvers, 35 | LiquidCarvers, 36 | Features, 37 | Light, 38 | Spawn, 39 | Heightmaps, 40 | Full, 41 | } 42 | 43 | let chunk: Chunk = from_bytes(CHUNK_RAW).unwrap(); 44 | assert_eq!(Status::Full, chunk.level.status) 45 | } 46 | #[test] 47 | fn chunk_to_value() { 48 | from_bytes::(CHUNK_RAW).unwrap(); 49 | } 50 | 51 | #[test] 52 | fn tile_entities() { 53 | #[derive(Deserialize)] 54 | struct Chunk { 55 | #[serde(rename = "Level")] 56 | level: Level, 57 | } 58 | 59 | #[derive(Deserialize)] 60 | struct Level { 61 | #[serde(rename = "Entities")] 62 | entities: Vec, 63 | } 64 | 65 | #[derive(Deserialize, Debug)] 66 | #[serde(untagged)] 67 | enum Entity { 68 | Known(KnownEntity), 69 | Unknown(Value), 70 | } 71 | 72 | #[derive(Deserialize, Debug)] 73 | #[serde(tag = "id")] 74 | enum KnownEntity { 75 | #[serde(rename = "minecraft:bat")] 76 | Bat { 77 | #[allow(dead_code)] 78 | #[serde(rename = "BatFlags")] 79 | bat_flags: i8, 80 | }, 81 | 82 | #[serde(rename = "minecraft:creeper")] 83 | Creeper { 84 | #[allow(dead_code)] 85 | ignited: i8, 86 | }, 87 | } 88 | 89 | let chunk: Chunk = from_bytes(CHUNK_RAW_WITH_ENTITIES).unwrap(); 90 | let entities = chunk.level.entities; 91 | 92 | println!("{:#?}", entities); 93 | } 94 | 95 | #[test] 96 | fn avoiding_alloc_with_chunk() { 97 | #[derive(Deserialize)] 98 | struct Chunk<'a> { 99 | #[serde(rename = "Level")] 100 | #[serde(borrow)] 101 | _level: Level<'a>, 102 | } 103 | 104 | #[derive(Deserialize)] 105 | struct Level<'a> { 106 | #[serde(rename = "Sections")] 107 | #[serde(borrow)] 108 | pub _sections: Option>>, 109 | } 110 | 111 | #[derive(Deserialize, Debug)] 112 | #[serde(rename_all = "PascalCase")] 113 | pub struct Section<'a> { 114 | #[serde(borrow)] 115 | pub block_states: Option<&'a [u8]>, 116 | } 117 | 118 | let _chunk: Chunk = from_bytes(CHUNK_RAW).unwrap(); 119 | } 120 | -------------------------------------------------------------------------------- /fastnbt/src/test/mod.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::Tag; 6 | 7 | #[allow(clippy::float_cmp)] 8 | mod de; 9 | 10 | #[allow(clippy::float_cmp)] 11 | mod value; 12 | 13 | pub mod builder; 14 | mod fuzz; 15 | mod macros; 16 | mod minecraft_chunk; 17 | mod resources; 18 | mod ser; 19 | mod stream; 20 | 21 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 22 | struct Single { 23 | val: T, 24 | } 25 | 26 | #[derive(Serialize, Deserialize)] 27 | struct Wrap(T); 28 | 29 | macro_rules! check_tags { 30 | {$($tag:ident = $val:literal),* $(,)?} => { 31 | $( 32 | assert_eq!(u8::from(Tag::$tag), $val); 33 | )* 34 | }; 35 | } 36 | 37 | #[test] 38 | fn exhaustive_tag_check() { 39 | check_tags! { 40 | End = 0, 41 | Byte = 1, 42 | Short = 2, 43 | Int = 3, 44 | Long = 4, 45 | Float = 5, 46 | Double = 6, 47 | ByteArray = 7, 48 | String = 8, 49 | List = 9, 50 | Compound = 10, 51 | IntArray = 11, 52 | LongArray = 12, 53 | } 54 | 55 | for value in 13..=u8::MAX { 56 | assert!(Tag::try_from(value).is_err()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /fastnbt/src/test/resources/chunk.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastnbt/src/test/resources/chunk.nbt -------------------------------------------------------------------------------- /fastnbt/src/test/resources/chunk1.14.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/fastnbt/src/test/resources/chunk1.14.nbt -------------------------------------------------------------------------------- /fastnbt/src/test/resources/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const CHUNK_RAW: &[u8] = include_bytes!("chunk.nbt"); 2 | pub(crate) const CHUNK_RAW_WITH_ENTITIES: &[u8] = include_bytes!("chunk1.14.nbt"); 3 | -------------------------------------------------------------------------------- /fastnbt/src/test/value/de.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::{value::from_value, ByteArray, IntArray, LongArray}; 6 | 7 | #[test] 8 | fn simple_types() { 9 | #[derive(Deserialize, PartialEq, Debug)] 10 | struct V<'a> { 11 | bool: bool, 12 | i8: i8, 13 | i16: i16, 14 | i32: i32, 15 | i64: i64, 16 | u8: u8, 17 | u16: u16, 18 | u32: u32, 19 | u64: u64, 20 | f32: f32, 21 | f64: f64, 22 | char: char, 23 | str: Cow<'a, str>, 24 | string: String, 25 | } 26 | 27 | let val: V = from_value(&nbt!({ 28 | "bool": true, 29 | "i8": i8::MAX, 30 | "i16": i16::MAX, 31 | "i32": i32::MAX, 32 | "i64": i64::MAX, 33 | "u8": u8::MAX, 34 | "u16": u16::MAX, 35 | "u32": u32::MAX, 36 | "u64": u64::MAX, 37 | "f32": f32::MAX, 38 | "f64": f64::MAX, 39 | "char": 'n', 40 | "str": "str", 41 | "string": "string", 42 | })) 43 | .unwrap(); 44 | 45 | let expected = V { 46 | bool: true, 47 | i8: i8::MAX, 48 | i16: i16::MAX, 49 | i32: i32::MAX, 50 | i64: i64::MAX, 51 | u8: u8::MAX, 52 | u16: u16::MAX, 53 | u32: u32::MAX, 54 | u64: u64::MAX, 55 | f32: f32::MAX, 56 | f64: f64::MAX, 57 | char: 'n', 58 | str: "str".into(), 59 | string: "string".to_string(), 60 | }; 61 | 62 | assert_eq!(expected, val); 63 | // A borrowed string cannot be deserialized from Value 64 | assert!(matches!(val.str, Cow::Owned(_))); 65 | } 66 | 67 | #[test] 68 | fn int_array_types() { 69 | #[derive(Deserialize, PartialEq, Debug)] 70 | struct V { 71 | i128: i128, 72 | u128: u128, 73 | bytes: ByteArray, 74 | ints: IntArray, 75 | longs: LongArray, 76 | } 77 | 78 | let val = from_value(&nbt!({ 79 | "i128": i128::MAX, 80 | "u128": u128::MAX, 81 | "bytes": [B; 1, 2, 3, 4, 5], 82 | "ints": [I; 1, 2, 3, 4, 5], 83 | "longs": [L; 1, 2, 3, 4, 5], 84 | })) 85 | .unwrap(); 86 | 87 | let expected = V { 88 | i128: i128::MAX, 89 | u128: u128::MAX, 90 | bytes: ByteArray::new(vec![1, 2, 3, 4, 5]), 91 | ints: IntArray::new(vec![1, 2, 3, 4, 5]), 92 | longs: LongArray::new(vec![1, 2, 3, 4, 5]), 93 | }; 94 | 95 | assert_eq!(expected, val); 96 | } 97 | 98 | #[test] 99 | fn nested() { 100 | #[derive(Deserialize, PartialEq, Debug)] 101 | struct V { 102 | list: Vec, 103 | nested: Inner, 104 | } 105 | 106 | #[derive(Deserialize, PartialEq, Debug)] 107 | struct Inner { 108 | key: u8, 109 | } 110 | 111 | let val = from_value(&nbt!({ 112 | "list": [1_i16, 2_i16], 113 | "nested": { 114 | "key": 42_u8, 115 | }, 116 | })) 117 | .unwrap(); 118 | 119 | let expected = V { 120 | list: vec![1, 2], 121 | nested: Inner { key: 42 }, 122 | }; 123 | 124 | assert_eq!(expected, val); 125 | } 126 | 127 | #[test] 128 | fn no_root_compound() { 129 | assert_eq!(Ok(128_u8), from_value(&nbt!(128_u8))); 130 | assert_eq!(Ok('a'), from_value(&nbt!('a'))); 131 | assert_eq!(Ok("string".to_string()), from_value(&nbt!("string"))); 132 | assert_eq!(Ok(u128::MAX), from_value(&nbt!(u128::MAX))); 133 | assert_eq!( 134 | Ok(ByteArray::new(vec![1, 2, 3])), 135 | from_value(&nbt!([B; 1, 2, 3])) 136 | ); 137 | assert_eq!( 138 | Ok(IntArray::new(vec![1, 2, 3])), 139 | from_value(&nbt!([I; 1, 2, 3])) 140 | ); 141 | assert_eq!( 142 | Ok(LongArray::new(vec![1, 2, 3])), 143 | from_value(&nbt!([L; 1, 2, 3])) 144 | ); 145 | assert_eq!(Ok(vec![1, 2, 3, 4]), from_value(&nbt!([1, 2, 3, 4]))); 146 | } 147 | -------------------------------------------------------------------------------- /fastnbt/src/test/value/mod.rs: -------------------------------------------------------------------------------- 1 | mod ser; 2 | mod de; 3 | 4 | use std::collections::HashMap; 5 | 6 | use crate::{from_bytes, to_bytes, Tag, Value}; 7 | 8 | use super::builder::Builder; 9 | 10 | // Given a v: Value, a key: str, and a pattern, check the value is a compound 11 | // withat key and it's value matches the pattern. Optionally add a condition for the 12 | // matched value 13 | macro_rules! assert_contains { 14 | ($v:ident, $key:expr, $p:pat) => { 15 | if let Value::Compound(v) = &$v { 16 | match v[$key] { 17 | $p => {} 18 | _ => panic!("expected Some({}), got {:?}", stringify!($p), v.get($key)), 19 | } 20 | } else { 21 | panic!("expected compound"); 22 | } 23 | }; 24 | ($v:ident, $key:expr, $p:pat, $check:expr) => { 25 | if let Value::Compound(v) = &$v { 26 | match v[$key] { 27 | $p => assert!($check), 28 | _ => panic!("expected Some({}), got {:?}", stringify!($p), v.get($key)), 29 | } 30 | } else { 31 | panic!("expected compound"); 32 | } 33 | }; 34 | } 35 | 36 | #[test] 37 | fn distinguish_byte() { 38 | let input = Builder::new() 39 | .start_compound("") 40 | .byte("a", 123) 41 | .byte("b", -123) 42 | .end_compound() 43 | .build(); 44 | 45 | let v: Value = from_bytes(&input).unwrap(); 46 | assert_contains!(v, "a", Value::Byte(123)); 47 | assert_contains!(v, "b", Value::Byte(-123)); 48 | } 49 | 50 | #[test] 51 | fn distinguish_short() { 52 | let input = Builder::new() 53 | .start_compound("") 54 | .short("a", 1) 55 | .short("b", 1000) 56 | .end_compound() 57 | .build(); 58 | 59 | let v: Value = from_bytes(&input).unwrap(); 60 | assert_contains!(v, "a", Value::Short(1)); 61 | assert_contains!(v, "b", Value::Short(1000)); 62 | } 63 | 64 | #[test] 65 | fn distinguish_int() { 66 | let input = Builder::new() 67 | .start_compound("") 68 | .int("a", 1) 69 | .int("b", 1000) 70 | .int("c", 1_000_000) 71 | .end_compound() 72 | .build(); 73 | 74 | let v: Value = from_bytes(&input).unwrap(); 75 | assert_contains!(v, "a", Value::Int(1)); 76 | assert_contains!(v, "b", Value::Int(1000)); 77 | assert_contains!(v, "c", Value::Int(1_000_000)); 78 | } 79 | 80 | #[test] 81 | fn distinguish_long() { 82 | let input = Builder::new() 83 | .start_compound("") 84 | .long("a", 1) 85 | .long("b", 1000) 86 | .long("c", 1_000_000) 87 | .long("d", 10_000_000_000) 88 | .end_compound() 89 | .build(); 90 | 91 | let v: Value = from_bytes(&input).unwrap(); 92 | assert_contains!(v, "a", Value::Long(1)); 93 | assert_contains!(v, "b", Value::Long(1000)); 94 | assert_contains!(v, "c", Value::Long(1_000_000)); 95 | assert_contains!(v, "d", Value::Long(10_000_000_000)); 96 | } 97 | 98 | #[test] 99 | fn distinguish_floats() { 100 | let input = Builder::new() 101 | .start_compound("") 102 | .float("a", 1.23) 103 | .double("b", 3.21) 104 | .float("c", 4.56) 105 | .end_compound() 106 | .build(); 107 | 108 | let v: Value = from_bytes(&input).unwrap(); 109 | assert_contains!(v, "a", Value::Float(f), f == 1.23); 110 | assert_contains!(v, "b", Value::Double(f), f == 3.21); 111 | assert_contains!(v, "c", Value::Float(f), f == 4.56); 112 | 113 | let bs = to_bytes(&v).unwrap(); 114 | let rt: Value = from_bytes(&bs).unwrap(); 115 | assert_eq!(rt, v); 116 | } 117 | 118 | #[test] 119 | fn fuzz_float() { 120 | let v = Value::Float(1.4e-44); 121 | let mut inner = HashMap::new(); 122 | inner.insert("".to_string(), v); 123 | 124 | let v = Value::Compound(inner); 125 | let bs = to_bytes(&v).unwrap(); 126 | let roundtrip: Value = from_bytes(&bs).unwrap(); // anything that serializes should deserialize. 127 | 128 | assert_eq!(v, roundtrip); 129 | } 130 | 131 | #[test] 132 | fn distinguish_string() { 133 | let input = Builder::new() 134 | .start_compound("") 135 | .string("a", "hello") 136 | .end_compound() 137 | .build(); 138 | 139 | let v: Value = from_bytes(&input).unwrap(); 140 | assert_contains!(v, "a", Value::String(ref s), s == "hello"); 141 | } 142 | 143 | #[test] 144 | fn distinguish_arrays() { 145 | let input = Builder::new() 146 | .start_compound("") 147 | .byte_array("a", &[1, 2, 3]) 148 | .int_array("b", &[4, 5, 6]) 149 | .long_array("c", &[7, 8, 9]) 150 | .end_compound() 151 | .build(); 152 | 153 | let v: Value = from_bytes(&input).unwrap(); 154 | assert_contains!( 155 | v, 156 | "a", 157 | Value::ByteArray(ref data), 158 | data.iter().eq(&[1, 2, 3]) 159 | ); 160 | assert_contains!( 161 | v, 162 | "b", 163 | Value::IntArray(ref data), 164 | data.iter().eq(&[4, 5, 6]) 165 | ); 166 | assert_contains!( 167 | v, 168 | "c", 169 | Value::LongArray(ref data), 170 | data.iter().eq(&[7, 8, 9]) 171 | ); 172 | } 173 | 174 | #[test] 175 | fn distinguish_lists() { 176 | let input = Builder::new() 177 | .start_compound("") 178 | .start_list("a", Tag::Byte, 3) 179 | .byte_payload(1) 180 | .byte_payload(2) 181 | .byte_payload(3) 182 | .start_list("b", Tag::Int, 3) 183 | .int_payload(1) 184 | .int_payload(2) 185 | .int_payload(3) 186 | .start_list("c", Tag::Long, 3) 187 | .long_payload(1) 188 | .long_payload(2) 189 | .long_payload(3) 190 | .end_compound() 191 | .build(); 192 | 193 | let v: Value = from_bytes(&input).unwrap(); 194 | assert_contains!(v, "a", Value::List(ref data), data.iter().eq(&[1, 2, 3])); 195 | assert_contains!(v, "b", Value::List(ref data), data.iter().eq(&[1, 2, 3])); 196 | assert_contains!(v, "c", Value::List(ref data), data.iter().eq(&[1, 2, 3])); 197 | } 198 | 199 | #[test] 200 | fn empty_compound() { 201 | let input = Builder::new().start_compound("").end_compound().build(); 202 | 203 | let v: Value = from_bytes(&input).unwrap(); 204 | assert!(matches!(v, Value::Compound(_))) 205 | } 206 | 207 | #[test] 208 | fn distinguish_compound() { 209 | let input = Builder::new() 210 | .start_compound("") 211 | .start_compound("a") 212 | .end_compound() 213 | .end_compound() 214 | .build(); 215 | 216 | let v: Value = from_bytes(&input).unwrap(); 217 | assert_contains!(v, "a", Value::Compound(_)); 218 | } 219 | -------------------------------------------------------------------------------- /fastnbt/src/test/value/ser.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Serialize; 4 | 5 | use crate::{to_value, ByteArray, IntArray, LongArray, Value}; 6 | 7 | #[test] 8 | fn simple_types() { 9 | #[derive(Serialize)] 10 | struct V { 11 | bool: bool, 12 | i8: i8, 13 | i16: i16, 14 | i32: i32, 15 | i64: i64, 16 | u8: u8, 17 | u16: u16, 18 | u32: u32, 19 | u64: u64, 20 | f32: f32, 21 | f64: f64, 22 | char: char, 23 | str: &'static str, 24 | string: String, 25 | } 26 | 27 | let v = V { 28 | bool: true, 29 | i8: i8::MAX, 30 | i16: i16::MAX, 31 | i32: i32::MAX, 32 | i64: i64::MAX, 33 | u8: u8::MAX, 34 | u16: u16::MAX, 35 | u32: u32::MAX, 36 | u64: u64::MAX, 37 | f32: f32::MAX, 38 | f64: f64::MAX, 39 | char: 'n', 40 | str: "value", 41 | string: "value".to_string(), 42 | }; 43 | 44 | let val = to_value(v).unwrap(); 45 | // Note: we cannot use the nbt! macro here as that uses the `to_value` function 46 | let expected = Value::Compound(HashMap::from([ 47 | ("bool".to_string(), Value::Byte(1)), 48 | ("i8".to_string(), Value::Byte(i8::MAX)), 49 | ("i16".to_string(), Value::Short(i16::MAX)), 50 | ("i32".to_string(), Value::Int(i32::MAX)), 51 | ("i64".to_string(), Value::Long(i64::MAX)), 52 | ("u8".to_string(), Value::Byte(u8::MAX as i8)), 53 | ("u16".to_string(), Value::Short(u16::MAX as i16)), 54 | ("u32".to_string(), Value::Int(u32::MAX as i32)), 55 | ("u64".to_string(), Value::Long(u64::MAX as i64)), 56 | ("f32".to_string(), Value::Float(f32::MAX)), 57 | ("f64".to_string(), Value::Double(f64::MAX)), 58 | ("char".to_string(), Value::Int('n' as i32)), 59 | ("str".to_string(), Value::String("value".to_string())), 60 | ("string".to_string(), Value::String("value".to_string())), 61 | ])); 62 | 63 | assert_eq!(expected, val); 64 | } 65 | 66 | #[test] 67 | fn int_array_types() { 68 | #[derive(Serialize)] 69 | struct V { 70 | i128: i128, 71 | u128: u128, 72 | bytes: ByteArray, 73 | ints: IntArray, 74 | longs: LongArray, 75 | } 76 | 77 | let v = V { 78 | i128: i128::MAX, 79 | u128: u128::MAX, 80 | bytes: ByteArray::new(vec![1, 2, 3, 4, 5]), 81 | ints: IntArray::new(vec![1, 2, 3, 4, 5]), 82 | longs: LongArray::new(vec![1, 2, 3, 4, 5]), 83 | }; 84 | 85 | let val = to_value(v).unwrap(); 86 | let expected = Value::Compound(HashMap::from([ 87 | ( 88 | "i128".to_string(), 89 | // Only left most bit is 0 90 | Value::IntArray(IntArray::new(vec![ 91 | i32::MAX, 92 | u32::MAX as i32, 93 | u32::MAX as i32, 94 | u32::MAX as i32, 95 | ])), 96 | ), 97 | ( 98 | "u128".to_string(), 99 | // All bits are 1 100 | Value::IntArray(IntArray::new(vec![u32::MAX as i32; 4])), 101 | ), 102 | ( 103 | "bytes".to_string(), 104 | Value::ByteArray(ByteArray::new(vec![1, 2, 3, 4, 5])), 105 | ), 106 | ( 107 | "ints".to_string(), 108 | Value::IntArray(IntArray::new(vec![1, 2, 3, 4, 5])), 109 | ), 110 | ( 111 | "longs".to_string(), 112 | Value::LongArray(LongArray::new(vec![1, 2, 3, 4, 5])), 113 | ), 114 | ])); 115 | 116 | assert_eq!(expected, val); 117 | } 118 | 119 | #[test] 120 | fn nested() { 121 | #[derive(Serialize)] 122 | struct V { 123 | list: Vec, 124 | nested: Inner, 125 | } 126 | 127 | #[derive(Serialize)] 128 | struct Inner { 129 | key: u8, 130 | } 131 | 132 | let v = V { 133 | list: vec![1, 2], 134 | nested: Inner { key: 42 }, 135 | }; 136 | 137 | let val = to_value(v).unwrap(); 138 | let expected = Value::Compound(HashMap::from([ 139 | ( 140 | "list".to_string(), 141 | Value::List(vec![Value::Short(1), Value::Short(2)]), 142 | ), 143 | ( 144 | "nested".to_string(), 145 | Value::Compound(HashMap::from([("key".to_string(), Value::Byte(42))])), 146 | ), 147 | ])); 148 | 149 | assert_eq!(expected, val); 150 | } 151 | 152 | #[test] 153 | fn no_root_compound() { 154 | assert_eq!(Ok(Value::Byte(-128)), to_value(-128_i8)); 155 | assert_eq!(Ok(Value::Int(97)), to_value('a')); 156 | assert_eq!(Ok(Value::String("string".to_string())), to_value("string")); 157 | assert_eq!( 158 | Ok(Value::IntArray(IntArray::new(vec![u32::MAX as i32; 4]))), 159 | to_value(u128::MAX) 160 | ); 161 | assert_eq!( 162 | Ok(Value::ByteArray(ByteArray::new(vec![1, 2, 3]))), 163 | to_value(ByteArray::new(vec![1, 2, 3])) 164 | ); 165 | assert_eq!( 166 | Ok(Value::IntArray(IntArray::new(vec![1, 2, 3]))), 167 | to_value(IntArray::new(vec![1, 2, 3])) 168 | ); 169 | assert_eq!( 170 | Ok(Value::LongArray(LongArray::new(vec![1, 2, 3]))), 171 | to_value(LongArray::new(vec![1, 2, 3])) 172 | ); 173 | assert_eq!( 174 | Ok(Value::List(vec![ 175 | Value::Byte(1), 176 | Value::Byte(2), 177 | Value::Byte(3), 178 | Value::Byte(4) 179 | ])), 180 | to_value(vec![ 181 | Value::Byte(1), 182 | Value::Byte(2), 183 | Value::Byte(3), 184 | Value::Byte(4) 185 | ]) 186 | ); 187 | } 188 | -------------------------------------------------------------------------------- /fastnbt/src/value/array_serializer.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{NativeEndian, ReadBytesExt}; 2 | use serde::ser::Impossible; 3 | 4 | use crate::{error::Error, ByteArray, IntArray, LongArray, Tag, Value}; 5 | 6 | use super::ser::Serializer; 7 | 8 | /// ArraySerializer is for serializing the NBT Arrays ie ByteArray, IntArray and 9 | /// LongArray. 10 | pub struct ArraySerializer<'a> { 11 | pub ser: &'a mut Serializer, 12 | pub tag: Tag, 13 | } 14 | 15 | impl<'a> serde::Serializer for ArraySerializer<'a> { 16 | type Ok = Value; 17 | type Error = Error; 18 | type SerializeSeq = Impossible; 19 | type SerializeTuple = Impossible; 20 | type SerializeTupleStruct = Impossible; 21 | type SerializeTupleVariant = Impossible; 22 | type SerializeMap = Impossible; 23 | type SerializeStruct = Impossible; 24 | type SerializeStructVariant = Impossible; 25 | 26 | fn serialize_bool(self, _v: bool) -> Result { 27 | unimplemented!() 28 | } 29 | 30 | fn serialize_i8(self, _v: i8) -> Result { 31 | unimplemented!() 32 | } 33 | 34 | fn serialize_i16(self, _v: i16) -> Result { 35 | unimplemented!() 36 | } 37 | 38 | fn serialize_i32(self, _v: i32) -> Result { 39 | unimplemented!() 40 | } 41 | 42 | fn serialize_i64(self, _v: i64) -> Result { 43 | unimplemented!() 44 | } 45 | 46 | fn serialize_u8(self, _v: u8) -> Result { 47 | unimplemented!() 48 | } 49 | 50 | fn serialize_u16(self, _v: u16) -> Result { 51 | unimplemented!() 52 | } 53 | 54 | fn serialize_u32(self, _v: u32) -> Result { 55 | unimplemented!() 56 | } 57 | 58 | fn serialize_u64(self, _v: u64) -> Result { 59 | unimplemented!() 60 | } 61 | 62 | fn serialize_f32(self, _v: f32) -> Result { 63 | unimplemented!() 64 | } 65 | 66 | fn serialize_f64(self, _v: f64) -> Result { 67 | unimplemented!() 68 | } 69 | 70 | fn serialize_char(self, _v: char) -> Result { 71 | unimplemented!() 72 | } 73 | 74 | fn serialize_str(self, _v: &str) -> Result { 75 | unimplemented!() 76 | } 77 | 78 | fn serialize_bytes(self, v: &[u8]) -> Result { 79 | match self.tag { 80 | Tag::ByteArray => Ok(Value::ByteArray(ByteArray::from_bytes(v))), 81 | Tag::IntArray => Ok(Value::IntArray(IntArray::new( 82 | v.chunks_exact(4) 83 | .map(|mut bs| bs.read_i32::()) 84 | .collect::>>()?, 85 | ))), 86 | Tag::LongArray => Ok(Value::LongArray(LongArray::new( 87 | v.chunks_exact(8) 88 | .map(|mut bs| bs.read_i64::()) 89 | .collect::>>()?, 90 | ))), 91 | _ => unreachable!(), 92 | } 93 | } 94 | 95 | fn serialize_none(self) -> Result { 96 | unimplemented!() 97 | } 98 | 99 | fn serialize_some(self, _value: &T) -> Result 100 | where 101 | T: serde::Serialize, 102 | { 103 | unimplemented!() 104 | } 105 | 106 | fn serialize_unit(self) -> Result { 107 | unimplemented!() 108 | } 109 | 110 | fn serialize_unit_struct(self, _name: &'static str) -> Result { 111 | unimplemented!() 112 | } 113 | 114 | fn serialize_unit_variant( 115 | self, 116 | _name: &'static str, 117 | _variant_index: u32, 118 | _variant: &'static str, 119 | ) -> Result { 120 | unimplemented!() 121 | } 122 | 123 | fn serialize_newtype_struct( 124 | self, 125 | _name: &'static str, 126 | _value: &T, 127 | ) -> Result 128 | where 129 | T: serde::Serialize, 130 | { 131 | unimplemented!() 132 | } 133 | 134 | fn serialize_newtype_variant( 135 | self, 136 | _name: &'static str, 137 | _variant_index: u32, 138 | _variant: &'static str, 139 | _value: &T, 140 | ) -> Result 141 | where 142 | T: serde::Serialize, 143 | { 144 | unimplemented!() 145 | } 146 | 147 | fn serialize_seq(self, _len: Option) -> Result { 148 | unimplemented!() 149 | } 150 | 151 | fn serialize_tuple(self, _len: usize) -> Result { 152 | unimplemented!() 153 | } 154 | 155 | fn serialize_tuple_struct( 156 | self, 157 | _name: &'static str, 158 | _len: usize, 159 | ) -> Result { 160 | unimplemented!() 161 | } 162 | 163 | fn serialize_tuple_variant( 164 | self, 165 | _name: &'static str, 166 | _variant_index: u32, 167 | _variant: &'static str, 168 | _len: usize, 169 | ) -> Result { 170 | unimplemented!() 171 | } 172 | 173 | fn serialize_map(self, _len: Option) -> Result { 174 | unimplemented!() 175 | } 176 | 177 | fn serialize_struct( 178 | self, 179 | _name: &'static str, 180 | _len: usize, 181 | ) -> Result { 182 | unimplemented!() 183 | } 184 | 185 | fn serialize_struct_variant( 186 | self, 187 | _name: &'static str, 188 | _variant_index: u32, 189 | _variant: &'static str, 190 | _len: usize, 191 | ) -> Result { 192 | unimplemented!() 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /fastsnbt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fastsnbt" 3 | description = "Serde deserializer for Minecraft's stringified NBT format" 4 | repository = "https://github.com/owengage/fastnbt" 5 | readme = "README.md" 6 | version = "0.2.0" 7 | authors = ["Owen Gage ", "GrizzlT "] 8 | edition = "2021" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["minecraft", "nbt", "snbt", "serde"] 11 | categories = ["parser-implementations"] 12 | 13 | [dependencies] 14 | serde = { version = "1" } 15 | byteorder = "1" 16 | itoa = "1" 17 | ryu = "1" 18 | nom = "7" 19 | 20 | [dev-dependencies] 21 | fastnbt = "2" 22 | -------------------------------------------------------------------------------- /fastsnbt/README.md: -------------------------------------------------------------------------------- 1 | # fastsnbt crate 2 | 3 | Documentation: [docs.rs](https://docs.rs/crate/fastsnbt) 4 | 5 | Fast serde deserializer and serializer for *Minecraft: Java Edition*'s sNBT format. 6 | 7 | Zero-copy is supported where possible through `from_str`. 8 | 9 | [See fastnbt's documentation](https://docs.rs/crate/fastnbt) for more information. 10 | 11 | ```toml 12 | [dependencies] 13 | fastsnbt = "0.2" 14 | ``` 15 | 16 | `fastsnbt` follows Semver, some things that this project does *not* count as a 17 | breaking change are: 18 | 19 | * Minimum Rust version change. Outside of corporate environments this should not 20 | be too difficult, and I don't see much need for sNBT in those environments. 21 | * Improving the (de)serializer such that valid sNBT that did not (de)serialize, then 22 | (de)serializes. Any of these cases I consider a bug. 23 | 24 | Changes that make `fastsnbt` incompatible with WebAssembly *are* considered 25 | breaking changes. 26 | 27 | ## NBT crate 28 | 29 | `fastsnbt` tightly cooperates with 30 | [`fastnbt`](https://github.com/owengage/fastnbt/blob/master/fastnbt/README.md). 31 | It serves more as an extension to `fastnbt` than a standalone crate. 32 | For NBT types, `Value` etc. see [fastnbt's docs here](https://docs.rs/crate/fastnbt). 33 | -------------------------------------------------------------------------------- /fastsnbt/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Contains the [`Error`] and [`Result`] type used by the deserializer. 2 | use std::fmt::Display; 3 | 4 | /// Various errors that can occur during (de)serialization. 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct Error(String); 7 | 8 | /// Convenience type for Result. 9 | pub type Result = std::result::Result; 10 | 11 | impl std::error::Error for Error {} 12 | 13 | impl std::fmt::Display for Error { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | f.write_str(&self.0) 16 | } 17 | } 18 | 19 | impl serde::de::Error for Error { 20 | fn custom(msg: T) -> Self { 21 | Error(msg.to_string()) 22 | } 23 | } 24 | 25 | impl serde::ser::Error for Error { 26 | fn custom(msg: T) -> Self 27 | where 28 | T: Display, 29 | { 30 | Error(msg.to_string()) 31 | } 32 | } 33 | 34 | impl From for Error { 35 | fn from(e: std::io::Error) -> Self { 36 | Error(format!("io error: {}", e)) 37 | } 38 | } 39 | 40 | impl Error { 41 | pub(crate) fn invalid_input(pos: usize) -> Error { 42 | Error(format!("invalid input at {}", pos)) 43 | } 44 | 45 | pub(crate) fn input_not_consumed() -> Error { 46 | Error("Input wasn't fully consumed".into()) 47 | } 48 | 49 | pub(crate) fn expected_comma() -> Error { 50 | Error("expected comma".into()) 51 | } 52 | 53 | pub(crate) fn expected_colon() -> Error { 54 | Error("expected colon".into()) 55 | } 56 | 57 | pub(crate) fn expected_collection_end() -> Error { 58 | Error("expected ] or } end".into()) 59 | } 60 | 61 | pub(crate) fn unexpected_eof() -> Error { 62 | Error("eof: unexpectedly ran out of input".to_owned()) 63 | } 64 | 65 | pub(crate) fn array_as_other() -> Error { 66 | Error("expected NBT Array: use ByteArray, IntArray or LongArray types".into()) 67 | } 68 | 69 | pub(crate) fn bespoke(msg: String) -> Error { 70 | Error(msg) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /fastsnbt/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `fastsnbt` aims for fast deserializing and serializing of 2 | //! sNBT data from *Minecraft: Java Edition*. 3 | //! This is the human-readable equivalent of NBT data. (see 4 | //! [`fastnbt`](https://crates.io/crates/fastnbt)). 5 | //! 6 | //! - For documentation of serde (de)serialization, see [`ser`] and [`de`]. 7 | //! - See [`fastnbt`](https://crates.io/crates/fastnbt) for most 8 | //! NBT related things. 9 | //! 10 | //! # Example 11 | //! ``` 12 | //! # use serde::{Serialize, Deserialize}; 13 | //! #[derive(Debug, PartialEq, Serialize, Deserialize)] 14 | //! struct SimpleStruct { 15 | //! num: i64, 16 | //! s: String, 17 | //! } 18 | //! 19 | //! let data = SimpleStruct { 20 | //! num: 31300, 21 | //! s: "Hello world!".into(), 22 | //! }; 23 | //! let ser = fastsnbt::to_string(&data).unwrap(); 24 | //! assert_eq!("{\"num\":31300l,\"s\":\"Hello world!\"}", ser); 25 | //! 26 | //! let input = "{num:31300L,s:\"Hello world!\"}"; 27 | //! let de: SimpleStruct = fastsnbt::from_str(input).unwrap(); 28 | //! assert_eq!(data, de); 29 | //! ``` 30 | 31 | use de::Deserializer; 32 | use error::Result; 33 | use ser::Serializer; 34 | use serde::Serialize; 35 | 36 | pub mod ser; 37 | pub mod de; 38 | pub mod error; 39 | pub(crate) mod parser; 40 | 41 | pub(crate) const BYTE_ARRAY_TOKEN_STR: &str = "\"__fastnbt_byte_array\""; 42 | pub(crate) const INT_ARRAY_TOKEN_STR: &str = "\"__fastnbt_int_array\""; 43 | pub(crate) const LONG_ARRAY_TOKEN_STR: &str = "\"__fastnbt_long_array\""; 44 | pub(crate) const BYTE_ARRAY_TOKEN: &str = "__fastnbt_byte_array"; 45 | pub(crate) const INT_ARRAY_TOKEN: &str = "__fastnbt_int_array"; 46 | pub(crate) const LONG_ARRAY_TOKEN: &str = "__fastnbt_long_array"; 47 | 48 | #[cfg(test)] 49 | mod tests; 50 | 51 | /// Deserialize into a `T` from some sNBT data. See the 52 | /// [`de`] module for more information. 53 | pub fn from_str<'a, T>(input: &'a str) -> Result 54 | where 55 | T: serde::de::Deserialize<'a>, 56 | { 57 | let mut des = Deserializer::from_str(input); 58 | let t = T::deserialize(&mut des)?; 59 | if !des.input.is_empty() { 60 | return Err(error::Error::input_not_consumed()); 61 | } 62 | Ok(t) 63 | } 64 | 65 | /// Serialize some `T` into some sNBT string. This produces 66 | /// valid utf-8. See the [`ser`] module for more information. 67 | pub fn to_vec(value: &T) -> Result> { 68 | let mut serializer = Serializer { writer: Vec::new() }; 69 | value.serialize(&mut serializer)?; 70 | Ok(serializer.writer) 71 | } 72 | 73 | /// Serialize some `T` into a sNBT string. See the [`ser`] 74 | /// module for more information. 75 | pub fn to_string(value: &T) -> Result { 76 | let vec = to_vec(value)?; 77 | let string = unsafe { 78 | // We do not emit invalid UTF-8. 79 | String::from_utf8_unchecked(vec) 80 | }; 81 | Ok(string) 82 | } 83 | -------------------------------------------------------------------------------- /fastsnbt/src/parser.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use nom::{IResult, combinator::{recognize, opt, map_res, map, cut}, sequence::{tuple, delimited, pair}, character::complete::{one_of, char, digit0, alphanumeric1, digit1}, branch::alt, bytes::complete::{tag, is_a, tag_no_case}, multi::many1, error::{ParseError, ErrorKind}}; 4 | 5 | pub fn parse_str(input: &str) -> IResult<&str, Cow<'_, str>> { 6 | alt(( 7 | delimited(char('"'), parse_escaped('"'), char('"')), 8 | delimited(char('\''), parse_escaped('\''), char('\'')), 9 | map(parse_simple_string, Cow::from), 10 | ))(input) 11 | } 12 | 13 | fn parse_escaped<'a, E: ParseError<&'a str>>(surround: char) -> impl FnMut(&'a str) -> IResult<&'a str, Cow<'a, str>, E> { 14 | move |input: &'a str| { 15 | let mut owned = String::new(); 16 | let mut start = 0; 17 | let mut skip = false; 18 | let mut chars = input.chars(); 19 | while let Some(c) = chars.next() { 20 | if skip { 21 | skip = false; 22 | owned.push(c); 23 | start = input.len() - chars.as_str().len(); 24 | } else if c == '\\' { 25 | let len = input.len() - chars.as_str().len() - 1; 26 | owned.push_str(&input[start..len]); 27 | skip = true; 28 | } else if c == surround { 29 | let len = input.len() - chars.as_str().len() - surround.len_utf8(); 30 | if !owned.is_empty() { 31 | if len > start { 32 | owned.push_str(&input[start..len]); 33 | } 34 | return Ok((&input[len..], Cow::from(owned))); 35 | } else { 36 | return Ok((&input[len..], Cow::from(&input[..len]))); 37 | } 38 | } 39 | } 40 | Err(nom::Err::Error(E::from_error_kind(input, ErrorKind::MapRes))) 41 | } 42 | } 43 | 44 | fn parse_simple_string(input: &str) -> IResult<&str, &str> { 45 | recognize(many1(alt(( 46 | alphanumeric1, 47 | is_a("_-.+"), 48 | ))))(input) 49 | } 50 | 51 | pub fn parse_bool(input: &str) -> IResult<&str, bool> { 52 | alt((map(tag("true"), |_| true), map(tag("false"), |_| false)))(input) 53 | } 54 | 55 | pub fn parse_i8(input: &str) -> IResult<&str, i8> { 56 | map_res(|input| { 57 | let (input, num) = decimal(input)?; 58 | let (input, _) = alt((char('b'), char('B')))(input)?; 59 | Ok((input, num)) 60 | }, |s: &str| s.parse())(input) 61 | } 62 | 63 | pub fn parse_i16(input: &str) -> IResult<&str, i16> { 64 | map_res(|input| { 65 | let (input, num) = decimal(input)?; 66 | let (input, _) = alt((char('s'), char('S')))(input)?; 67 | Ok((input, num)) 68 | }, |s: &str| s.parse())(input) 69 | } 70 | 71 | pub fn parse_i32(input: &str) -> IResult<&str, i32> { 72 | map_res(decimal, |s: &str| s.parse())(input) 73 | } 74 | 75 | pub fn parse_i64(input: &str) -> IResult<&str, i64> { 76 | map_res(|input| { 77 | let (input, num) = decimal(input)?; 78 | let (input, _) = alt((char('l'), char('L')))(input)?; 79 | Ok((input, num)) 80 | }, |s: &str| s.parse())(input) 81 | } 82 | 83 | pub fn parse_f32(input: &str) -> IResult<&str, f32> { 84 | map_res(|input| { 85 | let (input, num) = float(input)?; 86 | let (input, _) = alt((char('f'), char('F')))(input)?; 87 | Ok((input, num)) 88 | }, |s: &str| s.parse())(input) 89 | } 90 | 91 | pub fn parse_f64(input: &str) -> IResult<&str, f64> { 92 | map_res(|input| { 93 | let (input, num) = float(input)?; 94 | let (input, _) = opt(alt((char('d'), char('D'))))(input)?; 95 | Ok((input, num)) 96 | }, |s: &str| s.parse())(input) 97 | } 98 | 99 | fn float(input: &str) -> IResult<&str, &str> { 100 | alt((tag_no_case("inf"), tag_no_case("infinity"), tag_no_case("nan"), 101 | tag_no_case("-inf"), tag_no_case("-infinity"), 102 | recognize(alt(( 103 | map(tuple(( 104 | opt(alt((char('+'), char('-')))), 105 | map(tuple((digit1, exp)), |_| ()), 106 | )), |_| ()), 107 | map(tuple(( 108 | float_num, 109 | opt(exp), 110 | )), |_| ()), 111 | )))))(input) 112 | } 113 | 114 | fn float_num(input: &str) -> IResult<&str, &str> { 115 | recognize(tuple(( 116 | opt(alt((char('+'), char('-')))), 117 | alt(( 118 | map(tuple((digit1, pair(char('.'), opt(digit1)))), |_| ()), 119 | map(tuple((char('.'), digit1)), |_| ()) 120 | )) 121 | )))(input) 122 | } 123 | 124 | fn exp(input: &str) -> IResult<&str, &str> { 125 | recognize(tuple(( 126 | alt((char('e'), char('E'))), 127 | opt(alt((char('+'), char('-')))), 128 | cut(digit1) 129 | )))(input) 130 | } 131 | 132 | // parse a single 0 OR a non-zero digit followed by a 0 or more digits 133 | fn decimal(input: &str) -> IResult<&str, &str> { 134 | recognize(tuple((opt(one_of("+-")), 135 | alt((recognize( 136 | tuple(( 137 | one_of("123456789"), 138 | digit0, 139 | )), 140 | ), 141 | tag("0") 142 | )) 143 | )))(input) 144 | } 145 | -------------------------------------------------------------------------------- /fastsnbt/src/ser/array_serializer.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use byteorder::{ReadBytesExt, BigEndian}; 4 | use serde::ser::{Impossible, SerializeSeq}; 5 | 6 | use crate::{error::Error, error::Result}; 7 | 8 | use super::Serializer; 9 | 10 | /// ArraySerializer is for serializing the NBT Arrays ie ByteArray, IntArray and 11 | /// LongArray. 12 | pub(crate) struct ArraySerializer<'a, W> { 13 | pub(crate) ser: &'a mut Serializer, 14 | pub(crate) stride: usize, 15 | pub(crate) prefix: &'static str, 16 | } 17 | 18 | macro_rules! only_bytes { 19 | ($v:ident, $t:ty) => { 20 | fn $v(self, _: $t) -> Result<()> { 21 | Err(Error::array_as_other()) 22 | } 23 | }; 24 | } 25 | 26 | impl<'a, W: Write> serde::Serializer for ArraySerializer<'a, W> { 27 | type Ok = (); 28 | type Error = Error; 29 | type SerializeSeq = Impossible<(), Error>; 30 | type SerializeTuple = Impossible<(), Error>; 31 | type SerializeTupleStruct = Impossible<(), Error>; 32 | type SerializeTupleVariant = Impossible<(), Error>; 33 | type SerializeMap = Impossible<(), Error>; 34 | type SerializeStruct = Impossible<(), Error>; 35 | type SerializeStructVariant = Impossible<(), Error>; 36 | 37 | only_bytes!(serialize_bool, bool); 38 | only_bytes!(serialize_i8, i8); 39 | only_bytes!(serialize_i16, i16); 40 | only_bytes!(serialize_i32, i32); 41 | only_bytes!(serialize_i64, i64); 42 | only_bytes!(serialize_i128, i128); 43 | only_bytes!(serialize_u8, u8); 44 | only_bytes!(serialize_u16, u16); 45 | only_bytes!(serialize_u32, u32); 46 | only_bytes!(serialize_u64, u64); 47 | only_bytes!(serialize_u128, u128); 48 | only_bytes!(serialize_f32, f32); 49 | only_bytes!(serialize_f64, f64); 50 | only_bytes!(serialize_char, char); 51 | only_bytes!(serialize_str, &str); 52 | only_bytes!(serialize_unit_struct, &'static str); 53 | 54 | fn serialize_bytes(self, v: &[u8]) -> Result { 55 | let mut serializer = super::ArraySerializer::new(self.prefix, self.ser)?; 56 | match self.stride { 57 | 1 => { 58 | let data = unsafe { &*(v as *const [u8] as *const [i8]) }; 59 | data.iter().try_for_each(|i| SerializeSeq::serialize_element(&mut serializer, i))? 60 | } 61 | 4 => v.chunks_exact(4).map(|mut bs| bs.read_i32::()) 62 | .try_for_each(|i| SerializeSeq::serialize_element(&mut serializer, &i?))?, 63 | 8 => v.chunks_exact(8).map(|mut bs| bs.read_i64::()) 64 | .try_for_each(|l| SerializeSeq::serialize_element(&mut serializer, &l?))?, 65 | _ => panic!(), 66 | } 67 | SerializeSeq::end(serializer) 68 | } 69 | 70 | fn serialize_none(self) -> Result { 71 | Err(Error::array_as_other()) 72 | } 73 | 74 | fn serialize_some(self, _value: &T) -> Result 75 | where 76 | T: serde::Serialize, 77 | { 78 | Err(Error::array_as_other()) 79 | } 80 | 81 | fn serialize_unit(self) -> Result { 82 | Err(Error::array_as_other()) 83 | } 84 | 85 | fn serialize_unit_variant( 86 | self, 87 | _name: &'static str, 88 | _variant_index: u32, 89 | _variant: &'static str, 90 | ) -> Result { 91 | Err(Error::array_as_other()) 92 | } 93 | 94 | fn serialize_newtype_struct( 95 | self, 96 | _name: &'static str, 97 | _value: &T, 98 | ) -> Result 99 | where 100 | T: serde::Serialize, 101 | { 102 | Err(Error::array_as_other()) 103 | } 104 | 105 | fn serialize_newtype_variant( 106 | self, 107 | _name: &'static str, 108 | _variant_index: u32, 109 | _variant: &'static str, 110 | _value: &T, 111 | ) -> Result 112 | where 113 | T: serde::Serialize, 114 | { 115 | Err(Error::array_as_other()) 116 | } 117 | 118 | fn serialize_seq(self, _len: Option) -> Result { 119 | Err(Error::array_as_other()) 120 | } 121 | 122 | fn serialize_tuple(self, _len: usize) -> Result { 123 | Err(Error::array_as_other()) 124 | } 125 | 126 | fn serialize_tuple_struct( 127 | self, 128 | _name: &'static str, 129 | _len: usize, 130 | ) -> Result { 131 | Err(Error::array_as_other()) 132 | } 133 | 134 | fn serialize_tuple_variant( 135 | self, 136 | _name: &'static str, 137 | _variant_index: u32, 138 | _variant: &'static str, 139 | _len: usize, 140 | ) -> Result { 141 | Err(Error::array_as_other()) 142 | } 143 | 144 | fn serialize_map(self, _len: Option) -> Result { 145 | Err(Error::array_as_other()) 146 | } 147 | 148 | fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { 149 | Err(Error::array_as_other()) 150 | } 151 | 152 | fn serialize_struct_variant( 153 | self, 154 | _name: &'static str, 155 | _variant_index: u32, 156 | _variant: &'static str, 157 | _len: usize, 158 | ) -> Result { 159 | Err(Error::array_as_other()) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /fastsnbt/src/ser/name_serializer.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use serde::{ser::Impossible, Serializer}; 4 | 5 | use crate::error::Error; 6 | 7 | use super::write_escaped_str; 8 | 9 | pub(crate) struct NameSerializer { 10 | pub(crate) name: W, 11 | } 12 | 13 | fn name_must_be_stringy(ty: &str) -> Error { 14 | Error::bespoke(format!("field must be string-like, found {ty}")) 15 | } 16 | 17 | /// NameSerializer is all about serializing the name of a field. It does not 18 | /// write the length or the tag. We typically need to write this to a different 19 | /// buffer than the main one we're writing to, because we need to write out the 20 | /// field in tag, name, value order. In order the write the tag we need to know 21 | /// what value we're serializing, which we can only do when we serialize the 22 | /// value. So we save the name start serializing the value, which serializes the 23 | /// tag, then the saved name, then the value. 24 | impl Serializer for &mut NameSerializer { 25 | type Ok = (); 26 | type Error = Error; 27 | type SerializeSeq = Impossible<(), Error>; 28 | type SerializeTuple = Impossible<(), Error>; 29 | type SerializeTupleStruct = Impossible<(), Error>; 30 | type SerializeTupleVariant = Impossible<(), Error>; 31 | type SerializeMap = Impossible<(), Error>; 32 | type SerializeStruct = Impossible<(), Error>; 33 | type SerializeStructVariant = Impossible<(), Error>; 34 | 35 | fn serialize_bool(self, _: bool) -> Result { 36 | Err(name_must_be_stringy("bool")) 37 | } 38 | 39 | fn serialize_i8(self, _: i8) -> Result { 40 | Err(name_must_be_stringy("i8")) 41 | } 42 | 43 | fn serialize_i16(self, _: i16) -> Result { 44 | Err(name_must_be_stringy("i16")) 45 | } 46 | 47 | fn serialize_i32(self, _: i32) -> Result { 48 | Err(name_must_be_stringy("i32")) 49 | } 50 | 51 | fn serialize_i64(self, _: i64) -> Result { 52 | Err(name_must_be_stringy("i64")) 53 | } 54 | 55 | fn serialize_u8(self, _: u8) -> Result { 56 | Err(name_must_be_stringy("u8")) 57 | } 58 | 59 | fn serialize_u16(self, _: u16) -> Result { 60 | Err(name_must_be_stringy("u16")) 61 | } 62 | 63 | fn serialize_u32(self, _: u32) -> Result { 64 | Err(name_must_be_stringy("u32")) 65 | } 66 | 67 | fn serialize_u64(self, _: u64) -> Result { 68 | Err(name_must_be_stringy("u64")) 69 | } 70 | 71 | fn serialize_f32(self, _: f32) -> Result { 72 | Err(name_must_be_stringy("f32")) 73 | } 74 | 75 | fn serialize_f64(self, _: f64) -> Result { 76 | Err(name_must_be_stringy("f64")) 77 | } 78 | 79 | fn serialize_char(self, c: char) -> Result { 80 | let mut buf = [0; 4]; 81 | self.serialize_str(c.encode_utf8(&mut buf)) 82 | } 83 | 84 | fn serialize_str(self, v: &str) -> Result { 85 | write_escaped_str(&mut self.name, v) 86 | } 87 | 88 | fn serialize_bytes(self, v: &[u8]) -> Result { 89 | self.name.write_all(v)?; 90 | Ok(()) 91 | } 92 | 93 | fn serialize_none(self) -> Result { 94 | Err(name_must_be_stringy("none")) 95 | } 96 | 97 | fn serialize_some(self, _value: &T) -> Result 98 | where 99 | T: serde::Serialize, 100 | { 101 | Err(name_must_be_stringy("some")) 102 | } 103 | 104 | fn serialize_unit(self) -> Result { 105 | Err(name_must_be_stringy("unit")) 106 | } 107 | 108 | fn serialize_unit_struct(self, _name: &'static str) -> Result { 109 | Err(name_must_be_stringy("unit_struct")) 110 | } 111 | 112 | fn serialize_unit_variant( 113 | self, 114 | _name: &'static str, 115 | _variant_index: u32, 116 | _variant: &'static str, 117 | ) -> Result { 118 | Err(name_must_be_stringy("unit_variant")) 119 | } 120 | 121 | fn serialize_newtype_struct( 122 | self, 123 | _name: &'static str, 124 | _value: &T, 125 | ) -> Result 126 | where 127 | T: serde::Serialize, 128 | { 129 | Err(name_must_be_stringy("newtype_struct")) 130 | } 131 | 132 | fn serialize_newtype_variant( 133 | self, 134 | _: &'static str, 135 | _: u32, 136 | _: &'static str, 137 | _: &T, 138 | ) -> Result 139 | where 140 | T: serde::Serialize, 141 | { 142 | Err(name_must_be_stringy("newtype_variant")) 143 | } 144 | 145 | fn serialize_seq(self, _: Option) -> Result { 146 | Err(name_must_be_stringy("seq")) 147 | } 148 | 149 | fn serialize_tuple(self, _: usize) -> Result { 150 | Err(name_must_be_stringy("tuple")) 151 | } 152 | 153 | fn serialize_tuple_struct( 154 | self, 155 | _: &'static str, 156 | _: usize, 157 | ) -> Result { 158 | Err(name_must_be_stringy("tuple_struct")) 159 | } 160 | 161 | fn serialize_tuple_variant( 162 | self, 163 | _: &'static str, 164 | _: u32, 165 | _: &'static str, 166 | _: usize, 167 | ) -> Result { 168 | Err(name_must_be_stringy("tuple_variant")) 169 | } 170 | 171 | fn serialize_map(self, _: Option) -> Result { 172 | Err(name_must_be_stringy("map")) 173 | } 174 | 175 | fn serialize_struct( 176 | self, 177 | _: &'static str, 178 | _: usize, 179 | ) -> Result { 180 | Err(name_must_be_stringy("struct")) 181 | } 182 | 183 | fn serialize_struct_variant( 184 | self, 185 | _: &'static str, 186 | _: u32, 187 | _: &'static str, 188 | _: usize, 189 | ) -> Result { 190 | Err(name_must_be_stringy("struct_variant")) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /fastsnbt/src/tests/de_tests.rs: -------------------------------------------------------------------------------- 1 | use fastnbt::{ByteArray, IntArray, LongArray}; 2 | use serde::Deserialize; 3 | 4 | use crate::from_str; 5 | 6 | 7 | #[test] 8 | fn test_num() { 9 | let input = "20b"; 10 | let v: i32 = from_str(input).unwrap(); 11 | assert_eq!(20, v); 12 | let input = "20s"; 13 | let v: i32 = from_str(input).unwrap(); 14 | assert_eq!(20, v); 15 | let input = "20l"; 16 | let v: i32 = from_str(input).unwrap(); 17 | assert_eq!(20, v); 18 | let input = "20l"; 19 | let v: u8 = from_str(input).unwrap(); 20 | assert_eq!(20, v); 21 | } 22 | 23 | #[test] 24 | fn test_float() { 25 | let input = "50."; 26 | let f: f64 = from_str(input).unwrap(); 27 | assert_eq!(50., f); 28 | let input = "-5000.e-2f"; 29 | let f: f64 = from_str(input).unwrap(); 30 | assert_eq!(-50., f); 31 | let input = "inf"; 32 | let f: f64 = from_str(input).unwrap(); 33 | assert_eq!(f64::INFINITY, f); 34 | } 35 | 36 | #[test] 37 | fn test_str() { 38 | let input = "\"simple\""; 39 | let s: &str = from_str(input).unwrap(); 40 | assert_eq!("simple", s); 41 | let input = "no+.quo0tes"; 42 | let s: &str = from_str(input).unwrap(); 43 | assert_eq!("no+.quo0tes", s); 44 | let input = "'this\\'is a string'"; 45 | let s: String = from_str(input).unwrap(); 46 | assert_eq!("this\'is a string", s); 47 | let input = "\"yet\\\"ano\\\\\\\"ther\""; 48 | let s: String = from_str(input).unwrap(); 49 | assert_eq!("yet\"ano\\\"ther", s); 50 | assert!(from_str::<&str>("\"not closed").is_err()); 51 | assert!(from_str::<&str>("test/").is_err()); 52 | } 53 | 54 | #[test] 55 | fn test_seq() { 56 | let input = "[1b,2b,3b]"; 57 | let bytes: Vec = from_str(input).unwrap(); 58 | assert_eq!(&[1, 2, 3], bytes.as_slice()); 59 | let input = "[1B,2,5.0e1D]"; 60 | let data: (i8,u64,f64) = from_str(input).unwrap(); 61 | assert_eq!((1, 2, 50.), data); 62 | } 63 | 64 | #[test] 65 | fn test_map() { 66 | #[derive(Debug, Deserialize, PartialEq, Eq)] 67 | struct SimpleStruct<'a> { 68 | s: &'a str, 69 | x: i16, 70 | } 71 | 72 | let input = "{x:-10,s:test}"; 73 | let data: SimpleStruct = from_str(input).unwrap(); 74 | assert_eq!(SimpleStruct { s: "test", x: -10 }, data); 75 | } 76 | 77 | #[test] 78 | fn test_bytearray() { 79 | let input = "[B;1b,-2b,3B]"; 80 | let data: ByteArray = from_str(input).unwrap(); 81 | assert_eq!(ByteArray::new(vec![1,-2,3]), data); 82 | } 83 | 84 | #[test] 85 | fn test_intarray() { 86 | let input = "[I;1,2,-3]"; 87 | let data: IntArray = from_str(input).unwrap(); 88 | assert_eq!(IntArray::new(vec![1,2,-3]), data); 89 | } 90 | 91 | #[test] 92 | fn test_longarray() { 93 | let input = "[L;1l,2L,-3l]"; 94 | let data: LongArray = from_str(input).unwrap(); 95 | assert_eq!(LongArray::new(vec![1,2,-3]), data); 96 | } 97 | -------------------------------------------------------------------------------- /fastsnbt/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use fastnbt::IntArray; 2 | use serde::{Serialize, Deserialize}; 3 | 4 | use crate::{to_string, from_str}; 5 | 6 | mod de_tests; 7 | mod ser_tests; 8 | 9 | #[test] 10 | fn test_mixed() { 11 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 12 | struct MixedStruct { 13 | name: String, 14 | ints: IntArray, 15 | f: f64, 16 | collection: Vec, 17 | } 18 | 19 | let data = MixedStruct { 20 | name: "Cool \"name\"".into(), 21 | ints: IntArray::new(vec![-1, 3, 2000]), 22 | f: -5.0e-40, 23 | collection: vec![true, false, true, true] 24 | }; 25 | let serialized = to_string(&data).unwrap(); 26 | assert_eq!("{\"name\":\"Cool \\\"name\\\"\",\"ints\":[I;-1,3,2000],\"f\":-5e-40,\"collection\":[true,false,true,true]}", serialized); 27 | 28 | let deserialized: MixedStruct = from_str(&serialized).unwrap(); 29 | assert_eq!(deserialized, data); 30 | } 31 | -------------------------------------------------------------------------------- /fastsnbt/src/tests/ser_tests.rs: -------------------------------------------------------------------------------- 1 | use crate::to_string; 2 | use serde::Serialize; 3 | use fastnbt::{ByteArray, IntArray, LongArray}; 4 | 5 | #[test] 6 | fn test_true() { 7 | let snbt = to_string(&true).unwrap(); 8 | assert_eq!("true", snbt); 9 | let snbt = to_string(&false).unwrap(); 10 | assert_eq!("false", snbt); 11 | } 12 | 13 | #[test] 14 | fn test_string_escape() { 15 | let string = "this str \" contains \" quotes"; 16 | let snbt = to_string(string).unwrap(); 17 | assert_eq!("\"this str \\\" contains \\\" quotes\"", snbt); 18 | let string = "str \\\" with \" quotes \\ & backslashes"; 19 | let snbt = to_string(string).unwrap(); 20 | assert_eq!("\"str \\\\\\\" with \\\" quotes \\\\ & backslashes\"", snbt); 21 | } 22 | 23 | #[test] 24 | fn test_byte() { 25 | let byte = 10u8; 26 | let snbt = to_string(&byte).unwrap(); 27 | assert_eq!("10b", snbt); 28 | } 29 | 30 | #[test] 31 | fn test_short() { 32 | let byte = 10u16; 33 | let snbt = to_string(&byte).unwrap(); 34 | assert_eq!("10s", snbt); 35 | } 36 | 37 | #[test] 38 | fn test_int() { 39 | let byte = -10; 40 | let snbt = to_string(&byte).unwrap(); 41 | assert_eq!("-10", snbt); 42 | } 43 | 44 | #[test] 45 | fn test_long() { 46 | let byte = 10u64; 47 | let snbt = to_string(&byte).unwrap(); 48 | assert_eq!("10l", snbt); 49 | } 50 | 51 | #[test] 52 | fn test_float() { 53 | let byte = 10.4f32; 54 | let snbt = to_string(&byte).unwrap(); 55 | assert_eq!("10.4f", snbt); 56 | } 57 | 58 | #[test] 59 | fn test_double() { 60 | let byte = 10.4f64; 61 | let snbt = to_string(&byte).unwrap(); 62 | assert_eq!("10.4", snbt); 63 | } 64 | 65 | #[test] 66 | fn test_char() { 67 | let char = '"'; 68 | let snbt = to_string(&char).unwrap(); 69 | assert_eq!("\"\\\"\"", snbt); 70 | } 71 | 72 | #[test] 73 | fn test_struct() { 74 | #[derive(Serialize)] 75 | struct SimpleStruct<'a> { 76 | x: u8, 77 | y: &'a str, 78 | } 79 | 80 | let data = SimpleStruct { x: 10, y: "test" }; 81 | let snbt = to_string(&data).unwrap(); 82 | assert_eq!("{\"x\":10b,\"y\":\"test\"}", snbt); 83 | } 84 | 85 | #[test] 86 | fn test_normal_array() { 87 | #[derive(Serialize)] 88 | struct ByteStruct { 89 | bytes: Vec, 90 | } 91 | 92 | let data = ByteStruct { bytes: vec![0, 1, 2, 3] }; 93 | let snbt = to_string(&data).unwrap(); 94 | assert_eq!("{\"bytes\":[0b,1b,2b,3b]}", snbt); 95 | } 96 | 97 | #[test] 98 | fn test_bytearray() { 99 | let data = ByteArray::new(vec![-1, 2, -3, 4]); 100 | let snbt = to_string(&data).unwrap(); 101 | assert_eq!("[B;-1b,2b,-3b,4b]", snbt); 102 | } 103 | 104 | #[test] 105 | fn test_intarray() { 106 | let data = IntArray::new(vec![-1, 2, -3, 4]); 107 | let snbt = to_string(&data).unwrap(); 108 | assert_eq!("[I;-1,2,-3,4]", snbt); 109 | } 110 | 111 | #[test] 112 | fn test_longarray() { 113 | let data = LongArray::new(vec![-1, 2, -3, 4]); 114 | let snbt = to_string(&data).unwrap(); 115 | assert_eq!("[L;-1l,2l,-3l,4l]", snbt); 116 | } 117 | 118 | #[test] 119 | fn test_struct_arrays() { 120 | #[derive(Serialize)] 121 | struct ArrayStruct { 122 | bytes: ByteArray, 123 | longs: LongArray, 124 | } 125 | 126 | let data = ArrayStruct { bytes: ByteArray::new(vec![-20,10]), longs: LongArray::new(vec![-40, 10_000]) }; 127 | let snbt = to_string(&data).unwrap(); 128 | assert_eq!("{\"bytes\":[B;-20b,10b],\"longs\":[L;-40l,10000l]}", snbt); 129 | } 130 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fastanvil-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | edition = "2018" 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | libfuzzer-sys = "0.4.3" 13 | serde = {version="1", features=["derive"]} 14 | 15 | [dependencies.fastanvil] 16 | path = "../fastanvil" 17 | [dependencies.fastnbt] 18 | path = "../fastnbt" 19 | features = ["arbitrary1"] 20 | 21 | # Prevent this from interfering with workspaces 22 | [workspace] 23 | members = ["."] 24 | 25 | [[bin]] 26 | name = "deserialize_chunk" 27 | path = "fuzz_targets/deserialize_chunk.rs" 28 | test = false 29 | doc = false 30 | 31 | [[bin]] 32 | name = "deserialize_value" 33 | path = "fuzz_targets/deserialize_value.rs" 34 | test = false 35 | doc = false 36 | 37 | [[bin]] 38 | name = "serialize_value" 39 | path = "fuzz_targets/serialize_value.rs" 40 | test = false 41 | doc = false 42 | 43 | [[bin]] 44 | name = "read_region" 45 | path = "fuzz_targets/read_region.rs" 46 | test = false 47 | doc = false 48 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/deserialize_chunk.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use fastanvil::JavaChunk; 5 | use fastnbt::error::Result; 6 | use fastnbt::from_bytes; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | // fuzzed code goes here 10 | let _chunk: Result = from_bytes(data); 11 | }); 12 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/deserialize_value.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use std::collections::HashMap; 5 | 6 | use fastnbt::error::Result; 7 | use fastnbt::to_bytes; 8 | use fastnbt::Value; 9 | use fastnbt::{from_bytes_with_opts, DeOpts}; 10 | 11 | fuzz_target!(|data: &[u8]| { 12 | let value: Result = from_bytes_with_opts(data, DeOpts::new().max_seq_len(100)); 13 | if let Ok(v) = value { 14 | let mut wrapper = HashMap::new(); 15 | wrapper.insert("wrapper".to_string(), v); 16 | let _bs = to_bytes(&Value::Compound(wrapper)).unwrap(); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/read_region.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use fastanvil::Region; 5 | use std::io::Cursor; 6 | 7 | fuzz_target!(|data: Vec| { 8 | let reader = Cursor::new(data); 9 | let mut r = Region::create(reader); 10 | match r { 11 | Ok(mut r) => { 12 | r.read_chunk(0, 0); 13 | } 14 | Err(_) => {} 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/serialize_value.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use serde::Serialize; 5 | use std::collections::HashMap; 6 | 7 | use fastnbt::error::Result; 8 | use fastnbt::from_bytes; 9 | use fastnbt::to_bytes; 10 | use fastnbt::Value; 11 | 12 | fuzz_target!(|v: Value| { 13 | let mut inner = HashMap::new(); 14 | inner.insert("".to_string(), v); 15 | 16 | let v = Value::Compound(inner); 17 | let bs = to_bytes(&v); 18 | 19 | if let Ok(bs) = bs { 20 | let _: Result = from_bytes(&bs); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /palette.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owengage/fastnbt/e2a5d8a7001d4f074ae99fd21bb485667934baeb/palette.tar.gz -------------------------------------------------------------------------------- /tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fastnbt-tools" 3 | description = "tools built with fastnbt" 4 | version = "0.27.0" 5 | authors = ["Owen Gage "] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [dependencies] 10 | fastsnbt = { path = "../fastsnbt", version = "0.2.0" } 11 | fastnbt = { path = "../fastnbt", version = "2" } 12 | fastanvil = { path = "../fastanvil", version = "0.31" } 13 | rayon = "1.3.0" 14 | flate2 = "1.0" 15 | image = "0.23.4" 16 | clap = "2.33.1" 17 | regex = "1" 18 | serde_json = "1.0" 19 | serde = { version = "1.0.111", features = ["derive"] } 20 | tar = "0.4" 21 | log = "0.4" 22 | env_logger = "0.9" 23 | -------------------------------------------------------------------------------- /tools/src/bin/anvil-palette-swap.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs::File}; 2 | 3 | use clap::{App, Arg}; 4 | use fastanvil::Region; 5 | use fastnbt::Value; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | struct Chunk { 10 | sections: Vec
, 11 | 12 | #[serde(flatten)] 13 | other: HashMap, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | struct Section { 18 | block_states: Blockstates, 19 | #[serde(flatten)] 20 | other: HashMap, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | struct Blockstates { 25 | palette: Vec, 26 | #[serde(flatten)] 27 | other: HashMap, 28 | } 29 | 30 | #[derive(Serialize, Deserialize)] 31 | struct PaletteItem { 32 | #[serde(rename = "Name")] 33 | name: String, 34 | #[serde(rename = "Properties")] 35 | properties: Option, 36 | } 37 | 38 | fn main() { 39 | let matches = App::new("anvil-palette-swap") 40 | .arg(Arg::with_name("region").required(true)) 41 | .arg( 42 | Arg::with_name("from") 43 | .long("from") 44 | .short("f") 45 | .takes_value(true) 46 | .required(true) 47 | .help("blockstate to transform from, eg minecraft:oak_leaves"), 48 | ) 49 | .arg( 50 | Arg::with_name("out-file") 51 | .long("out-file") 52 | .short("o") 53 | .takes_value(true) 54 | .required(true) 55 | .help("full path to write the resulting region file"), 56 | ) 57 | .arg( 58 | Arg::with_name("to") 59 | .long("to") 60 | .short("t") 61 | .takes_value(true) 62 | .required(true) 63 | .help("blockstate to transform to, eg minecraft:diamond_block"), 64 | ) 65 | .get_matches(); 66 | 67 | let region = matches.value_of_os("region").unwrap(); 68 | let out_path = matches.value_of_os("out-file").unwrap(); 69 | let from = matches.value_of("from").unwrap(); 70 | let to = matches.value_of("to").unwrap(); 71 | 72 | let region = File::open(region).unwrap(); 73 | let mut region = Region::from_stream(region).unwrap(); 74 | 75 | let out_file = File::options() 76 | .read(true) 77 | .write(true) 78 | .create_new(true) 79 | .open(out_path) 80 | .unwrap(); 81 | 82 | let mut new_region = Region::create(out_file).unwrap(); 83 | 84 | for z in 0..32 { 85 | for x in 0..32 { 86 | match region.read_chunk(x, z) { 87 | Ok(Some(data)) => { 88 | let mut chunk: Chunk = fastnbt::from_bytes(&data).unwrap(); 89 | for section in chunk.sections.iter_mut() { 90 | let palette: &mut Vec = &mut section.block_states.palette; 91 | for item in palette { 92 | if item.name == from { 93 | item.name = to.to_owned(); 94 | } 95 | } 96 | } 97 | let ser = fastnbt::to_bytes(&chunk).unwrap(); 98 | new_region.write_chunk(x, z, &ser).unwrap(); 99 | } 100 | Ok(None) => {} 101 | Err(e) => eprintln!("{e}"), 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tools/src/bin/anvil-palette.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, path::Path}; 2 | 3 | use fastnbt_tools::make_palette; 4 | 5 | type Result = std::result::Result>; 6 | 7 | fn main() -> Result<()> { 8 | let args: Vec<_> = std::env::args().skip(1).collect(); 9 | make_palette(Path::new(&args[0]))?; 10 | 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /tools/src/bin/flatten-model.rs: -------------------------------------------------------------------------------- 1 | use fastanvil::tex::{Model, Renderer}; 2 | use std::error::Error; 3 | use std::path::Path; 4 | use std::{collections::HashMap, fmt::Display}; 5 | 6 | type Result = std::result::Result>; 7 | 8 | #[derive(Debug)] 9 | struct ErrorMessage(&'static str); 10 | impl std::error::Error for ErrorMessage {} 11 | 12 | impl Display for ErrorMessage { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | write!(f, "{}", self.0) 15 | } 16 | } 17 | 18 | fn load_models(path: &Path) -> Result> { 19 | let mut models = HashMap::::new(); 20 | 21 | for entry in std::fs::read_dir(path)? { 22 | let entry = entry?; 23 | let path = entry.path(); 24 | 25 | if path.is_file() { 26 | let json = std::fs::read_to_string(&path)?; 27 | 28 | let json: Model = serde_json::from_str(&json)?; 29 | models.insert( 30 | "minecraft:block/".to_owned() 31 | + path 32 | .file_stem() 33 | .ok_or(format!("invalid file name: {}", path.display()))? 34 | .to_str() 35 | .ok_or(format!("nonunicode file name: {}", path.display()))?, 36 | json, 37 | ); 38 | } 39 | } 40 | 41 | Ok(models) 42 | } 43 | 44 | fn main() -> Result<()> { 45 | let args: Vec<_> = std::env::args().skip(1).collect(); 46 | let root = Path::new(&args[0]); 47 | let assets = root.to_owned().join("assets").join("minecraft"); 48 | 49 | let models = load_models(&assets.join("models").join("block"))?; 50 | 51 | let renderer = Renderer::new(HashMap::new(), models.clone(), HashMap::new()); 52 | let mut failed = 0; 53 | let mut success = 0; 54 | 55 | for name in models 56 | .keys() 57 | .filter(|name| args.get(1).map(|s| s == *name).unwrap_or(true)) 58 | { 59 | let res = renderer.flatten_model(name); 60 | match res { 61 | Ok(model) => { 62 | println!("{}: {:#?}", name, model); 63 | success += 1; 64 | } 65 | Err(e) => { 66 | println!("{:?}", e); 67 | failed += 1; 68 | } 69 | }; 70 | } 71 | 72 | println!("success {:?}, failed {:?}", success, failed); 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /tools/src/bin/nbt-dump-gz.rs: -------------------------------------------------------------------------------- 1 | use fastnbt::stream::{Parser, Value}; 2 | use flate2::read::GzDecoder; 3 | use std::io; 4 | 5 | fn main() { 6 | let stdin = io::stdin(); 7 | let decoder = GzDecoder::new(stdin); 8 | 9 | let mut parser = Parser::new(decoder); 10 | let mut indent = 0; 11 | 12 | loop { 13 | match parser.next() { 14 | Err(e) => { 15 | println!("{:?}", e); 16 | break; 17 | } 18 | Ok(value) => { 19 | match value { 20 | Value::CompoundEnd => indent -= 4, 21 | Value::ListEnd => indent -= 4, 22 | _ => {} 23 | } 24 | 25 | println!("{:indent$}{:?}", "", value, indent = indent); 26 | 27 | match value { 28 | Value::Compound(_) => indent += 4, 29 | Value::List(_, _, _) => indent += 4, 30 | _ => {} 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tools/src/bin/nbt-dump-raw.rs: -------------------------------------------------------------------------------- 1 | use fastnbt::stream::{Parser, Value}; 2 | use std::io; 3 | 4 | fn main() { 5 | let stdin = io::stdin(); 6 | let mut parser = Parser::new(stdin); 7 | let mut indent = 0; 8 | 9 | loop { 10 | match parser.next() { 11 | Err(e) => { 12 | println!("{:?}", e); 13 | break; 14 | } 15 | Ok(value) => { 16 | match value { 17 | Value::CompoundEnd => indent -= 4, 18 | Value::ListEnd => indent -= 4, 19 | _ => {} 20 | } 21 | 22 | println!("{:indent$}{:?}", "", value, indent = indent); 23 | 24 | match value { 25 | Value::Compound(_) => indent += 4, 26 | Value::List(_, _, _) => indent += 4, 27 | _ => {} 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tools/src/bin/region-dump.rs: -------------------------------------------------------------------------------- 1 | use core::panic; 2 | use std::{ 3 | error::Error, 4 | fs::{create_dir, File}, 5 | io::{self, Write}, 6 | }; 7 | 8 | use clap::{App, Arg}; 9 | use env_logger::Env; 10 | use fastanvil::Region; 11 | use fastnbt::Value; 12 | 13 | fn main() -> Result<(), Box> { 14 | env_logger::Builder::from_env(Env::default().default_filter_or("info")) 15 | .format_timestamp(None) 16 | .init(); 17 | 18 | let matches = App::new("region-dump") 19 | .arg(Arg::with_name("file").required(true)) 20 | .arg( 21 | Arg::with_name("format") 22 | .long("format") 23 | .short("f") 24 | .takes_value(true) 25 | .required(false) 26 | .default_value("rust") 27 | .possible_values(&["rust", "rust-pretty", "json", "json-pretty", "nbt", "snbt"]) 28 | .help("output format"), 29 | ) 30 | .arg( 31 | Arg::with_name("out-dir") 32 | .long("out-dir") 33 | .short("o") 34 | .takes_value(true) 35 | .required(false) 36 | .help("optionally separate each chunk into a file in the specified directory"), 37 | ) 38 | .get_matches(); 39 | 40 | let file = matches.value_of("file").expect("file is required"); 41 | let file = File::open(file).expect("file does not exist"); 42 | let output_format = matches 43 | .value_of("format") 44 | .expect("no output format specified"); 45 | let out_dir = matches.value_of("out-dir"); 46 | 47 | let mut region = Region::from_stream(file).unwrap(); 48 | 49 | if let Some(dir) = out_dir { 50 | create_dir(dir).unwrap_or_default(); 51 | } 52 | 53 | for z in 0..32 { 54 | for x in 0..32 { 55 | match region.read_chunk(x, z) { 56 | Ok(Some(data)) => { 57 | if !should_output_chunk(&data) { 58 | continue; 59 | } 60 | 61 | let mut out: Box = if let Some(dir) = out_dir { 62 | let ext = match output_format { 63 | "nbt" => "nbt", 64 | "json" | "json-pretty" => "json", 65 | _ => "txt", 66 | }; 67 | Box::new(File::create(format!("{}/{}.{}.{}", dir, x, z, ext)).unwrap()) 68 | } else { 69 | Box::new(io::stdout()) 70 | }; 71 | 72 | let chunk: Value = fastnbt::from_bytes(&data).unwrap(); 73 | 74 | match output_format { 75 | "rust" => { 76 | write!(&mut out, "{:?}", chunk).unwrap(); 77 | } 78 | "rust-pretty" => { 79 | write!(&mut out, "{:#?}", chunk).unwrap(); 80 | } 81 | "nbt" => { 82 | out.write_all(&data).unwrap(); 83 | } 84 | "json" => { 85 | serde_json::ser::to_writer(out, &chunk).unwrap(); 86 | } 87 | "json-pretty" => { 88 | serde_json::ser::to_writer_pretty(out, &chunk).unwrap(); 89 | } 90 | "snbt" => { 91 | let s = fastsnbt::to_string(&chunk).unwrap(); 92 | out.write_all(s.as_bytes()).unwrap(); 93 | } 94 | _ => panic!("unknown output format '{}'", output_format), 95 | } 96 | } 97 | Ok(None) => {} 98 | Err(e) => return Err(e.into()), 99 | } 100 | } 101 | } 102 | Ok(()) 103 | } 104 | 105 | fn should_output_chunk(_data: &[u8]) -> bool { 106 | // If you're trying to locate a misbehaving chunk, you can filter out chunks here. 107 | // let chunk = JavaChunk::from_bytes(data).unwrap(); 108 | true 109 | } 110 | --------------------------------------------------------------------------------