online Minecraft written book viewer

feat(1.12): a lot of stuff

kokirigla.de 36623306 608831c6

verified
+417 -1
+1
.gitignore
··· 1 1 /target 2 2 3 + # my local input data 3 4 /worlds 4 5 realm.nbt
+31
Cargo.lock
··· 1037 1037 ] 1038 1038 1039 1039 [[package]] 1040 + name = "nara_1_12" 1041 + version = "0.1.0" 1042 + dependencies = [ 1043 + "nara_text", 1044 + "serde", 1045 + ] 1046 + 1047 + [[package]] 1048 + name = "nara_io" 1049 + version = "0.1.0" 1050 + dependencies = [ 1051 + "crab_nbt", 1052 + "flate2", 1053 + "serde", 1054 + "thiserror", 1055 + ] 1056 + 1057 + [[package]] 1058 + name = "nara_slurper_1_12_world" 1059 + version = "0.1.0" 1060 + dependencies = [ 1061 + "crab_nbt", 1062 + "insta", 1063 + "nara_1_12", 1064 + "nara_io", 1065 + "nara_text", 1066 + "serde", 1067 + "serde_with", 1068 + ] 1069 + 1070 + [[package]] 1040 1071 name = "nara_text" 1041 1072 version = "0.1.0" 1042 1073 dependencies = [
+6 -1
Cargo.toml
··· 1 1 [workspace] 2 2 resolver = "3" 3 - members = ["nara_text"] 3 + members = ["nara_io", "nara_1_12", "nara_slurper_1_12_world", "nara_text"] 4 4 5 5 [workspace.package] 6 6 version = "0.1.0" ··· 14 14 axum = "=0.8.8" 15 15 clap = { version = "=4.5.60", features = ["derive"] } 16 16 crab_nbt = { version = "=0.2.11", features = ["serde"] } 17 + flate2 = "=1.1.9" 17 18 hex = "=0.4.3" 18 19 html-escape = "=0.2.13" 19 20 insta = "=1.46.3" 20 21 lru = "=0.16.3" 22 + nara_1_12 = { path = "nara_1_12" } 23 + nara_io = { path = "nara_io" } 24 + nara_slurper_1_12_world = { path = "nara_slurper_1_12_world" } 25 + nara_text = { path = "nara_text" } 21 26 serde = { version = "=1.0.228", features = ["derive"] } 22 27 serde_json = "=1.0.149" 23 28 serde_with = "=3.17.0"
+8
nara_1_12/Cargo.toml
··· 1 + [package] 2 + name = "nara_1_12" 3 + version.workspace = true 4 + edition.workspace = true 5 + 6 + [dependencies] 7 + nara_text.workspace = true 8 + serde.workspace = true
+3
nara_1_12/README.md
··· 1 + # nara_1_12 2 + 3 + Shared code for 1.12.2 version handling.
+132
nara_1_12/src/item.rs
··· 1 + use nara_text::Component; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, Deserialize)] 5 + #[serde(untagged)] 6 + pub enum ItemStack { 7 + WrittenBook(WrittenBookStack), 8 + BlockEntity(BlockEntityStack), 9 + Base(BaseItemStack), 10 + } 11 + 12 + #[derive(Debug, Deserialize)] 13 + #[serde(rename_all = "PascalCase")] 14 + pub struct BaseItemStack { 15 + pub count: i8, 16 + pub damage: i16, 17 + #[serde(rename = "id")] 18 + pub id: String, 19 + } 20 + 21 + #[derive(Debug, Deserialize)] 22 + #[serde(rename_all = "PascalCase")] 23 + pub struct DisplayTag { 24 + pub name: String, 25 + } 26 + 27 + #[derive(Debug, Deserialize)] 28 + pub struct BaseTag { 29 + #[serde(rename = "display")] 30 + pub display: Option<DisplayTag>, 31 + } 32 + 33 + #[derive(Debug, Deserialize)] 34 + pub struct WrittenBookStack { 35 + #[serde(flatten)] 36 + pub base: BaseItemStack, 37 + pub tag: WrittenBookTag, 38 + } 39 + 40 + #[derive(Debug, Deserialize)] 41 + pub struct WrittenBookTag { 42 + #[serde(flatten)] 43 + pub base: BaseTag, 44 + pub author: String, 45 + pub title: String, 46 + #[serde(default)] 47 + pub generation: BookGeneration, 48 + #[serde(default)] 49 + pub resolved: bool, 50 + pub pages: Vec<Component>, 51 + } 52 + 53 + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] 54 + #[serde(try_from = "i32")] 55 + pub enum BookGeneration { 56 + #[default] 57 + Original, 58 + CopyOfOriginal, 59 + CopyOfCopy, 60 + Tattered, 61 + } 62 + 63 + impl TryFrom<i32> for BookGeneration { 64 + type Error = i32; 65 + 66 + fn try_from(value: i32) -> Result<Self, Self::Error> { 67 + match value { 68 + 0 => Ok(Self::Original), 69 + 1 => Ok(Self::CopyOfOriginal), 70 + 2 => Ok(Self::CopyOfCopy), 71 + 3 => Ok(Self::Tattered), 72 + other => Err(other), 73 + } 74 + } 75 + } 76 + 77 + #[derive(Debug, Deserialize)] 78 + pub struct BlockEntityStack { 79 + #[serde(flatten)] 80 + pub base: BaseItemStack, 81 + pub tag: BlockEntityTag, 82 + } 83 + 84 + #[derive(Debug, Deserialize)] 85 + pub struct BlockEntityTag { 86 + #[serde(flatten)] 87 + pub base: BaseTag, 88 + #[serde(rename = "BlockEntityTag")] 89 + pub block_entity: BlockEntity, 90 + } 91 + 92 + #[derive(Debug, Deserialize)] 93 + #[serde(rename_all = "PascalCase")] 94 + pub struct BlockEntity { 95 + #[serde(default)] 96 + pub items: Vec<InventoryItemStack>, 97 + } 98 + 99 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 100 + pub enum BlockEntityKind { 101 + ShulkerBox, 102 + Chest, 103 + TrappedChest, 104 + Hopper, 105 + Dropper, 106 + Dispenser, 107 + Barrel, 108 + Other, 109 + } 110 + 111 + impl BlockEntityStack { 112 + pub fn kind(&self) -> BlockEntityKind { 113 + match self.base.id.as_str() { 114 + id if id.contains("shulker_box") => BlockEntityKind::ShulkerBox, 115 + "minecraft:chest" => BlockEntityKind::Chest, 116 + "minecraft:trapped_chest" => BlockEntityKind::TrappedChest, 117 + "minecraft:hopper" => BlockEntityKind::Hopper, 118 + "minecraft:dropper" => BlockEntityKind::Dropper, 119 + "minecraft:dispenser" => BlockEntityKind::Dispenser, 120 + "minecraft:barrel" => BlockEntityKind::Barrel, 121 + _ => BlockEntityKind::Other, 122 + } 123 + } 124 + } 125 + 126 + #[derive(Debug, Deserialize)] 127 + #[serde(rename_all = "PascalCase")] 128 + pub struct InventoryItemStack { 129 + #[serde(flatten)] 130 + pub item: ItemStack, 131 + pub slot: i8, 132 + }
+1
nara_1_12/src/lib.rs
··· 1 + pub mod item;
+10
nara_io/Cargo.toml
··· 1 + [package] 2 + name = "nara_io" 3 + version.workspace = true 4 + edition.workspace = true 5 + 6 + [dependencies] 7 + crab_nbt.workspace = true 8 + flate2.workspace = true 9 + serde.workspace = true 10 + thiserror.workspace = true
+52
nara_io/src/lib.rs
··· 1 + use flate2::read::{GzDecoder, ZlibDecoder}; 2 + use serde::Deserialize; 3 + use std::io::{Cursor, Read}; 4 + 5 + #[derive(Debug, thiserror::Error)] 6 + pub enum Error { 7 + #[error(transparent)] 8 + Io(#[from] std::io::Error), 9 + #[error(transparent)] 10 + Nbt(#[from] crab_nbt::error::Error), 11 + } 12 + 13 + pub type Result<T> = std::result::Result<T, Error>; 14 + 15 + pub fn read_nbt<T>(src: &[u8]) -> Result<T> 16 + where 17 + T: for<'de> Deserialize<'de>, 18 + { 19 + let decomp = maybe_decompress(src)?; 20 + let mut cur = Cursor::new(decomp.as_slice()); 21 + 22 + Ok(crab_nbt::serde::de::from_cursor::<T>(&mut cur)?) 23 + } 24 + 25 + pub fn maybe_decompress(src: &[u8]) -> Result<Vec<u8>> { 26 + let mut src_cursor = Cursor::new(src); 27 + 28 + src_cursor.set_position(0); 29 + let mut header = [0u8; 2]; 30 + src_cursor.read_exact(&mut header)?; 31 + 32 + let mut decoded = Vec::new(); 33 + match &header { 34 + [0x1f, 0x8b] => { 35 + src_cursor.set_position(0); 36 + GzDecoder::new(src_cursor).read_to_end(&mut decoded)?; 37 + } 38 + [0x78, _b @ 0x01..=0xda] 39 + if (u16::from(header[0]) * 256 + u16::from(header[1])) % 31 40 + == 0 => 41 + { 42 + src_cursor.set_position(0); 43 + ZlibDecoder::new(src_cursor).read_to_end(&mut decoded)?; 44 + } 45 + _ => { 46 + src_cursor.set_position(0); 47 + src_cursor.read_to_end(&mut decoded)?; 48 + } 49 + } 50 + 51 + Ok(decoded) 52 + }
+15
nara_slurper_1_12_world/Cargo.toml
··· 1 + [package] 2 + name = "nara_slurper_1_12_world" 3 + version.workspace = true 4 + edition.workspace = true 5 + 6 + [dependencies] 7 + crab_nbt.workspace = true 8 + nara_1_12.workspace = true 9 + nara_io.workspace = true 10 + nara_text.workspace = true 11 + serde.workspace = true 12 + serde_with.workspace = true 13 + 14 + [dev-dependencies] 15 + insta.workspace = true
+17
nara_slurper_1_12_world/README.md
··· 1 + # nara_slurper_1_12_world 2 + 3 + Pulls book data from everywhere it possibly can from Minecraft: Java Edition 4 + worlds saved in 1.12.2's world format. 5 + 6 + Checks: 7 + 8 + - playerdata 9 + - inventory 10 + - ender chest 11 + - entitydata 12 + - item frames 13 + - block entity data 14 + - chests 15 + - shulker boxes 16 + - dispensers 17 + - etc.
+16
nara_slurper_1_12_world/src/lib.rs
··· 1 + use nara_1_12::item::InventoryItemStack; 2 + use serde::Deserialize; 3 + 4 + // the shit we care about from playerdata.dat files :^) 5 + #[derive(Debug, Deserialize)] 6 + #[serde(rename_all = "PascalCase")] 7 + pub struct PlayerData { 8 + #[serde(default, skip_serializing_if = "Vec::is_empty")] 9 + pub inventory: Vec<InventoryItemStack>, 10 + #[serde( 11 + default, 12 + rename = "EnderItems", 13 + skip_serializing_if = "Vec::is_empty" 14 + )] 15 + pub ender_chest: Vec<InventoryItemStack>, 16 + }
+9
nara_slurper_1_12_world/tests/data.rs
··· 1 + use nara_slurper_1_12_world::PlayerData; 2 + 3 + const PLAYER_DATA_FIXTURE: &[u8] = include_bytes!("fixtures/playerdata.dat"); 4 + 5 + #[test] 6 + fn playerdata() { 7 + let parsed = nara_io::read_nbt::<PlayerData>(PLAYER_DATA_FIXTURE); 8 + insta::assert_debug_snapshot!(parsed); 9 + }
nara_slurper_1_12_world/tests/fixtures/playerdata.dat

