diff --git a/examples/basic/config.toml b/examples/basic/config.toml index abb3b49..f62e40d 100644 --- a/examples/basic/config.toml +++ b/examples/basic/config.toml @@ -9,12 +9,11 @@ compression-threshold = 64 # Packet compression level 0..=9 (0 is no compression) compression-level = 3 -# world = "/home/vulae/.local/share/PrismLauncher/instances/Fabulously Optimized 1.21.4/.minecraft/saves/pkmc/" # NOTE: If you are loading an old world (<=1.19), you will want to optimize it first, or it will not load. # Worlds > Edit World > Optimize World > Confirm (And wait (-.-)...Zzz) # If it is a massive world you're optimizing, it's very likely you'll want to create a tmpfs directory for that folder. # As Minecraft is extremely slow at doing this, and probably will be IO-limited (For some stupid reason, IDK) -world = "/home/vulae/.local/share/PrismLauncher/instances/1.21.5 temp/minecraft/saves/pkmc/" +world = "/home/vulae/.local/share/PrismLauncher/instances/Fabulously Optimized 1.21.5/.minecraft/saves/pkmc/" view-distance = 32 entity-distance = 256.0 diff --git a/examples/basic/src/player.rs b/examples/basic/src/player.rs index 38676c3..1b09676 100644 --- a/examples/basic/src/player.rs +++ b/examples/basic/src/player.rs @@ -1,24 +1,23 @@ use std::sync::{Arc, Mutex}; use pkmc_defs::{ - block::Block, packet::{self, play::EntityAnimationType}, particle::Particle, text_component::TextComponent, }; -use pkmc_generated::registry::EntityType; +use pkmc_generated::{block::Block, registry::EntityType}; use pkmc_server::{ - entity_manager::{new_entity_id, Entity, EntityBase, EntityViewer}, + entity_manager::{Entity, EntityBase, EntityViewer, new_entity_id}, tab_list::{TabListPlayer, TabListViewer}, - world::{anvil::AnvilError, World as _, WorldBlock, WorldViewer}, + world::{World as _, WorldViewer, anvil::AnvilError}, }; use pkmc_util::{ + Color, Position, UUID, Vec3, connection::{Connection, ConnectionError}, - Color, Position, Vec3, UUID, }; use thiserror::Error; -use crate::server::{ServerState, REGISTRIES}; +use crate::server::{REGISTRIES, ServerState}; const KEEPALIVE_PING_TIME: std::time::Duration = std::time::Duration::from_millis(10000); @@ -352,7 +351,7 @@ impl Player { .get_block(*p) .ok() .flatten() - .map(|b| !b.as_block().is_air()) + .map(|b| !b.is_air()) .unwrap_or(false) }) { //self.connection.send(&packet::play::LevelParticles { @@ -368,9 +367,8 @@ impl Player { // particle_count: 64, // particle: Particle::ExplosionEmitter, //})?; - Position::iter_offset(Position::iter_sphere(32.0), position).try_for_each( - |p| world.set_block(p, WorldBlock::Block(Block::air())), - )?; + Position::iter_offset(Position::iter_sphere(48.0), position) + .try_for_each(|p| world.set_block(p, Block::Air))?; } } packet::play::PlayPacket::ChatMessage(chat_message) => { diff --git a/pkmc-defs/src/block.rs b/pkmc-defs/src/block.rs index f39a814..eb0f574 100644 --- a/pkmc-defs/src/block.rs +++ b/pkmc-defs/src/block.rs @@ -1,14 +1,17 @@ use std::{collections::BTreeMap, sync::LazyLock}; -use pkmc_generated::{block::BLOCKS_REPORT, registry::BlockEntityType}; +use pkmc_generated::{ + block::{Block, BLOCKS_REPORT}, + registry::BlockEntityType, +}; use pkmc_util::{nbt::NBT, IdTable}; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, Default)] #[serde(transparent)] -pub struct BlockProperties(BTreeMap); +pub struct DynamicBlockProperties(BTreeMap); -impl BlockProperties { +impl DynamicBlockProperties { pub fn new() -> Self { Self(BTreeMap::new()) } @@ -38,9 +41,9 @@ impl BlockProperties { } } -impl> From for BlockProperties { +impl> From for DynamicBlockProperties { fn from(value: I) -> Self { - BlockProperties( + DynamicBlockProperties( value .into_iter() .map(|(k, v)| (k.to_string(), v.to_string())) @@ -50,15 +53,15 @@ impl> From for Block } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct Block { +pub struct DynamicBlock { #[serde(alias = "Name")] pub name: String, #[serde(alias = "Properties", default)] - pub properties: BlockProperties, + pub properties: DynamicBlockProperties, } -impl Block { - pub fn new_p>(name: N, properties: P) -> Self { +impl DynamicBlock { + pub fn new_p>(name: N, properties: P) -> Self { Self { name: name.to_string(), properties: properties.into(), @@ -73,44 +76,30 @@ impl Block { Self::new(&self.name) } - pub fn air() -> Self { - Self::new("minecraft:air") - } - - pub fn is_air(&self) -> bool { - //matches!( - // self.name.as_ref(), - // "minecraft:air" | "minecraft:cave_air" | "minecraft:void_air" - //) - self.id() - .map(pkmc_generated::block::is_air) - .unwrap_or(false) - } - - pub fn id(&self) -> Option { - BLOCKS_TO_IDS.get(self).copied() - } - - pub fn id_with_default_fallback(&self) -> Option { - self.id().or_else(|| self.without_properties().id()) + pub fn to_block(&self) -> Option { + BLOCKS_TO_IDS + .get(self) + .or_else(|| BLOCKS_TO_IDS.get(&self.without_properties())) + .copied() + .and_then(Block::from_id) } } -impl Default for Block { +impl Default for DynamicBlock { fn default() -> Self { - Self::air() + Self::new("minecraft:air") } } #[derive(Debug, Clone, PartialEq)] pub struct BlockEntity { - pub block: Block, + pub block: DynamicBlock, pub r#type: BlockEntityType, pub data: NBT, } impl BlockEntity { - pub fn new(block: Block, r#type: BlockEntityType, data: NBT) -> Self { + pub fn new(block: DynamicBlock, r#type: BlockEntityType, data: NBT) -> Self { Self { block, r#type, @@ -118,19 +107,19 @@ impl BlockEntity { } } - pub fn into_block(self) -> Block { + pub fn into_block(self) -> DynamicBlock { self.block } } -pub static BLOCKS_TO_IDS: LazyLock> = LazyLock::new(|| { +pub static BLOCKS_TO_IDS: LazyLock> = LazyLock::new(|| { let mut blocks_to_ids = IdTable::new(); BLOCKS_REPORT.0.iter().for_each(|(name, block)| { block.states.iter().for_each(|state| { if state.default { - blocks_to_ids.insert(Block::new(name), state.id); + blocks_to_ids.insert(DynamicBlock::new(name), state.id); } - blocks_to_ids.insert(Block::new_p(name, state.properties.iter()), state.id); + blocks_to_ids.insert(DynamicBlock::new_p(name, state.properties.iter()), state.id); }); }); blocks_to_ids @@ -138,13 +127,20 @@ pub static BLOCKS_TO_IDS: LazyLock> = LazyLock::new(|| { #[cfg(test)] mod test { - use crate::block::{Block, BLOCKS_TO_IDS}; + use crate::block::{DynamicBlock, BLOCKS_TO_IDS}; #[test] fn test_blocks_to_ids() { - assert_eq!(BLOCKS_TO_IDS.get(&Block::air()).copied(), Some(0)); assert_eq!( - BLOCKS_TO_IDS.get(&Block::new("minecraft:stone")).copied(), + BLOCKS_TO_IDS + .get(&DynamicBlock::new("minecraft:air")) + .copied(), + Some(0) + ); + assert_eq!( + BLOCKS_TO_IDS + .get(&DynamicBlock::new("minecraft:stone")) + .copied(), Some(1) ); } diff --git a/pkmc-defs/src/packet/play.rs b/pkmc-defs/src/packet/play.rs index 56f9191..9ae0469 100644 --- a/pkmc-defs/src/packet/play.rs +++ b/pkmc-defs/src/packet/play.rs @@ -3,6 +3,10 @@ use std::{ io::{Read, Write}, }; +use pkmc_generated::{ + block::Block, + registry::{BlockEntityType, EntityType}, +}; use pkmc_util::{ connection::{ paletted_container::to_paletted_data_singular, ClientboundPacket, ConnectionError, @@ -395,7 +399,7 @@ pub struct BlockEntity { /// u4 pub z: u8, pub y: i16, - pub r#type: i32, + pub r#type: BlockEntityType, pub data: NBT, } @@ -412,7 +416,7 @@ pub enum LevelChunkHeightmapType { } impl LevelChunkHeightmapType { - fn to_id(&self) -> i32 { + fn to_id(self) -> i32 { match self { LevelChunkHeightmapType::WorldSurfaceWorldgen => 0, LevelChunkHeightmapType::WorldSurface => 1, @@ -475,7 +479,7 @@ impl LevelChunkData { debug_assert!(block_entity.z <= 15); writer.write_all(&((block_entity.x << 4) | block_entity.z).to_be_bytes())?; writer.write_all(&block_entity.y.to_be_bytes())?; - writer.encode(block_entity.r#type)?; + writer.encode(block_entity.r#type.to_id())?; writer.encode(&block_entity.data)?; } //println!("{:#?}", self.block_entities); @@ -564,19 +568,21 @@ impl LevelChunkWithLight { let mut writer = Vec::new(); for i in 0..num_sections { - let block_id = if i == 0 { 1 } else { 0 }; - // Num non-air blocks - writer.write_all( - &if pkmc_generated::block::is_air(block_id) { - 0i16 - } else { - 4096i16 + let block = match i { + 0 => { + if (chunk_x + chunk_z) % 2 == 0 { + Block::PinkConcrete + } else { + Block::LightBlueConcrete + } } - .to_be_bytes(), - )?; + _ => Block::Air, + }; + // Num non-air blocks + writer.write_all(&(!block.is_air() as i16 * 4096).to_be_bytes())?; // Blocks - writer.write_all(&to_paletted_data_singular(block_id)?)?; + writer.write_all(&to_paletted_data_singular(block.into_id())?)?; // Biome writer.write_all(&to_paletted_data_singular(0)?)?; } @@ -885,7 +891,7 @@ impl ClientboundPacket for UpdateSectionBlocks { pub struct AddEntity { pub id: i32, pub uuid: UUID, - pub r#type: i32, + pub r#type: EntityType, pub position: Vec3, pub pitch: u8, pub yaw: u8, @@ -902,7 +908,7 @@ impl ClientboundPacket for AddEntity { fn packet_write(&self, mut writer: impl Write) -> Result<(), ConnectionError> { writer.encode(self.id)?; writer.encode(&self.uuid)?; - writer.encode(self.r#type)?; + writer.encode(self.r#type.to_id())?; writer.write_all(&self.position.x.to_be_bytes())?; writer.write_all(&self.position.y.to_be_bytes())?; writer.write_all(&self.position.z.to_be_bytes())?; @@ -935,8 +941,8 @@ pub enum EntityMetadata { // TODO: Implement enum Direction(i32), OptionalUUID(Option), - BlockState(i32), - OptionalBlockState(Option), + BlockState(Block), + OptionalBlockState(Option), NBT(NBT), Particle(i32), Particles(Vec), @@ -1020,16 +1026,11 @@ impl EntityMetadata { } EntityMetadata::BlockState(block_state) => { writer.encode(14)?; - writer.encode(*block_state)?; + writer.encode(block_state.into_id())?; } EntityMetadata::OptionalBlockState(block_state) => { writer.encode(15)?; - if let Some(block_state) = block_state { - assert_ne!(*block_state, 0); - writer.encode(*block_state)?; - } else { - writer.encode(0)?; - } + writer.encode(block_state.unwrap_or(Block::Air).into_id())?; } EntityMetadata::NBT(nbt) => { writer.encode(16)?; @@ -1623,10 +1624,10 @@ impl ClientboundPacket for LevelParticles { writer.encode(self.particle.r#type().to_id())?; match &self.particle { Particle::Block(block) => { - writer.encode(block.id_with_default_fallback().unwrap())?; + writer.encode(block.into_id())?; } Particle::BlockMarker(block) => { - writer.encode(block.id_with_default_fallback().unwrap())?; + writer.encode(block.into_id())?; } Particle::Dust { color, scale } => { writer.write_all(&color.to_argb8888(0).to_be_bytes())?; @@ -1641,7 +1642,7 @@ impl ClientboundPacket for LevelParticles { writer.write_all(&color.to_argb8888(*alpha).to_be_bytes())?; } Particle::FallingDust(block) => { - writer.encode(block.id_with_default_fallback().unwrap())?; + writer.encode(block.into_id())?; } Particle::SculkCharge { roll } => { writer.write_all(&roll.to_be_bytes())?; @@ -1676,10 +1677,10 @@ impl ClientboundPacket for LevelParticles { writer.encode(*delay)?; } Particle::DustPillar(block) => { - writer.encode(block.id_with_default_fallback().unwrap())?; + writer.encode(block.into_id())?; } Particle::BlockCrumble(block) => { - writer.encode(block.id_with_default_fallback().unwrap())?; + writer.encode(block.into_id())?; } _ => {} } diff --git a/pkmc-defs/src/particle.rs b/pkmc-defs/src/particle.rs index 36bc926..7dbf875 100644 --- a/pkmc-defs/src/particle.rs +++ b/pkmc-defs/src/particle.rs @@ -1,8 +1,6 @@ -use pkmc_generated::registry::ParticleType; +use pkmc_generated::{block::Block, registry::ParticleType}; use pkmc_util::{Color, Position, Vec3}; -use crate::block::Block; - #[derive(Debug)] pub enum Particle { AngryVillager, diff --git a/pkmc-generated/generated/src/lib.rs b/pkmc-generated/generated/src/lib.rs index 3856da1..872b164 100644 --- a/pkmc-generated/generated/src/lib.rs +++ b/pkmc-generated/generated/src/lib.rs @@ -53,7 +53,7 @@ pub mod block { use serde::Deserialize; - use pkmc_generated_proc::include_cached_json_compressed_bytes; + use pkmc_generated_proc::{include_cached_json_compressed_bytes, report_blocks_enum}; #[derive(Deserialize)] pub struct ReportBlockState { @@ -83,9 +83,51 @@ pub mod block { .unwrap() }); - // TODO: Autogenerate this - pub const fn is_air(id: i32) -> bool { - matches!(id, 0 | 13971 | 13972) + pub trait IdIndexable { + const MAX_INDEX: u32; + fn into_index(self) -> u32; + fn from_index(index: u32) -> Option + where + Self: Sized; + } + + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct PropertyUint(u32); + + impl PropertyUint { + /// Asserts if outside of range, use as IdIndexable>::from_index for runtime creation instead. + pub const fn new(value: u32) -> Self { + // TODO: Make compile-time check. + assert!(value <= MAX, "PropertyUint value is outside of range"); + Self(value) + } + } + + impl IdIndexable for PropertyUint { + const MAX_INDEX: u32 = MAX; + + fn into_index(self) -> u32 { + self.0 + } + + fn from_index(index: u32) -> Option { + (index <= MAX).then_some(PropertyUint(index)) + } + } + + report_blocks_enum!("assets/reports/blocks.json"); + + impl Default for Block { + fn default() -> Self { + Self::Air + } + } + + impl Block { + pub fn is_air(&self) -> bool { + matches!(self, Block::Air | Block::CaveAir | Block::VoidAir) + } } } @@ -101,3 +143,106 @@ pub mod consts { // TODO: Autogenerate this value. pub const PALETTED_DATA_BIOMES_DIRECT: u8 = 6; } + +#[cfg(test)] +mod simple_test { + use crate::block::{IdIndexable, PropertyUint}; + use pkmc_generated_proc::report_blocks_enum; + + report_blocks_enum!("generated/src/test_blocks_report.json"); + + fn do_test(block: Block, id: i32) { + if id != block.into_id() { + panic!( + "Block::into_id test failed for block {:?}, expected {} but got {}", + block, + id, + block.into_id(), + ); + } + if Some(block) != Block::from_id(id) { + panic!( + "Block::from_id test failed for id {}, expected {:?} but got {:?}", + id, + Some(block), + Block::from_id(id), + ); + } + } + + #[rustfmt::skip] + #[test] + fn test_simple_blocks_ids() { + do_test(Block::Air, 0); + do_test(Block::Stone, 1); + do_test(Block::Barrier { waterlogged: true }, 2); + do_test(Block::Barrier { waterlogged: false }, 3); + do_test(Block::RedstoneWallTorch { facing: Facing0::North, lit: true }, 4); + do_test(Block::RedstoneWallTorch { facing: Facing0::North, lit: false }, 5); + do_test(Block::RedstoneWallTorch { facing: Facing0::South, lit: true }, 6); + do_test(Block::RedstoneWallTorch { facing: Facing0::South, lit: false }, 7); + do_test(Block::RedstoneWallTorch { facing: Facing0::West, lit: true }, 8); + do_test(Block::RedstoneWallTorch { facing: Facing0::West, lit: false }, 9); + do_test(Block::RedstoneWallTorch { facing: Facing0::East, lit: true }, 10); + do_test(Block::RedstoneWallTorch { facing: Facing0::East, lit: false }, 11); + do_test(Block::Wheat { age: PropertyUint::new(0) }, 12); + do_test(Block::Wheat { age: PropertyUint::new(1) }, 13); + do_test(Block::Wheat { age: PropertyUint::new(2) }, 14); + do_test(Block::Wheat { age: PropertyUint::new(3) }, 15); + do_test(Block::Wheat { age: PropertyUint::new(4) }, 16); + do_test(Block::Wheat { age: PropertyUint::new(5) }, 17); + do_test(Block::Wheat { age: PropertyUint::new(6) }, 18); + do_test(Block::Wheat { age: PropertyUint::new(7) }, 19); + } +} + +#[cfg(test)] +mod complex_test { + use crate::block::{self, Block, PropertyUint}; + + fn do_test(block: Block, id: i32) { + if id != block.into_id() { + panic!( + "Block::into_id test failed for block {:?}, expected {} but got {}", + block, + id, + block.into_id(), + ); + } + if Some(block) != Block::from_id(id) { + panic!( + "Block::from_id test failed for id {}, expected {:?} but got {:?}", + id, + Some(block), + Block::from_id(id), + ); + } + } + + #[test] + fn test_simple_blocks_ids() { + do_test(Block::Air, 0); + do_test(Block::Stone, 1); + // Just some random block states to test, not anything special to them. + do_test( + Block::RedStainedGlassPane { + east: false, + north: false, + south: true, + waterlogged: false, + west: false, + }, + 10656, + ); + do_test( + Block::RedstoneWire { + east: block::East2::Up, + north: block::North2::Up, + power: PropertyUint::new(14), + south: block::South2::Up, + west: block::West2::None, + }, + 3170, + ); + } +} diff --git a/pkmc-generated/generated/src/test_blocks_report.json b/pkmc-generated/generated/src/test_blocks_report.json new file mode 100644 index 0000000..d8db4c9 --- /dev/null +++ b/pkmc-generated/generated/src/test_blocks_report.json @@ -0,0 +1,199 @@ +{ + "minecraft:air": { + "definition": { + "type": "minecraft:air", + "properties": {} + }, + "states": [ + { + "default": true, + "id": 0 + } + ] + }, + "minecraft:stone": { + "definition": { + "type": "minecraft:block", + "properties": {} + }, + "states": [ + { + "default": true, + "id": 1 + } + ] + }, + "minecraft:barrier": { + "definition": { + "type": "minecraft:barrier", + "properties": {} + }, + "properties": { + "waterlogged": [ + "true", + "false" + ] + }, + "states": [ + { + "id": 2, + "properties": { + "waterlogged": "true" + } + }, + { + "default": true, + "id": 3, + "properties": { + "waterlogged": "false" + } + } + ] + }, + "minecraft:redstone_wall_torch": { + "definition": { + "type": "minecraft:redstone_wall_torch", + "properties": {} + }, + "properties": { + "facing": [ + "north", + "south", + "west", + "east" + ], + "lit": [ + "true", + "false" + ] + }, + "states": [ + { + "default": true, + "id": 4, + "properties": { + "facing": "north", + "lit": "true" + } + }, + { + "id": 5, + "properties": { + "facing": "north", + "lit": "false" + } + }, + { + "id": 6, + "properties": { + "facing": "south", + "lit": "true" + } + }, + { + "id": 7, + "properties": { + "facing": "south", + "lit": "false" + } + }, + { + "id": 8, + "properties": { + "facing": "west", + "lit": "true" + } + }, + { + "id": 9, + "properties": { + "facing": "west", + "lit": "false" + } + }, + { + "id": 10, + "properties": { + "facing": "east", + "lit": "true" + } + }, + { + "id": 11, + "properties": { + "facing": "east", + "lit": "false" + } + } + ] + }, + "minecraft:wheat": { + "definition": { + "type": "minecraft:crop", + "properties": {} + }, + "properties": { + "age": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7" + ] + }, + "states": [ + { + "default": true, + "id": 12, + "properties": { + "age": "0" + } + }, + { + "id": 13, + "properties": { + "age": "1" + } + }, + { + "id": 14, + "properties": { + "age": "2" + } + }, + { + "id": 15, + "properties": { + "age": "3" + } + }, + { + "id": 16, + "properties": { + "age": "4" + } + }, + { + "id": 17, + "properties": { + "age": "5" + } + }, + { + "id": 18, + "properties": { + "age": "6" + } + }, + { + "id": 19, + "properties": { + "age": "7" + } + } + ] + } +} \ No newline at end of file diff --git a/pkmc-generated/proc/src/lib.rs b/pkmc-generated/proc/src/lib.rs index 5b88775..7ab72aa 100644 --- a/pkmc-generated/proc/src/lib.rs +++ b/pkmc-generated/proc/src/lib.rs @@ -1,8 +1,8 @@ use std::{env, io::Write as _, path::PathBuf}; use convert_case::Casing as _; -use quote::{quote, ToTokens}; -use syn::{parse::Parse, parse_macro_input, spanned::Spanned as _, LitStr, Token}; +use quote::{ToTokens, quote}; +use syn::{LitStr, Token, parse::Parse, parse_macro_input, spanned::Spanned as _}; mod reports; @@ -34,6 +34,11 @@ pub fn report_packets_generate_consts(input: proc_macro::TokenStream) -> proc_ma reports::packets::report_packets_generate_consts(input) } +#[proc_macro] +pub fn report_blocks_enum(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + reports::blocks::report_blocks_generate_enum(input) +} + struct CachedCompressedJson { input: LitStr, output: LitStr, diff --git a/pkmc-generated/proc/src/reports/blocks.rs b/pkmc-generated/proc/src/reports/blocks.rs new file mode 100644 index 0000000..db4e6a0 --- /dev/null +++ b/pkmc-generated/proc/src/reports/blocks.rs @@ -0,0 +1,413 @@ +/* + TODO: + Cleanup all of this! I know it's very bad right now, I'm very sorry for that. + Actually use IdIndexable::MAX for calculating IDs. + Add ability to rename placeholder property enum types. + Add ability to group certain block types (Eg. Woods & colored blocks with an extra property with enum) to reduce code & enum size. +*/ + +use std::collections::{BTreeMap, BTreeSet}; + +use convert_case::Casing; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use serde::Deserialize; +use syn::{parse::Parse, parse_macro_input, spanned::Spanned as _, Ident, LitStr}; + +use crate::{file_path, fix_identifier}; + +#[derive(Deserialize)] +#[allow(unused)] +struct ReportBlockState { + id: i32, + #[serde(default)] + default: bool, + #[serde(default)] + properties: BTreeMap, +} + +#[derive(Deserialize)] +#[allow(unused)] +struct ReportBlock { + definition: serde_json::Value, + #[serde(default)] + properties: BTreeMap>, + states: Vec, +} + +type ReportBlocks = BTreeMap; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum PropertyType { + Boolean, + Number(u32), + Enum(Vec), +} + +impl From> for PropertyType { + fn from(value: Vec) -> Self { + assert!(value.len() > 1, "PropertyType invalid number of states"); + if value == vec!["true".to_owned(), "false".to_owned()] { + PropertyType::Boolean + } else if let Some(count) = value + .iter() + .map(|v| v.parse::().ok()) + .collect::>>() + .and_then(|mut nums| { + nums.sort(); + match nums.first().unwrap() { + 0 => nums + .iter() + .enumerate() + .all(|(i, v)| i as u32 == *v) + .then_some(nums.len() - 1), + 1 => nums + .iter() + .enumerate() + .all(|(i, v)| (i as u32 + 1) == *v) + .then_some(nums.len() - 1), + _ => panic!(), + } + }) + { + PropertyType::Number(count as u32) + } else { + PropertyType::Enum(value) + } + } +} + +type PropertiesMap = BTreeMap>; + +struct ReportBlocksGenerator { + blocks_report: ReportBlocks, +} + +impl ReportBlocksGenerator { + fn generate_properties(&self) -> PropertiesMap { + let mut properties = PropertiesMap::new(); + self.blocks_report + .iter() + .for_each(|(_block_name, block_data)| { + block_data + .properties + .iter() + .for_each(|(property_name, property_values)| { + let property_name = property_name.clone(); + let property_type = PropertyType::from(property_values.clone()); + properties + .entry(property_name) + .or_default() + .insert(property_type); + }); + }); + properties + } + + fn generate_properties_code(map: &PropertiesMap, tokens: &mut proc_macro2::TokenStream) { + map.iter().for_each(|(property_name, property_types)| { + property_types + .iter() + .enumerate() + .for_each(|(i, property_type)| { + if let PropertyType::Enum(vec) = property_type { + let enum_name = Ident::new( + &format!("{}{}", property_name, i).to_case(convert_case::Case::Pascal), + tokens.span(), + ); + let enum_values = vec + .iter() + .map(|v| { + Ident::new(&v.to_case(convert_case::Case::Pascal), tokens.span()) + }) + .collect::>(); + let max_value = vec.len() as u32 - 1; + let enum_indices = (0..vec.len()).map(|v| v as u32).collect::>(); + tokens.extend(quote! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub enum #enum_name { + #(#enum_values,)* + } + + impl IdIndexable for #enum_name { + const MAX_INDEX: u32 = #max_value; + + fn into_index(self) -> u32 { + match self { + #(Self::#enum_values => #enum_indices,)* + } + } + + fn from_index(index: u32) -> Option { + match index { + #(#enum_indices => Some(Self::#enum_values),)* + _ => None, + } + } + } + }); + } + }); + }); + } + + fn generate_blocks_code( + &self, + properties_map: &PropertiesMap, + tokens: &mut proc_macro2::TokenStream, + ) { + let mut blocks_tokens = proc_macro2::TokenStream::new(); + let mut blocks_to_id_tokens = proc_macro2::TokenStream::new(); + let mut blocks_from_id_tokens = proc_macro2::TokenStream::new(); + + self.blocks_report.iter().for_each(|(name, def)| { + let name = Ident::new(&fix_identifier(name), tokens.span()); + let id = def.states.first().unwrap().id as u32; + def.states.iter().enumerate().for_each(|(i, state)| { + assert_eq!(id + i as u32, state.id as u32); + }); + + if def.properties.is_empty() { + blocks_tokens.extend(quote! { + #name, + }); + blocks_to_id_tokens.extend(quote! { + Self::#name => #id, + }); + blocks_from_id_tokens.extend(quote! { + #id => Self::#name, + }); + return; + } + + struct ParsedProperty<'a> { + ident_name: Ident, + ident_type: TokenStream, + r#type: &'a PropertyType, + } + + let parsed_props: Vec = def + .properties + .iter() + .map(|(property_name, property_values)| { + let (i, r#type) = properties_map + .get(property_name) + .unwrap() + .iter() + .enumerate() + .find(|(_, v)| *v == &PropertyType::from(property_values.clone())) + .unwrap(); + ParsedProperty { + ident_name: match property_name.as_ref() { + "type" => Ident::new_raw(property_name, tokens.span()), + _ => Ident::new(property_name, tokens.span()), + }, + ident_type: match r#type { + PropertyType::Boolean => quote! { bool }, + PropertyType::Number(max) => quote! { PropertyUint<#max> }, + PropertyType::Enum(_) => { + let property_type = Ident::new( + &format!("{}{}", property_name, i) + .to_case(convert_case::Case::Pascal), + tokens.span(), + ); + quote! { #property_type } + } + }, + r#type, + } + }) + .collect(); + + let property_names = parsed_props + .iter() + .map(|v| v.ident_name.clone()) + .collect::>(); + let property_types = parsed_props + .iter() + .map(|v| v.ident_type.clone()) + .collect::>(); + blocks_tokens.extend(quote! { + #name { + #(#property_names: #property_types,)* + }, + }); + + let mut mul: u32 = 1; + let property_index_calcs = parsed_props + .iter() + .rev() + .map(|parsed| { + let name = &parsed.ident_name; + match parsed.r#type { + PropertyType::Boolean => { + let v = quote! { + (!#name as u32) * #mul + }; + mul *= 2; + v + } + PropertyType::Number(max) => { + let v = quote! { + #name.into_index() * #mul + }; + mul *= max + 1; + v + } + PropertyType::Enum(items) => { + let v = quote! { + #name.into_index() * #mul + }; + mul *= items.len() as u32; + v + } + } + }) + .collect::>(); + + blocks_to_id_tokens.extend(quote! { + Self::#name { + #(#property_names,)* + } => #id #(+ #property_index_calcs)*, + }); + + let res = parsed_props + .iter() + .rev() + .map(|parsed| { + let name = &parsed.ident_name; + let r#type = &parsed.ident_type; + match parsed.r#type { + PropertyType::Boolean => { + quote! { + let #name = v % 2 == 0; + let v = v / 2; + } + } + PropertyType::Number(max) => { + let max = max + 1; + quote! { + let #name = PropertyUint::from_index(v % #max).unwrap(); + let v = v / #max; + } + } + PropertyType::Enum(items) => { + let max = items.len() as u32; + quote! { + let #name = #r#type::from_index(v % #max).unwrap(); + let v = v / #max; + } + } + } + }) + .collect::>(); + let res2 = parsed_props.iter().map(|parsed| &parsed.ident_name); + + let max_id = def.states.last().unwrap().id as u32; + + blocks_from_id_tokens.extend(quote! { + #id..=#max_id => { + let v = id - #id; + #(#res)* + Self::#name { + #(#res2,)* + } + }, + }); + }); + + tokens.extend(quote! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub enum Block { + #blocks_tokens + } + + impl Block { + pub fn into_id(self) -> i32 { + let v: u32 = match self { + #blocks_to_id_tokens + }; + v as i32 + } + + pub fn from_id(id: i32) -> Option { + let id = id as u32; + Some(match id { + #blocks_from_id_tokens + _ => return None, + }) + } + } + }); + } +} + +struct ReportBlocksEnum { + file: LitStr, +} + +impl Parse for ReportBlocksEnum { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(Self { + file: input.parse()?, + }) + } +} + +impl ToTokens for ReportBlocksEnum { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let file = file_path(&self.file.value()); + + let blocks_report: ReportBlocks = + serde_json::from_reader(std::fs::File::open(&file).expect("Failed to open file")) + .expect("Failed to parse JSON"); + + let generator = ReportBlocksGenerator { blocks_report }; + + let properties = generator.generate_properties(); + ReportBlocksGenerator::generate_properties_code(&properties, tokens); + generator.generate_blocks_code(&properties, tokens); + } +} + +pub(crate) fn report_blocks_generate_enum( + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let c = parse_macro_input!(input as ReportBlocksEnum); + quote! { #c }.into() +} + +#[cfg(test)] +mod test { + use crate::reports::blocks::PropertyType; + + #[test] + fn test_property_from() { + assert_eq!( + PropertyType::from(vec!["true".to_owned(), "false".to_owned()]), + PropertyType::Boolean, + ); + assert_eq!( + PropertyType::from(vec![ + "0".to_owned(), + "1".to_owned(), + "2".to_owned(), + "3".to_owned() + ]), + PropertyType::Number(3), + ); + assert_eq!( + PropertyType::from(vec![ + "1".to_owned(), + "2".to_owned(), + "3".to_owned(), + "4".to_owned() + ]), + PropertyType::Number(3), + ); + assert_eq!( + PropertyType::from(vec!["Hello".to_owned(), "World".to_owned()]), + PropertyType::Enum(vec!["Hello".to_owned(), "World".to_owned()]), + ); + } +} diff --git a/pkmc-generated/proc/src/reports/mod.rs b/pkmc-generated/proc/src/reports/mod.rs index 4a7ad3b..6d09afc 100644 --- a/pkmc-generated/proc/src/reports/mod.rs +++ b/pkmc-generated/proc/src/reports/mod.rs @@ -1,2 +1,3 @@ +pub(crate) mod blocks; pub(crate) mod packets; pub(crate) mod registries; diff --git a/pkmc-server/src/entity_manager.rs b/pkmc-server/src/entity_manager.rs index f3e1cb0..4821358 100644 --- a/pkmc-server/src/entity_manager.rs +++ b/pkmc-server/src/entity_manager.rs @@ -109,7 +109,7 @@ impl EntityVisibility { pub struct EntityHandler { id: i32, uuid: UUID, - r#type: i32, + r#type: EntityType, pub position: Vec3, last_position: Vec3, pub velocity: Vec3, @@ -125,7 +125,7 @@ pub struct EntityHandler { } impl EntityHandler { - fn new(id: i32, uuid: UUID, r#type: i32) -> Self { + fn new(id: i32, uuid: UUID, r#type: EntityType) -> Self { Self { id, uuid, @@ -386,7 +386,7 @@ impl EntityManager { EntityBase { handler: self.entities.insert_ignored( id, - Mutex::new(EntityHandler::new(id, uuid, entity.r#type().to_id())), + Mutex::new(EntityHandler::new(id, uuid, entity.r#type())), ), inner: entity, uuid, diff --git a/pkmc-server/src/world/anvil/chunk_format.rs b/pkmc-server/src/world/anvil/chunk_format.rs new file mode 100644 index 0000000..bd615da --- /dev/null +++ b/pkmc-server/src/world/anvil/chunk_format.rs @@ -0,0 +1,51 @@ +#![allow(unused)] + +use pkmc_defs::{biome::Biome, block::DynamicBlock}; +use serde::Deserialize; +use std::{collections::HashMap, fmt::Debug}; + +#[derive(Deserialize)] +pub struct PalettedData { + #[serde(default)] + pub palette: Box<[T]>, + #[serde(default)] + pub data: Box<[i64]>, +} + +#[derive(Deserialize)] +pub struct ChunkSection { + #[serde(rename = "Y")] + pub y: i8, + pub block_states: Option>, + pub biomes: Option>, +} + +#[derive(Deserialize)] +pub struct BlockEntity { + pub id: String, + #[serde(rename = "keepPacked", default)] + pub keep_packed: bool, + pub x: i32, + pub y: i16, + pub z: i32, + #[serde(flatten)] + pub data: HashMap, +} + +#[derive(Deserialize)] +pub struct Chunk { + #[serde(rename = "DataVersion")] + pub data_version: i32, + #[serde(rename = "xPos")] + pub x_pos: i32, + #[serde(rename = "zPos")] + pub z_pos: i32, + #[serde(rename = "yPos")] + pub y_pos: Option, + #[serde(rename = "Status")] + pub status: String, + #[serde(rename = "LastUpdate")] + pub last_update: i64, + pub sections: Vec, + pub block_entities: Vec, +} diff --git a/pkmc-server/src/world/anvil/mod.rs b/pkmc-server/src/world/anvil/mod.rs new file mode 100644 index 0000000..4cc7b92 --- /dev/null +++ b/pkmc-server/src/world/anvil/mod.rs @@ -0,0 +1,23 @@ +use pkmc_util::{connection::ConnectionError, nbt::NBTError}; +use thiserror::Error; + +mod chunk_format; +mod world; + +#[derive(Error, Debug)] +pub enum AnvilError { + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error(transparent)] + ConnectionError(#[from] ConnectionError), + #[error("Region chunk unknown compression \"{0}\"")] + RegionUnknownCompression(u8), + #[error("Region chunk unsupported compression \"{0}\"")] + RegionUnsupportedCompression(String), + #[error(transparent)] + NBTError(#[from] NBTError), + #[error("Invalid block entity type \"{0}\"")] + InvalidBlockEntityType(String), +} + +pub use world::*; diff --git a/pkmc-server/src/world/anvil.rs b/pkmc-server/src/world/anvil/world.rs similarity index 61% rename from pkmc-server/src/world/anvil.rs rename to pkmc-server/src/world/anvil/world.rs index 9c43f2c..9927edf 100644 --- a/pkmc-server/src/world/anvil.rs +++ b/pkmc-server/src/world/anvil/world.rs @@ -3,89 +3,68 @@ use std::{ fmt::Debug, fs::File, hash::Hash, - io::{Seek, Write}, + io::{Seek as _, Write}, path::PathBuf, sync::{Arc, Mutex}, }; -use itertools::Itertools; -use pkmc_defs::{ - biome::Biome, - block::{Block, BlockEntity}, - packet, -}; +use itertools::Itertools as _; +use pkmc_defs::{biome::Biome, packet}; use pkmc_generated::{ + block::Block, consts::{ PALETTED_DATA_BIOMES_DIRECT, PALETTED_DATA_BIOMES_INDIRECT, PALETTED_DATA_BLOCKS_DIRECT, PALETTED_DATA_BLOCKS_INDIRECT, }, - registry::BlockEntityType, }; use pkmc_util::{ connection::{ - paletted_container::{ - calculate_bpe, to_paletted_data, to_paletted_data_precomputed, - to_paletted_data_singular, - }, - ConnectionError, ConnectionSender, + paletted_container::{calculate_bpe, to_paletted_data, to_paletted_data_precomputed}, + ConnectionSender, }, - nbt::{from_nbt, NBTError, NBT}, - IdTable, PackedArray, Position, ReadExt, Transmutable, Vec3, WeakList, + nbt::{from_nbt, NBT}, + IdTable, PackedArray, Position, ReadExt as _, Transmutable as _, Vec3, WeakList, }; -use serde::Deserialize; -use thiserror::Error; - -use crate::world::{chunk_loader::ChunkPosition, SECTION_SIZE}; -use super::{ - chunk_loader::ChunkLoader, World, WorldBlock, WorldViewer, CHUNK_SIZE, SECTION_BIOMES, - SECTION_BLOCKS, +use crate::world::{ + chunk_loader::{ChunkLoader, ChunkPosition}, + section_get_block_index, World, WorldViewer, CHUNK_SIZE, SECTION_BIOMES, SECTION_BLOCKS, + SECTION_BLOCKS_SIZE, }; +use super::{chunk_format, AnvilError}; + pub const REGION_SIZE: usize = 32; pub const CHUNKS_PER_REGION: usize = REGION_SIZE * REGION_SIZE; // Each time the world updates & sends new data to client, we either send sections or chunks. -// NOTE: When sending sections, the client calculates lighting instead of server. +// Note that when sending sections, the client calculates lighting instead of server. pub const UPDATE_SECTION_CHUNK_SWITCH_NUM_SECTIONS: usize = 4; pub const UPDATE_SECTION_CHUNK_SWITCH_NUM_BLOCKS: usize = 1024; -#[derive(Error, Debug)] -pub enum AnvilError { - #[error(transparent)] - IoError(#[from] std::io::Error), - #[error(transparent)] - ConnectionError(#[from] ConnectionError), - #[error("Region chunk unknown compression \"{0}\"")] - RegionUnknownCompression(u8), - #[error("Region chunk unsupported compression \"{0}\"")] - RegionUnsupportedCompression(String), - #[error(transparent)] - NBTError(#[from] NBTError), - #[error("Invalid block entity type \"{0}\"")] - InvalidBlockEntityType(String), -} - -fn default_paletted_data() -> Box<[T]> { - vec![T::default()].into_boxed_slice() -} - -#[derive(Debug, Deserialize)] -struct PalettedData { - #[serde(default = "default_paletted_data")] +#[derive(Debug)] +struct PalettedData { palette: Box<[T]>, - #[serde(default)] data: Box<[i64]>, } -impl - PalettedData +impl Default + for PalettedData { + fn default() -> Self { + Self { + palette: vec![T::default()].into_boxed_slice(), + data: vec![].into_boxed_slice(), + } + } +} + +impl PalettedData { fn bpe(palette_count: usize) -> u8 { match palette_count { 0 => panic!(), 1 => 0, - // NOTE: Data stored inside the world files doesn't have direct paletting. + // Data stored inside the world files doesn't have direct paletting. palette_count => PackedArray::bits_per_entry(palette_count as u64 - 1).max(I_S), } } @@ -107,7 +86,7 @@ impl } } -impl +impl PalettedData { fn set(&mut self, index: usize, value: T) -> bool { @@ -178,57 +157,25 @@ type ChunkSectionBlockStates = PalettedData< >; impl ChunkSectionBlockStates { - fn get_block_index(x: u8, y: u8, z: u8) -> usize { - debug_assert!((x as usize) < SECTION_SIZE); - debug_assert!((y as usize) < SECTION_SIZE); - debug_assert!((z as usize) < SECTION_SIZE); - (y as usize) * SECTION_SIZE * SECTION_SIZE + (z as usize) * SECTION_SIZE + (x as usize) - } - - fn get_block(&self, x: u8, y: u8, z: u8) -> &Block { - self.get(Self::get_block_index(x, y, z)) - } - - fn set_block(&mut self, x: u8, y: u8, z: u8, block: Block) -> bool { - self.set(Self::get_block_index(x, y, z), block) - } - fn write(&self, mut writer: impl Write) -> Result<(), AnvilError> { - //if self.palette.len() == 1 { - // let id = self.palette[0] - // .id_with_default_fallback() - // .unwrap_or_else(|| Block::air().id().unwrap()); - // writer.write_all( - // &if generated::block::is_air(id) { - // 0u16 - // } else { - // 4096 - // } - // .to_be_bytes(), - // )?; - // writer.write_all(&to_paletted_data_singular(id)?)?; - // return Ok(()); - //} + // TODO: Special case for if there's only 1 block state. let block_ids = self .palette .iter() - .map(|b| { - b.id_with_default_fallback() - .unwrap_or_else(|| Block::air().id().unwrap()) - }) + .map(|b| b.into_id()) .collect::>(); let block_count = (0..SECTION_BLOCKS) - .filter(|i| !pkmc_generated::block::is_air(block_ids[self.palette_index(*i)])) + .filter(|i| !self.palette[self.palette_index(*i)].is_air()) .count(); writer.write_all(&(block_count as u16).to_be_bytes())?; - const FORCE_CHUNK_REENCODE: bool = false; + const FORCE_SECTION_REENCODE: bool = false; - if !FORCE_CHUNK_REENCODE - // NOTE: Data stored in the anvil format doesn't have direct paletting. + if !FORCE_SECTION_REENCODE + // Data stored in the anvil format doesn't have direct paletting. // So we need to re-encode the data if there's too many palette values. && calculate_bpe(block_ids.len()) <= PALETTED_DATA_BLOCKS_INDIRECT_END { @@ -266,235 +213,153 @@ impl ChunkSectionBiomes { let biome_ids = self .palette .iter() - .map(|b| { - b.id(mapper) - .unwrap_or_else(|| Biome::default().id(mapper).unwrap()) - }) - .collect::>(); - - writer.write_all(&to_paletted_data( - &(0..SECTION_BIOMES) - .map(|i| biome_ids[self.palette_index(i)]) - .collect::>(), - PALETTED_DATA_BIOMES_INDIRECT, - PALETTED_DATA_BIOMES_DIRECT, - )?)?; + .map(|b| b.id(mapper).unwrap_or_default()) + .collect::>(); + + const FORCE_SECTION_REENCODE: bool = false; + + if !FORCE_SECTION_REENCODE + // Data stored in the anvil format doesn't have direct paletting. + // So we need to re-encode the data if there's too many palette values. + && calculate_bpe(biome_ids.len()) <= PALETTED_DATA_BIOMES_INDIRECT_END + { + writer.write_all(&to_paletted_data_precomputed( + &biome_ids, + &self.data, + PALETTED_DATA_BIOMES_INDIRECT, + PALETTED_DATA_BIOMES_DIRECT, + )?)?; + } else { + writer.write_all(&to_paletted_data( + &(0..SECTION_BIOMES) + .map(|i| biome_ids[self.palette_index(i)]) + .collect::>(), + PALETTED_DATA_BIOMES_INDIRECT, + PALETTED_DATA_BIOMES_DIRECT, + )?)?; + } Ok(()) } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Default)] struct ChunkSection { - #[serde(rename = "Y")] - y: i8, - block_states: Option, - biomes: Option, + blocks: ChunkSectionBlockStates, + biomes: ChunkSectionBiomes, } -#[derive(Debug, Deserialize, Clone)] -struct AnvilBlockEntity { - id: String, - #[allow(unused)] - #[serde(rename = "keepPacked", default)] - keep_packed: bool, - x: i32, - y: i16, - z: i32, - #[serde(flatten)] - data: HashMap, -} - -#[derive(Debug, Deserialize)] -pub struct AnvilChunk { - //#[serde(rename = "DataVersion")] - //data_version: i32, - #[serde(rename = "xPos")] - x_pos: i32, - #[serde(rename = "zPos")] - z_pos: i32, - #[serde(rename = "yPos")] - y_pos: Option, - #[serde(skip, default)] - section_y_pos: i8, - //#[serde(rename = "Status")] - //status: String, - //#[serde(rename = "LastUpdate")] - //last_update: i64, - sections: Vec, - block_entities: Vec, - #[serde(skip, default)] - parsed_block_entities: HashMap<(u8, i16, u8), BlockEntity>, +#[derive(Debug)] +struct Chunk { + chunk_x: i32, + chunk_z: i32, + sections_y_start: i8, + sections: Box<[ChunkSection]>, } -impl AnvilChunk { - fn initialize( - &mut self, - section_y_range: std::ops::RangeInclusive, - ) -> Result<(), AnvilError> { - if let Some(y_pos) = self.y_pos { - assert!(*section_y_range.start() == y_pos); +impl Chunk { + fn new(parsed: chunk_format::Chunk, section_y_range: std::ops::RangeInclusive) -> Self { + if let Some(y_pos) = parsed.y_pos { + assert_eq!(*section_y_range.start(), y_pos); } - self.section_y_pos = *section_y_range.start(); - - // Insert missing sections - section_y_range.for_each(|section_y| { - if self.sections.iter().any(|section| section.y == section_y) { - return; - } - self.sections.push(ChunkSection { - y: section_y, - block_states: None, - biomes: None, - }) - }); - - // Sometimes sections are unsorted. - // And also the inserting of sections in the above code may also cause it to become unsorted. - self.sections.sort_by(|a, b| a.y.cmp(&b.y)); - self.parsed_block_entities = self - .block_entities - .iter() - .map(|b| { - let bx = b.x.rem_euclid(CHUNK_SIZE as i32) as u8; - let bz = b.z.rem_euclid(CHUNK_SIZE as i32) as u8; - Ok::<_, AnvilError>(( - (bx, b.y, bz), - BlockEntity::new( - self.get_tile_block(bx, b.y, bz).unwrap(), - BlockEntityType::from_str(&b.id) - .ok_or_else(|| AnvilError::InvalidBlockEntityType(b.id.clone()))?, - NBT::try_from(serde_json::Value::from_iter(b.data.clone())).unwrap(), - ), - )) - }) - .collect::>()?; - - Ok(()) - } - - fn get_section(&self, section_y: i8) -> Option<&ChunkSection> { - self.sections.iter().find(|section| section.y == section_y) - } - - fn get_section_mut(&mut self, section_y: i8) -> Option<&mut ChunkSection> { - //self.sections - // .iter_mut() - // .find(|section| section.y == section_y) - self.sections - .get_mut((section_y - self.section_y_pos) as usize) - } - - fn get_tile_block(&self, block_x: u8, block_y: i16, block_z: u8) -> Option { - debug_assert!((block_x as usize) < SECTION_SIZE); - debug_assert!((block_z as usize) < SECTION_SIZE); - Some( - self.get_section(block_y.div_euclid(SECTION_SIZE as i16) as i8)? - .block_states - .as_ref()? - .get_block( - block_x, - (block_y.rem_euclid(SECTION_SIZE as i16)) as u8, - block_z, - ) - .clone(), - ) - } - - fn get_block(&self, block_x: u8, block_y: i16, block_z: u8) -> Option { - // TODO: WorldBlock::BlockEntity - self.get_tile_block(block_x, block_y, block_z) - .map(WorldBlock::Block) + Chunk { + chunk_x: parsed.x_pos, + chunk_z: parsed.z_pos, + sections_y_start: *section_y_range.start(), + sections: section_y_range + .map(|section_y| { + let Some(section) = parsed + .sections + .iter() + .find(|section| section.y == section_y) + else { + return ChunkSection::default(); + }; + ChunkSection { + blocks: section + .block_states + .as_ref() + .map(|i| PalettedData { + palette: i + .palette + .iter() + .map(|v| { + v.to_block().unwrap_or_else(|| { + println!( + "Invalid block in chunk {} {}: {:#?}", + parsed.x_pos, parsed.z_pos, v, + ); + Block::Air + }) + }) + .collect(), + data: i.data.clone(), + }) + .unwrap_or_default(), + biomes: section + .biomes + .as_ref() + .map(|i| PalettedData { + palette: i.palette.clone(), + data: i.data.clone(), + }) + .unwrap_or_default(), + } + }) + .collect(), + } } - fn set_block(&mut self, block_x: u8, block_y: i16, block_z: u8, block: WorldBlock) -> bool { - debug_assert!((block_x as usize) < SECTION_SIZE); - debug_assert!((block_z as usize) < SECTION_SIZE); - - let block = match block { - WorldBlock::Block(block) => { - self.parsed_block_entities - .remove(&(block_x, block_y, block_z)); - block - } - WorldBlock::BlockEntity(block_entity) => { - let block = block_entity.block.clone(); - - self.parsed_block_entities - .insert((block_x, block_y, block_z), block_entity); - - block - } - }; - - let Some(section) = self.get_section_mut(block_y.div_euclid(SECTION_SIZE as i16) as i8) - else { + fn get_block(&self, x: u8, y: i16, z: u8) -> Option { + debug_assert!((x as usize) < SECTION_BLOCKS_SIZE); + debug_assert!((z as usize) < SECTION_BLOCKS_SIZE); + let section = self.sections.get( + (y.div_euclid(SECTION_BLOCKS_SIZE as i16) - (self.sections_y_start as i16)) as usize, + )?; + Some(*section.blocks.get(section_get_block_index( + x, + y.rem_euclid(SECTION_BLOCKS_SIZE as i16) as u8, + z, + ))) + } + + fn set_block(&mut self, x: u8, y: i16, z: u8, block: Block) -> bool { + debug_assert!((x as usize) < SECTION_BLOCKS_SIZE); + debug_assert!((z as usize) < SECTION_BLOCKS_SIZE); + let Some(section) = self.sections.get_mut( + (y.div_euclid(SECTION_BLOCKS_SIZE as i16) - (self.sections_y_start as i16)) as usize, + ) else { return false; }; - let Some(block_states) = section.block_states.as_mut() else { - return false; - }; - - block_states.set_block( - block_x, - (block_y.rem_euclid(SECTION_SIZE as i16)) as u8, - block_z, + section.blocks.set( + section_get_block_index(x, y.rem_euclid(SECTION_BLOCKS_SIZE as i16) as u8, z), block, ) } - fn block_entities(&self) -> &HashMap<(u8, i16, u8), BlockEntity> { - &self.parsed_block_entities - } - fn to_packet( &self, biome_mapper: &IdTable, ) -> Result { Ok(packet::play::LevelChunkWithLight { - chunk_x: self.x_pos, - chunk_z: self.z_pos, + chunk_x: self.chunk_x, + chunk_z: self.chunk_z, chunk_data: packet::play::LevelChunkData { heightmaps: HashMap::new(), data: { let mut writer = Vec::new(); self.sections.iter().try_for_each(|section| { - if let Some(block_states) = §ion.block_states { - block_states.write(&mut writer)?; - } else { - writer.write_all(&0u16.to_be_bytes())?; - writer.write_all(&to_paletted_data_singular( - Block::air().id().unwrap(), - )?)?; - } - if let Some(biomes) = §ion.biomes { - biomes.write(&mut writer, biome_mapper)?; - } else { - writer.write_all(&to_paletted_data_singular( - Biome::default().id(biome_mapper).unwrap(), - )?)?; - } + section.blocks.write(&mut writer)?; + section.biomes.write(&mut writer, biome_mapper)?; Ok::<_, AnvilError>(()) })?; writer.into_boxed_slice() }, - block_entities: self - .block_entities() - .iter() - .filter(|(_, b)| b.r#type.nbt_visible()) - .map(|((x, y, z), b)| packet::play::BlockEntity { - x: *x, - z: *z, - y: *y, - r#type: b.r#type.to_id(), - data: b.data.clone(), - }) - .collect(), + block_entities: Vec::new(), }, - // TODO: Light data light_data: packet::play::LevelLightData::full_bright(self.sections.len()), }) } @@ -567,11 +432,17 @@ impl Region { .transpose()?) } - fn load_chunk(&mut self, chunk_x: u8, chunk_z: u8) -> Result, AnvilError> { + fn load_chunk( + &mut self, + chunk_x: u8, + chunk_z: u8, + section_y_range: std::ops::RangeInclusive, + ) -> Result, AnvilError> { Ok(self .read_nbt(chunk_x, chunk_z)? - .map(|(_, nbt)| from_nbt::(nbt)) - .transpose()?) + .map(|(_, nbt)| from_nbt::(nbt)) + .transpose()? + .map(|parsed| Chunk::new(parsed, section_y_range))) } } @@ -582,11 +453,11 @@ struct SectionDiff { } impl SectionDiff { - fn set(&mut self, x: u8, y: u8, z: u8, id: i32) { - assert!((x as usize) < SECTION_SIZE); - assert!((y as usize) < SECTION_SIZE); - assert!((z as usize) < SECTION_SIZE); - self.change.insert((x, y, z), id); + fn set(&mut self, x: u8, y: u8, z: u8, block: Block) { + assert!((x as usize) < SECTION_BLOCKS_SIZE); + assert!((y as usize) < SECTION_BLOCKS_SIZE); + assert!((z as usize) < SECTION_BLOCKS_SIZE); + self.change.insert((x, y, z), block.into_id()); } fn num_blocks(&self) -> usize { @@ -606,7 +477,7 @@ pub struct AnvilWorld { root: PathBuf, identifier: String, regions: HashMap<(i32, i32), Option>, - chunks: HashMap<(i32, i32), Option>, + chunks: HashMap<(i32, i32), Option>, section_y_range: std::ops::RangeInclusive, biome_mapper: IdTable, viewers: WeakList>, @@ -676,15 +547,17 @@ impl AnvilWorld { self.prepare_region(region_x, region_z)?; + let range = self.section_y_range(); + let Some(Some(region)) = self.regions.get_mut(&(region_x, region_z)) else { return Ok(()); }; - if let Some(mut chunk) = region.load_chunk( + if let Some(chunk) = region.load_chunk( chunk_x.rem_euclid(REGION_SIZE as i32) as u8, chunk_z.rem_euclid(REGION_SIZE as i32) as u8, + range, )? { - chunk.initialize(self.section_y_range())?; self.chunks.insert((chunk_x, chunk_z), Some(chunk)); } @@ -781,7 +654,7 @@ impl World for AnvilWorld { Ok(()) } - fn get_block(&mut self, position: Position) -> Result, Self::Error> { + fn get_block(&mut self, position: Position) -> Result, Self::Error> { let chunk_x = position.x.div_euclid(CHUNK_SIZE as i32); let chunk_z = position.z.div_euclid(CHUNK_SIZE as i32); self.prepare_chunk(chunk_x, chunk_z)?; @@ -798,7 +671,7 @@ impl World for AnvilWorld { )) } - fn set_block(&mut self, position: Position, block: WorldBlock) -> Result<(), Self::Error> { + fn set_block(&mut self, position: Position, block: Block) -> Result<(), Self::Error> { let chunk_x = position.x.div_euclid(CHUNK_SIZE as i32); let chunk_z = position.z.div_euclid(CHUNK_SIZE as i32); self.prepare_chunk(chunk_x, chunk_z)?; @@ -812,24 +685,21 @@ impl World for AnvilWorld { (position.x.rem_euclid(CHUNK_SIZE as i32)) as u8, position.y, (position.z.rem_euclid(CHUNK_SIZE as i32)) as u8, - block.clone(), + block, ) { self.diffs .entry(( - position.x.div_euclid(SECTION_SIZE as i32), - position.z.div_euclid(SECTION_SIZE as i32), + position.x.div_euclid(SECTION_BLOCKS_SIZE as i32), + position.z.div_euclid(SECTION_BLOCKS_SIZE as i32), )) .or_default() - .entry(position.y.div_euclid(SECTION_SIZE as i16)) + .entry(position.y.div_euclid(SECTION_BLOCKS_SIZE as i16)) .or_default() .set( - position.x.rem_euclid(SECTION_SIZE as i32) as u8, - position.y.rem_euclid(SECTION_SIZE as i16) as u8, - position.z.rem_euclid(SECTION_SIZE as i32) as u8, - block - .as_block() - .id_with_default_fallback() - .unwrap_or_else(|| Block::air().id().unwrap()), + position.x.rem_euclid(SECTION_BLOCKS_SIZE as i32) as u8, + position.y.rem_euclid(SECTION_BLOCKS_SIZE as i16) as u8, + position.z.rem_euclid(SECTION_BLOCKS_SIZE as i32) as u8, + block, ); } Ok(()) @@ -880,17 +750,17 @@ mod test { let position = Position::new(1 + grid_z * 2, 70, 1 + grid_x * 2); - let Some(block) = world.get_block(position)?.map(|b| b.into_block()) else { + let Some(block) = world.get_block(position)? else { panic!("Expected loaded block at {:?}", position); }; - if block.id() != Some(block_id) { + if block.into_id() != block_id { panic!( "Block at {:?} is {:?} with ID {:?}, but our ID is {}", position, block, - block.id(), - block_id + block.into_id(), + block_id, ); } } diff --git a/pkmc-server/src/world/mod.rs b/pkmc-server/src/world/mod.rs index 8030935..380ea37 100644 --- a/pkmc-server/src/world/mod.rs +++ b/pkmc-server/src/world/mod.rs @@ -4,10 +4,8 @@ use std::{ }; use chunk_loader::ChunkLoader; -use pkmc_defs::{ - block::{Block, BlockEntity}, - packet, -}; +use pkmc_defs::packet; +use pkmc_generated::block::Block; use pkmc_util::{ connection::{ConnectionError, ConnectionSender}, Position, Vec3, @@ -17,11 +15,29 @@ pub mod anvil; pub mod chunk_loader; pub const CHUNK_SIZE: usize = 16; -pub const SECTION_SIZE: usize = 16; +pub const SECTION_BLOCKS_SIZE: usize = 16; pub const SECTION_BLOCKS: usize = 4096; pub const SECTION_BIOMES_SIZE: usize = 4; pub const SECTION_BIOMES: usize = 64; +pub fn section_get_block_index(x: u8, y: u8, z: u8) -> usize { + debug_assert!((x as usize) < SECTION_BLOCKS_SIZE); + debug_assert!((y as usize) < SECTION_BLOCKS_SIZE); + debug_assert!((z as usize) < SECTION_BLOCKS_SIZE); + (y as usize) * SECTION_BLOCKS_SIZE * SECTION_BLOCKS_SIZE + + (z as usize) * SECTION_BLOCKS_SIZE + + (x as usize) +} + +pub fn section_get_biome_index(x: u8, y: u8, z: u8) -> usize { + debug_assert!((x as usize) < SECTION_BIOMES_SIZE); + debug_assert!((y as usize) < SECTION_BIOMES_SIZE); + debug_assert!((z as usize) < SECTION_BIOMES_SIZE); + (y as usize) * SECTION_BIOMES_SIZE * SECTION_BIOMES_SIZE + + (z as usize) * SECTION_BIOMES_SIZE + + (x as usize) +} + #[derive(Debug)] pub struct WorldViewer { connection: ConnectionSender, @@ -45,41 +61,12 @@ impl WorldViewer { } } -#[derive(Debug, Clone, PartialEq)] -pub enum WorldBlock { - Block(Block), - BlockEntity(BlockEntity), -} - -impl WorldBlock { - pub fn as_block(&self) -> &Block { - match self { - WorldBlock::Block(block) => block, - WorldBlock::BlockEntity(block_entity) => &block_entity.block, - } - } - - pub fn into_block(self) -> Block { - match self { - WorldBlock::Block(block) => block, - WorldBlock::BlockEntity(block_entity) => block_entity.block, - } - } - - pub fn as_block_entity(&self) -> Option<&BlockEntity> { - match self { - WorldBlock::Block(..) => None, - WorldBlock::BlockEntity(block_entity) => Some(block_entity), - } - } -} - pub trait World: Debug { type Error: std::error::Error; fn add_viewer(&mut self, connection: ConnectionSender) -> Arc>; fn update_viewers(&mut self) -> Result<(), Self::Error>; - fn get_block(&mut self, position: Position) -> Result, Self::Error>; - fn set_block(&mut self, position: Position, block: WorldBlock) -> Result<(), Self::Error>; + fn get_block(&mut self, position: Position) -> Result, Self::Error>; + fn set_block(&mut self, position: Position, block: Block) -> Result<(), Self::Error>; }