This is a binary file and will not be displayed.

+114
nara_slurper_1_12_world/tests/snapshots/data__playerdata.snap
··· 1 + --- 2 + source: nara_slurper_1_12_world/tests/data.rs 3 + expression: parsed 4 + --- 5 + Ok( 6 + PlayerData { 7 + inventory: [ 8 + InventoryItemStack { 9 + item: WrittenBook( 10 + WrittenBookStack { 11 + base: BaseItemStack { 12 + count: 1, 13 + damage: 0, 14 + id: "minecraft:written_book", 15 + }, 16 + tag: WrittenBookTag { 17 + base: BaseTag { 18 + display: None, 19 + }, 20 + author: "kokiriglade", 21 + title: "just a book", 22 + generation: Original, 23 + resolved: false, 24 + pages: [ 25 + String( 26 + "{\"text\":\"Hello, World!\\n\\n§cThis is red text!§r\\n\\n§9This is blue :^)§r§r\"}", 27 + ), 28 + ], 29 + }, 30 + }, 31 + ), 32 + slot: 0, 33 + }, 34 + InventoryItemStack { 35 + item: BlockEntity( 36 + BlockEntityStack { 37 + base: BaseItemStack { 38 + count: 1, 39 + damage: 0, 40 + id: "minecraft:pink_shulker_box", 41 + }, 42 + tag: BlockEntityTag { 43 + base: BaseTag { 44 + display: None, 45 + }, 46 + block_entity: BlockEntity { 47 + items: [ 48 + InventoryItemStack { 49 + item: WrittenBook( 50 + WrittenBookStack { 51 + base: BaseItemStack { 52 + count: 1, 53 + damage: 0, 54 + id: "minecraft:written_book", 55 + }, 56 + tag: WrittenBookTag { 57 + base: BaseTag { 58 + display: None, 59 + }, 60 + author: "kokiriglade", 61 + title: "in a shulker box", 62 + generation: Original, 63 + resolved: false, 64 + pages: [ 65 + String( 66 + "{\"text\":\"this one is in a shulker box!! wow!!!\"}", 67 + ), 68 + ], 69 + }, 70 + }, 71 + ), 72 + slot: 2, 73 + }, 74 + ], 75 + }, 76 + }, 77 + }, 78 + ), 79 + slot: 1, 80 + }, 81 + InventoryItemStack { 82 + item: Base( 83 + BaseItemStack { 84 + count: 1, 85 + damage: 0, 86 + id: "minecraft:stone", 87 + }, 88 + ), 89 + slot: 2, 90 + }, 91 + InventoryItemStack { 92 + item: Base( 93 + BaseItemStack { 94 + count: 1, 95 + damage: 0, 96 + id: "minecraft:reeds", 97 + }, 98 + ), 99 + slot: 4, 100 + }, 101 + InventoryItemStack { 102 + item: Base( 103 + BaseItemStack { 104 + count: 1, 105 + damage: 0, 106 + id: "minecraft:golden_chestplate", 107 + }, 108 + ), 109 + slot: 5, 110 + }, 111 + ], 112 + ender_chest: [], 113 + }, 114 + )
+2
nara_text/src/lib.rs
··· 3 3 pub mod color; 4 4 pub mod profile; 5 5 6 + pub const LEGACY_SYMBOL: char = '§'; 7 + 6 8 #[derive(Debug, Serialize, Deserialize)] 7 9 #[serde(untagged)] 8 10 pub enum Component {