From bc6d05849f735c93d22ff5cb4030ffc07b3ed4cc Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sat, 25 Feb 2023 21:17:36 +0100 Subject: [PATCH 01/22] initial sponge schematic version 2 implementation --- Cargo.toml | 5 +- crates/valence/Cargo.toml | 4 +- crates/valence/examples/schem_loading.rs | 76 ++++ crates/valence_block/Cargo.toml | 1 + crates/valence_block/src/lib.rs | 49 +++ crates/valence_instance/src/lib.rs | 4 + crates/valence_schem/Cargo.toml | 20 ++ crates/valence_schem/README.md | 3 + crates/valence_schem/src/lib.rs | 422 +++++++++++++++++++++++ 9 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 crates/valence/examples/schem_loading.rs create mode 100644 crates/valence_schem/Cargo.toml create mode 100644 crates/valence_schem/README.md create mode 100644 crates/valence_schem/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 9f0ba57fb..f7bc9759b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,9 @@ async-trait = "0.1.60" atty = "0.2.14" base64 = "0.21.0" bevy_app = { version = "0.10.1", default-features = false } -bevy_ecs = { version = "0.10.1", default-features = false, features = ["trace"] } +bevy_ecs = { version = "0.10.1", default-features = false, features = [ + "trace", +] } bevy_mod_debugdump = "0.7.0" bitfield-struct = "0.3.1" byteorder = "1.4.3" @@ -72,6 +74,7 @@ tracing-subscriber = "0.3.16" url = { version = "2.2.2", features = ["serde"] } uuid = "1.3.1" valence_anvil.path = "crates/valence_anvil" +valence_schem.path = "crates/valence_schem" valence_biome.path = "crates/valence_biome" valence_block.path = "crates/valence_block" valence_build_utils.path = "crates/valence_build_utils" diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index 7c84c437a..86161ca4b 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -11,11 +11,12 @@ keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] [features] -default = ["network", "player_list", "inventory", "anvil"] +default = ["network", "player_list", "inventory", "anvil", "schem"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] inventory = ["dep:valence_inventory"] anvil = ["dep:valence_anvil"] +schem = ["dep:valence_schem"] [dependencies] bevy_app.workspace = true @@ -35,6 +36,7 @@ valence_network = { workspace = true, optional = true } valence_player_list = { workspace = true, optional = true } valence_inventory = { workspace = true, optional = true } valence_anvil = { workspace = true, optional = true } +valence_schem = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true diff --git a/crates/valence/examples/schem_loading.rs b/crates/valence/examples/schem_loading.rs new file mode 100644 index 000000000..95e858988 --- /dev/null +++ b/crates/valence/examples/schem_loading.rs @@ -0,0 +1,76 @@ +use std::path::PathBuf; + +use bevy_ecs::prelude::*; +use clap::Parser; +use glam::DVec3; +use valence::prelude::*; +use valence_schem::Schematic; + +const SPAWN_POS: DVec3 = DVec3::new(0.0, 256.0, 0.0); + +#[derive(Parser)] +#[clap(author, version, about)] +struct Cli { + /// The path to a Sponge Schematic. + path: PathBuf, +} + +#[derive(Resource)] +struct SchemRes(Schematic); + +pub fn main() { + tracing_subscriber::fmt().init(); + + let Cli { path } = Cli::parse(); + + if !path.exists() { + eprintln!("File `{}` does not exist. Exiting.", path.display()); + return; + } else if !path.is_file() { + eprintln!("`{}` is not a file. Exiting.", path.display()); + return; + } + + let schem = match Schematic::load(path) { + Ok(schem) => schem, + Err(err) => { + eprintln!("Error loading schematic: {err}"); + return; + } + }; + + App::new() + .insert_resource(SchemRes(schem)) + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); +} + +fn setup( + mut commands: Commands, + dimensions: Query<&DimensionType>, + biomes: Query<&Biome>, + server: Res, + schem: Res, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + + schem.0.paste(&mut instance, BlockPos::new(0, 0, 0), |_| { + BiomeId::default() + }); + + commands.spawn(instance); +} + +fn init_clients( + mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + instances: Query>, +) { + for (mut loc, mut pos, mut game_mode) in &mut clients { + *game_mode = GameMode::Creative; + loc.0 = instances.single(); + pos.set(SPAWN_POS); + } +} diff --git a/crates/valence_block/Cargo.toml b/crates/valence_block/Cargo.toml index 0a315df9a..11cb1236e 100644 --- a/crates/valence_block/Cargo.toml +++ b/crates/valence_block/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] valence_core.workspace = true +thiserror.workspace = true anyhow.workspace = true [build-dependencies] diff --git a/crates/valence_block/src/lib.rs b/crates/valence_block/src/lib.rs index a9fe07e1a..b1544c609 100644 --- a/crates/valence_block/src/lib.rs +++ b/crates/valence_block/src/lib.rs @@ -22,7 +22,9 @@ use std::fmt; use std::fmt::Display; use std::io::Write; use std::iter::FusedIterator; +use std::str::FromStr; +use thiserror::Error; use anyhow::Context; use valence_core::ident; use valence_core::ident::Ident; @@ -100,6 +102,53 @@ impl Decode<'_> for BlockKind { } } +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ParseBlockStateError { + #[error("unknown block kind '{0}'")] + UnknownBlockKind(String), + #[error("invalid prop string '{0}'")] + InvalidPropString(String), + #[error("unknown prop name '{0}'")] + UnknownPropName(String), + #[error("unknown prop value '{0}'")] + UnknownPropValue(String), +} + +impl FromStr for BlockState { + type Err = ParseBlockStateError; + + fn from_str(s: &str) -> std::result::Result { + let state = match s.split_once('[') { + Some((kind, props)) => { + let Some(kind) = BlockKind::from_str(kind) else { + return Err(ParseBlockStateError::UnknownBlockKind(kind.to_string())); + }; + props[..props.len() - 1] + .split(',') + .map(|prop| prop.trim()) + .try_fold(kind.to_state(), |state, prop| { + let Some((name, val)) = prop.split_once('=') else { + return Err(ParseBlockStateError::InvalidPropString(prop.to_string())); + }; + let Some(name) = PropName::from_str(name) else { + return Err(ParseBlockStateError::UnknownPropName(name.to_string())); + }; + let Some(val) = PropValue::from_str(val) else { + return Err(ParseBlockStateError::UnknownPropValue(val.to_string())); + }; + Ok(state.set(name, val)) + })? + } + None => match BlockKind::from_str(s) { + Some(kind) => kind.to_state(), + None => return Err(ParseBlockStateError::UnknownBlockKind(s.to_string())), + }, + }; + + Ok(state) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/valence_instance/src/lib.rs b/crates/valence_instance/src/lib.rs index 670b83f4c..da9b98c84 100644 --- a/crates/valence_instance/src/lib.rs +++ b/crates/valence_instance/src/lib.rs @@ -527,6 +527,10 @@ impl Instance { self.info.section_count } + pub fn min_y(&self) -> i32 { + self.info.min_y + } + /// Get a reference to the chunk at the given position, if it is loaded. pub fn chunk(&self, pos: impl Into) -> Option<&Chunk> { self.partition diff --git a/crates/valence_schem/Cargo.toml b/crates/valence_schem/Cargo.toml new file mode 100644 index 000000000..2d43c7098 --- /dev/null +++ b/crates/valence_schem/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "valence_schem" +description = "A library for the Sponge Schematic Format." +documentation.workspace = true +repository = "https://github.com/valence-rs/valence/tree/main/crates/valence_schem" +readme = "README.md" +license.workspace = true +keywords = ["schematics", "minecraft", "deserialization"] +version.workspace = true +edition.workspace = true + +[dependencies] +flate2.workspace = true +glam.workspace = true +thiserror.workspace = true +valence_biome.workspace = true +valence_block.workspace = true +valence_core.workspace = true +valence_instance.workspace = true +valence_nbt.workspace = true diff --git a/crates/valence_schem/README.md b/crates/valence_schem/README.md new file mode 100644 index 000000000..f91e3fab7 --- /dev/null +++ b/crates/valence_schem/README.md @@ -0,0 +1,3 @@ +# valence_schem + +Support for the [Sponge schematic file format](https://github.com/SpongePowered/Schematic-Specification). diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs new file mode 100644 index 000000000..b3914ad3f --- /dev/null +++ b/crates/valence_schem/src/lib.rs @@ -0,0 +1,422 @@ +#![doc = include_str!("../README.md")] +#![deny( + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + rustdoc::invalid_html_tags +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_import_braces, + unreachable_pub, + clippy::dbg_macro +)] + +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::fs::File; +use std::io::{self, BufReader, Read}; +use std::path::PathBuf; +use std::str::FromStr; + +use flate2::bufread::GzDecoder; +use glam::IVec3; +use thiserror::Error; +use valence_biome::BiomeId; +use valence_block::{BlockEntityKind, BlockState, ParseBlockStateError}; +use valence_core::block_pos::BlockPos; +use valence_core::chunk_pos::ChunkPos; +use valence_core::ident::Ident; +use valence_core::packet::var_int::{VarInt, VarIntDecodeError}; +use valence_instance::{BlockEntity as ValenceBlockEntity, Instance}; +use valence_nbt::{Compound, List, Value}; + +pub struct Schematic { + pub metadata: Option, + pub width: u16, + pub height: u16, + pub length: u16, + pub offset: IVec3, + block_data: Box<[BlockState]>, + block_entities: Box<[BlockEntity]>, + // TODO: pub entities: Box<[Entity]>, + biome_palette: Box<[Ident]>, + biome_data: Box<[usize]>, +} + +pub struct BlockEntity { + pub pos: BlockPos, + pub kind: BlockEntityKind, + pub nbt: Compound, +} + +#[derive(Debug, Error)] +pub enum LoadSchematicError { + #[error(transparent)] + Io(#[from] io::Error), + + #[error(transparent)] + Nbt(#[from] valence_nbt::Error), + + #[error("missing version")] + MissingVersion, + + #[error("unknown version {0}")] + UnknownVersion(i32), + + #[error("missing width")] + MissingWidth, + + #[error("missing height")] + MissingHeight, + + #[error("missing length")] + MissingLength, + + #[error("missing offset")] + MissingOffset, + + #[error("missing palette")] + MissingPalette, + + #[error("invalid palette")] + InvalidPalette, + + #[error(transparent)] + ParseBlockStateError(#[from] ParseBlockStateError), + + #[error("missing block data")] + MissingBlockData, + + #[error(transparent)] + VarIntDecodeError(#[from] VarIntDecodeError), + + #[error("invalid block count")] + InvalidBlockCount, + + #[error("missing block entity pos")] + MissingBlockEntityPos, + + #[error("missing block entity id")] + MissingBlockEntityId, + + #[error("invalid block entity id '{0}'")] + InvalidBlockEntityId(String), + + #[error("unknown block entity '{0}'")] + UnknownBlockEntity(String), + + #[error("missing biome palette")] + MissingBiomePalette, + + #[error("invalid biome palette")] + InvalidBiomePalette, + + #[error("invalid biome ident '{0}'")] + InvalidBiomeIdent(String), + + #[error("missing biome data")] + MissingBiomeData, +} + +struct VarIntReader>(I); +impl> Iterator for VarIntReader { + type Item = Result; + + fn next(&mut self) -> Option { + struct ReadWrapper>(I); + impl> Read for ReadWrapper { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + for (idx, byte) in buf.iter_mut().enumerate() { + let Some(val) = self.0.next() else { + return Ok(idx); + }; + *byte = val; + } + Ok(buf.len()) + } + } + + if self.0.len() == 0 { + None + } else { + Some(VarInt::decode_partial(ReadWrapper(&mut self.0))) + } + } +} + +// fn read_varints( +// iter: impl ExactSizeIterator, +// transform: impl Fn(i32) -> T, +// ) -> Result, VarIntDecodeError> { +// let mut data = vec![]; +// let mut block_data = ReadWrapper(iter); +// while block_data.0.len() > 0 { +// let var_int = VarInt::decode_partial(&mut block_data)?; +// data.push(transform(var_int)); +// } +// Ok(data) +// } + +impl Schematic { + pub fn load(path: PathBuf) -> Result { + let file = File::open(path)?; + + let mut buf = vec![]; + let mut z = GzDecoder::new(BufReader::new(file)); + z.read_to_end(&mut buf)?; + + let data = valence_nbt::from_binary_slice(&mut buf.as_slice())?.0; + + let metadata = data + .get("Metadata") + .and_then(|val| val.as_compound()) + .cloned(); + + let Some(&Value::Int(version)) = data.get("Version") else { + return Err(LoadSchematicError::MissingVersion); + }; + if version != 2 { + return Err(LoadSchematicError::UnknownVersion(version)); + } + let Some(&Value::Short(width)) = data.get("Width") else { + return Err(LoadSchematicError::MissingWidth); + }; + let width = width as u16; + let Some(&Value::Short(height)) = data.get("Height") else { + return Err(LoadSchematicError::MissingHeight); + }; + let height = height as u16; + let Some(&Value::Short(length)) = data.get("Length") else { + return Err(LoadSchematicError::MissingLength); + }; + let length = length as u16; + let offset = { + let &[x, y, z] = data.get("Offset").and_then(|val| val.as_int_array()).map(|arr| arr.as_slice()).unwrap_or(&[0; 3]) else { + return Err(LoadSchematicError::MissingOffset); + }; + IVec3::new(x, y, z) + }; + let palette: HashMap = { + let Some(Value::Compound(palette)) = data.get("Palette") else { + return Err(LoadSchematicError::MissingPalette); + }; + let palette: Result<_, _> = palette + .into_iter() + .map(|(state, value)| { + let &Value::Int(i) = value else { + return Err(LoadSchematicError::InvalidPalette); + }; + let state = + BlockState::from_str(state.strip_prefix("minecraft:").unwrap_or(state))?; + Ok((i, state)) + }) + .collect(); + palette? + }; + let block_data = { + let Some(Value::ByteArray(block_data)) = data.get("BlockData") else { + return Err(LoadSchematicError::MissingBlockData); + }; + + let data: Result, LoadSchematicError> = + VarIntReader(block_data.iter().map(|byte| *byte as u8)) + .map(|val| Ok(palette[&val?])) + .collect(); + let data = data?; + + if u16::try_from(data.len()) != Ok(width * height * length) { + return Err(LoadSchematicError::InvalidBlockCount); + } + + data.into_boxed_slice() + }; + let block_entities = if let Some(Value::List(List::Compound(block_entities))) = + data.get("BlockEntities") + { + let block_entities: Result, _> = block_entities.iter().map(|block_entity| { + let Some(&[x, y, z]) = block_entity.get("Pos").and_then(|val| val.as_int_array()).map(|arr| arr.as_slice()) else { + return Err(LoadSchematicError::MissingBlockEntityPos); + }; + let pos = BlockPos::new(x, y, z); + let kind = { + let Some(Value::String(id)) = block_entity.get("Id") else { + return Err(LoadSchematicError::MissingBlockEntityId); + }; + if id.is_empty() { + block_data[(x + z * width as i32 + y * width as i32 * length as i32) as usize].block_entity_kind().ok_or(LoadSchematicError::MissingBlockEntityId)? + } else { + let Ok(id) = Ident::new(id) else { + return Err(LoadSchematicError::InvalidBlockEntityId(id.clone())); + }; + let Some(kind) = BlockEntityKind::from_ident(id.as_str_ident()) else { + return Err(LoadSchematicError::UnknownBlockEntity(id.to_string())); + }; + kind + } + }; + let mut nbt = block_entity.clone(); + nbt.remove("Pos"); + nbt.remove("Id"); + Ok(BlockEntity { pos, kind, nbt }) + }).collect(); + + block_entities?.into_boxed_slice() + } else { + Box::new([]) + }; + + let Some(Value::Compound(biome_palette)) = data.get("BiomePalette") else { + return Err(LoadSchematicError::MissingBiomePalette); + }; + let biome_palette: Result, _> = biome_palette + .iter() + .map(|(biome, value)| { + let &Value::Int(i) = value else { + return Err(LoadSchematicError::InvalidBiomePalette); + }; + let Ok(ident) = Ident::new(biome.clone()) else { + return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); + }; + Ok((i, ident)) + }) + .collect(); + let biome_palette = biome_palette?; + + let Some(Value::ByteArray(biome_data)) = data.get("BiomeData") else { + return Err(LoadSchematicError::MissingBiomeData); + }; + let biome_data: Result, LoadSchematicError> = + VarIntReader(biome_data.iter().map(|byte| *byte as u8)) + .map(|val| Ok(&biome_palette[&val?])) + .collect(); + let biome_data = biome_data?; + + let (biome_palette, biome_data) = { + let mut palette = vec![]; + let mut map = HashMap::new(); + let biome_data: Vec<_> = biome_data + .into_iter() + .map(|biome| match map.entry(biome) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let idx = palette.len(); + palette.push(biome.to_string_ident()); + entry.insert(idx); + idx + } + }) + .collect(); + + (palette.into_boxed_slice(), biome_data.into_boxed_slice()) + }; + + Ok(Self { + metadata, + width, + height, + length, + offset, + block_data, + block_entities, + biome_palette, + biome_data, + }) + } + + pub fn paste(&self, instance: &mut Instance, origin: BlockPos, map_biome: F) + where + F: FnMut(Ident<&str>) -> BiomeId, + { + let blocks = self.block_data.iter().enumerate().map(|(idx, state)| { + let idx = u16::try_from(idx).unwrap(); + let y = idx / (self.width * self.length); + let z = (idx % (self.width * self.length)) / self.width; + let x = (idx % (self.width * self.length)) % self.width; + + ([x, y, z], state) + }); + + let min_y = instance.min_y(); + for ([x, y, z], state) in blocks { + let block_pos = BlockPos::new( + x as i32 + origin.x - self.offset.x, + y as i32 + origin.y - self.offset.y, + z as i32 + origin.z - self.offset.z, + ); + let chunk = instance + .chunk_entry(ChunkPos::from_block_pos(block_pos)) + .or_default(); + chunk.set_block_state( + block_pos.x.rem_euclid(16) as usize, + (block_pos.y - min_y) as usize, + block_pos.z.rem_euclid(16) as usize, + *state, + ); + } + + for BlockEntity { + pos: BlockPos { x, y, z }, + kind, + nbt, + } in self.block_entities.iter() + { + let block_pos = BlockPos::new( + x + origin.x - self.offset.x, + y + origin.y - self.offset.y, + z + origin.z - self.offset.z, + ); + let chunk = instance + .chunk_entry(ChunkPos::from_block_pos(block_pos)) + .or_default(); + let x = block_pos.x.rem_euclid(16) as usize; + let y = (block_pos.y - min_y) as usize; + let z = block_pos.z.rem_euclid(16) as usize; + chunk.set_block_entity( + x, + y, + z, + ValenceBlockEntity { + kind: *kind, + nbt: nbt.clone(), + }, + ); + } + + let section_count = instance.section_count(); + for ([x, z], biome) in self + .biome_data + .iter() + .map(|biome| self.biome_palette[*biome].as_str_ident()) + .map(map_biome) + .enumerate() + .map(|(idx, biome)| { + let idx = u16::try_from(idx).unwrap(); + let z = idx / self.width; + let x = idx % self.width; + + ([x, z], biome) + }) + { + let x = x as i32 + origin.x - self.offset.x; + let z = z as i32 + origin.z - self.offset.z; + let chunk = instance + .chunk_entry(ChunkPos::at(x as f64, z as f64)) + .or_default(); + + for y in 0..section_count * 4 { + chunk.set_biome( + (x / 4).rem_euclid(4) as usize, + y, + (z / 4).rem_euclid(4) as usize, + biome, + ); + } + } + } +} From dbe79ab68700afe9f8a695927b9a5c43f13532cf Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 26 Feb 2023 14:53:59 +0100 Subject: [PATCH 02/22] upgrade to sponge schematic version 3 --- crates/valence/examples/schem_loading.rs | 10 +- crates/valence/src/lib.rs | 2 + crates/valence_schem/src/lib.rs | 428 +++++++++++------------ 3 files changed, 222 insertions(+), 218 deletions(-) diff --git a/crates/valence/examples/schem_loading.rs b/crates/valence/examples/schem_loading.rs index 95e858988..a3dc83262 100644 --- a/crates/valence/examples/schem_loading.rs +++ b/crates/valence/examples/schem_loading.rs @@ -1,12 +1,10 @@ use std::path::PathBuf; -use bevy_ecs::prelude::*; use clap::Parser; -use glam::DVec3; use valence::prelude::*; use valence_schem::Schematic; -const SPAWN_POS: DVec3 = DVec3::new(0.0, 256.0, 0.0); +const SPAWN_POS: BlockPos = BlockPos::new(0, 256, 0); #[derive(Parser)] #[clap(author, version, about)] @@ -71,6 +69,10 @@ fn init_clients( for (mut loc, mut pos, mut game_mode) in &mut clients { *game_mode = GameMode::Creative; loc.0 = instances.single(); - pos.set(SPAWN_POS); + pos.set([ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64, + SPAWN_POS.z as f64 + 0.5, + ]); } } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index e316d0e16..7bab9e8c2 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -35,6 +35,8 @@ pub use valence_inventory as inventory; pub use valence_network as network; #[cfg(feature = "player_list")] pub use valence_player_list as player_list; +#[cfg(feature = "schem")] +pub use valence_schem as schem; pub use { bevy_app as app, bevy_ecs as ecs, glam, valence_biome as biome, valence_block as block, valence_client as client, valence_dimension as dimension, valence_entity as entity, diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index b3914ad3f..6b4cbe4d8 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -33,28 +33,39 @@ use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_core::packet::var_int::{VarInt, VarIntDecodeError}; -use valence_instance::{BlockEntity as ValenceBlockEntity, Instance}; +use valence_instance::{Block as ValenceBlock, Instance}; use valence_nbt::{Compound, List, Value}; +#[derive(Debug, Clone, PartialEq)] pub struct Schematic { pub metadata: Option, pub width: u16, pub height: u16, pub length: u16, pub offset: IVec3, - block_data: Box<[BlockState]>, - block_entities: Box<[BlockEntity]>, - // TODO: pub entities: Box<[Entity]>, - biome_palette: Box<[Ident]>, - biome_data: Box<[usize]>, + blocks: Option>, + biomes: Option, + // TODO: pub entities: Option>, } +#[derive(Debug, Clone, PartialEq)] +pub struct Block { + pub state: BlockState, + pub block_entity: Option, +} + +#[derive(Debug, Clone, PartialEq)] pub struct BlockEntity { - pub pos: BlockPos, pub kind: BlockEntityKind, pub nbt: Compound, } +#[derive(Debug, Clone, PartialEq)] +pub struct Biomes { + palette: Box<[Ident]>, + data: Box<[usize]>, +} + #[derive(Debug, Error)] pub enum LoadSchematicError { #[error(transparent)] @@ -63,10 +74,13 @@ pub enum LoadSchematicError { #[error(transparent)] Nbt(#[from] valence_nbt::Error), + #[error("missing schematic")] + MissingSchematic, + #[error("missing version")] MissingVersion, - #[error("unknown version {0}")] + #[error("unknown version {0} (only version 3 is supported)")] UnknownVersion(i32), #[error("missing width")] @@ -78,14 +92,14 @@ pub enum LoadSchematicError { #[error("missing length")] MissingLength, - #[error("missing offset")] - MissingOffset, + #[error("invalid offset")] + InvalidOffset, - #[error("missing palette")] - MissingPalette, + #[error("missing block palette")] + MissingBlockPalette, - #[error("invalid palette")] - InvalidPalette, + #[error("invalid block palette")] + InvalidBlockPalette, #[error(transparent)] ParseBlockStateError(#[from] ParseBlockStateError), @@ -111,6 +125,9 @@ pub enum LoadSchematicError { #[error("unknown block entity '{0}'")] UnknownBlockEntity(String), + #[error("missing block entity data")] + MissingBlockEntityData, + #[error("missing biome palette")] MissingBiomePalette, @@ -150,19 +167,6 @@ impl> Iterator for VarIntReader { } } -// fn read_varints( -// iter: impl ExactSizeIterator, -// transform: impl Fn(i32) -> T, -// ) -> Result, VarIntDecodeError> { -// let mut data = vec![]; -// let mut block_data = ReadWrapper(iter); -// while block_data.0.len() > 0 { -// let var_int = VarInt::decode_partial(&mut block_data)?; -// data.push(transform(var_int)); -// } -// Ok(data) -// } - impl Schematic { pub fn load(path: PathBuf) -> Result { let file = File::open(path)?; @@ -171,149 +175,160 @@ impl Schematic { let mut z = GzDecoder::new(BufReader::new(file)); z.read_to_end(&mut buf)?; - let data = valence_nbt::from_binary_slice(&mut buf.as_slice())?.0; + let root = valence_nbt::from_binary_slice(&mut buf.as_slice())?.0; + let Some(Value::Compound(root)) = root.get("Schematic") else { + return Err(LoadSchematicError::MissingSchematic); + }; - let metadata = data + let metadata = root .get("Metadata") .and_then(|val| val.as_compound()) .cloned(); - let Some(&Value::Int(version)) = data.get("Version") else { + let Some(&Value::Int(version)) = root.get("Version") else { return Err(LoadSchematicError::MissingVersion); }; - if version != 2 { + //TODO: Allow version 1 and 2 + if version != 3 { return Err(LoadSchematicError::UnknownVersion(version)); } - let Some(&Value::Short(width)) = data.get("Width") else { + let Some(&Value::Short(width)) = root.get("Width") else { return Err(LoadSchematicError::MissingWidth); }; let width = width as u16; - let Some(&Value::Short(height)) = data.get("Height") else { + let Some(&Value::Short(height)) = root.get("Height") else { return Err(LoadSchematicError::MissingHeight); }; let height = height as u16; - let Some(&Value::Short(length)) = data.get("Length") else { + let Some(&Value::Short(length)) = root.get("Length") else { return Err(LoadSchematicError::MissingLength); }; let length = length as u16; let offset = { - let &[x, y, z] = data.get("Offset").and_then(|val| val.as_int_array()).map(|arr| arr.as_slice()).unwrap_or(&[0; 3]) else { - return Err(LoadSchematicError::MissingOffset); + let &[x, y, z] = root.get("Offset").and_then(|val| val.as_int_array()).map(|arr| arr.as_slice()).unwrap_or(&[0; 3]) else { + return Err(LoadSchematicError::InvalidOffset); }; IVec3::new(x, y, z) }; - let palette: HashMap = { - let Some(Value::Compound(palette)) = data.get("Palette") else { - return Err(LoadSchematicError::MissingPalette); - }; - let palette: Result<_, _> = palette - .into_iter() - .map(|(state, value)| { - let &Value::Int(i) = value else { - return Err(LoadSchematicError::InvalidPalette); - }; - let state = - BlockState::from_str(state.strip_prefix("minecraft:").unwrap_or(state))?; - Ok((i, state)) - }) - .collect(); - palette? - }; - let block_data = { - let Some(Value::ByteArray(block_data)) = data.get("BlockData") else { - return Err(LoadSchematicError::MissingBlockData); - }; - - let data: Result, LoadSchematicError> = - VarIntReader(block_data.iter().map(|byte| *byte as u8)) - .map(|val| Ok(palette[&val?])) + let blocks: Option> = match root.get("Blocks") { + Some(Value::Compound(blocks)) => { + let Some(Value::Compound(palette)) = blocks.get("Palette") else { + return Err(LoadSchematicError::MissingBlockPalette); + }; + let palette: Result, _> = palette + .into_iter() + .map(|(state, value)| { + let &Value::Int(i) = value else { + return Err(LoadSchematicError::InvalidBlockPalette); + }; + let state = BlockState::from_str( + state.strip_prefix("minecraft:").unwrap_or(state), + )?; + Ok((i, state)) + }) .collect(); - let data = data?; - - if u16::try_from(data.len()) != Ok(width * height * length) { - return Err(LoadSchematicError::InvalidBlockCount); - } + let palette = palette?; - data.into_boxed_slice() - }; - let block_entities = if let Some(Value::List(List::Compound(block_entities))) = - data.get("BlockEntities") - { - let block_entities: Result, _> = block_entities.iter().map(|block_entity| { - let Some(&[x, y, z]) = block_entity.get("Pos").and_then(|val| val.as_int_array()).map(|arr| arr.as_slice()) else { - return Err(LoadSchematicError::MissingBlockEntityPos); + let Some(Value::ByteArray(data)) = blocks.get("Data") else { + return Err(LoadSchematicError::MissingBlockData); }; - let pos = BlockPos::new(x, y, z); - let kind = { - let Some(Value::String(id)) = block_entity.get("Id") else { - return Err(LoadSchematicError::MissingBlockEntityId); - }; - if id.is_empty() { - block_data[(x + z * width as i32 + y * width as i32 * length as i32) as usize].block_entity_kind().ok_or(LoadSchematicError::MissingBlockEntityId)? - } else { - let Ok(id) = Ident::new(id) else { + let data: Result, LoadSchematicError> = + VarIntReader(data.iter().map(|byte| *byte as u8)) + .map(|val| { + let state = palette[&val?]; + Ok(Block { + state, + block_entity: None, + }) + }) + .collect(); + let mut data = data?; + if u16::try_from(data.len()) != Ok(width * height * length) { + return Err(LoadSchematicError::InvalidBlockCount); + } + if let Some(Value::List(List::Compound(block_entities))) = + blocks.get("BlockEntities") + { + for block_entity in block_entities { + let Some(&[x, y, z]) = block_entity.get("Pos").and_then(|val| val.as_int_array()).map(|arr| arr.as_slice()) else { + return Err(LoadSchematicError::MissingBlockEntityPos); + }; + let Some(Value::String(id)) = block_entity.get("Id") else { + return Err(LoadSchematicError::MissingBlockEntityId); + }; + + let Ok(id) = Ident::new(&id[..]) else { return Err(LoadSchematicError::InvalidBlockEntityId(id.clone())); }; let Some(kind) = BlockEntityKind::from_ident(id.as_str_ident()) else { return Err(LoadSchematicError::UnknownBlockEntity(id.to_string())); }; - kind + let Some(Value::Compound(nbt)) = block_entity.get("Data") else { + return Err(LoadSchematicError::MissingBlockEntityData); + }; + let block_entity = BlockEntity { + kind, + nbt: nbt.clone(), + }; + data[(x + z * width as i32 + y * width as i32 * length as i32) as usize] + .block_entity + .replace(block_entity); } - }; - let mut nbt = block_entity.clone(); - nbt.remove("Pos"); - nbt.remove("Id"); - Ok(BlockEntity { pos, kind, nbt }) - }).collect(); - - block_entities?.into_boxed_slice() - } else { - Box::new([]) - }; - - let Some(Value::Compound(biome_palette)) = data.get("BiomePalette") else { - return Err(LoadSchematicError::MissingBiomePalette); + } + Some(data.into_boxed_slice()) + } + _ => None, }; - let biome_palette: Result, _> = biome_palette - .iter() - .map(|(biome, value)| { - let &Value::Int(i) = value else { - return Err(LoadSchematicError::InvalidBiomePalette); - }; - let Ok(ident) = Ident::new(biome.clone()) else { - return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); - }; - Ok((i, ident)) - }) - .collect(); - let biome_palette = biome_palette?; - let Some(Value::ByteArray(biome_data)) = data.get("BiomeData") else { - return Err(LoadSchematicError::MissingBiomeData); - }; - let biome_data: Result, LoadSchematicError> = - VarIntReader(biome_data.iter().map(|byte| *byte as u8)) - .map(|val| Ok(&biome_palette[&val?])) - .collect(); - let biome_data = biome_data?; - - let (biome_palette, biome_data) = { - let mut palette = vec![]; - let mut map = HashMap::new(); - let biome_data: Vec<_> = biome_data - .into_iter() - .map(|biome| match map.entry(biome) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let idx = palette.len(); - palette.push(biome.to_string_ident()); - entry.insert(idx); - idx - } - }) - .collect(); + let biomes = match root.get("Biomes") { + Some(Value::Compound(biomes)) => { + let Some(Value::Compound(palette)) = biomes.get("Palette") else { + return Err(LoadSchematicError::MissingBiomePalette); + }; + let palette: Result, _> = palette + .iter() + .map(|(biome, value)| { + let &Value::Int(i) = value else { + return Err(LoadSchematicError::InvalidBiomePalette); + }; + let Ok(ident) = Ident::new(biome.clone()) else { + return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); + }; + Ok((i, ident)) + }) + .collect(); + let palette = palette?; + let Some(Value::ByteArray(data)) = biomes.get("Data") else { + return Err(LoadSchematicError::MissingBiomeData); + }; + let data: Result, LoadSchematicError> = + VarIntReader(data.iter().map(|byte| *byte as u8)) + .map(|val| Ok(&palette[&val?])) + .collect(); + let data = data?; + + let mut palette = vec![]; + let mut map = HashMap::new(); + let data: Vec<_> = data + .into_iter() + .map(|biome| match map.entry(biome) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let idx = palette.len(); + palette.push(biome.to_string_ident()); + entry.insert(idx); + idx + } + }) + .collect(); - (palette.into_boxed_slice(), biome_data.into_boxed_slice()) + let biomes = Biomes { + palette: palette.into_boxed_slice(), + data: data.into_boxed_slice(), + }; + Some(biomes) + } + _ => None, }; Ok(Self { @@ -322,10 +337,8 @@ impl Schematic { height, length, offset, - block_data, - block_entities, - biome_palette, - biome_data, + blocks, + biomes, }) } @@ -333,86 +346,73 @@ impl Schematic { where F: FnMut(Ident<&str>) -> BiomeId, { - let blocks = self.block_data.iter().enumerate().map(|(idx, state)| { - let idx = u16::try_from(idx).unwrap(); - let y = idx / (self.width * self.length); - let z = (idx % (self.width * self.length)) / self.width; - let x = (idx % (self.width * self.length)) % self.width; - - ([x, y, z], state) - }); - let min_y = instance.min_y(); - for ([x, y, z], state) in blocks { - let block_pos = BlockPos::new( - x as i32 + origin.x - self.offset.x, - y as i32 + origin.y - self.offset.y, - z as i32 + origin.z - self.offset.z, - ); - let chunk = instance - .chunk_entry(ChunkPos::from_block_pos(block_pos)) - .or_default(); - chunk.set_block_state( - block_pos.x.rem_euclid(16) as usize, - (block_pos.y - min_y) as usize, - block_pos.z.rem_euclid(16) as usize, - *state, - ); - } - - for BlockEntity { - pos: BlockPos { x, y, z }, - kind, - nbt, - } in self.block_entities.iter() - { - let block_pos = BlockPos::new( - x + origin.x - self.offset.x, - y + origin.y - self.offset.y, - z + origin.z - self.offset.z, - ); - let chunk = instance - .chunk_entry(ChunkPos::from_block_pos(block_pos)) - .or_default(); - let x = block_pos.x.rem_euclid(16) as usize; - let y = (block_pos.y - min_y) as usize; - let z = block_pos.z.rem_euclid(16) as usize; - chunk.set_block_entity( - x, - y, - z, - ValenceBlockEntity { - kind: *kind, - nbt: nbt.clone(), + if let Some(blocks) = &self.blocks { + let blocks = blocks.iter().enumerate().map(|(idx, block)| { + let idx = u16::try_from(idx).unwrap(); + let y = idx / (self.width * self.length); + let z = (idx % (self.width * self.length)) / self.width; + let x = (idx % (self.width * self.length)) % self.width; + + ([x, y, z], block) + }); + + for ( + [x, y, z], + Block { + state, + block_entity, }, - ); + ) in blocks + { + let block_pos = BlockPos::new( + x as i32 + origin.x + self.offset.x, + y as i32 + origin.y + self.offset.y, + z as i32 + origin.z + self.offset.z, + ); + let chunk = instance + .chunk_entry(ChunkPos::from_block_pos(block_pos)) + .or_default(); + let block = match block_entity { + Some(BlockEntity { kind, nbt }) if Some(*kind) == state.block_entity_kind() => { + ValenceBlock::with_nbt(*state, nbt.clone()) + } + _ => ValenceBlock::new(*state), + }; + chunk.set_block( + block_pos.x.rem_euclid(16) as usize, + (block_pos.y - min_y) as usize, + block_pos.z.rem_euclid(16) as usize, + block, + ); + } } - let section_count = instance.section_count(); - for ([x, z], biome) in self - .biome_data - .iter() - .map(|biome| self.biome_palette[*biome].as_str_ident()) - .map(map_biome) - .enumerate() - .map(|(idx, biome)| { - let idx = u16::try_from(idx).unwrap(); - let z = idx / self.width; - let x = idx % self.width; - - ([x, z], biome) - }) - { - let x = x as i32 + origin.x - self.offset.x; - let z = z as i32 + origin.z - self.offset.z; - let chunk = instance - .chunk_entry(ChunkPos::at(x as f64, z as f64)) - .or_default(); - - for y in 0..section_count * 4 { + if let Some(Biomes { palette, data }) = &self.biomes { + for ([x, y, z], biome) in data + .iter() + .map(|biome| palette[*biome].as_str_ident()) + .map(map_biome) + .enumerate() + .map(|(idx, biome)| { + let idx = u16::try_from(idx).unwrap(); + let y = idx / (self.width * self.length); + let z = (idx % (self.width * self.length)) / self.width; + let x = (idx % (self.width * self.length)) % self.width; + + ([x, y, z], biome) + }) + { + let x = x as i32 + origin.x + self.offset.x; + let y = y as i32 + origin.y + self.offset.y; + let z = z as i32 + origin.z + self.offset.z; + let chunk = instance + .chunk_entry(ChunkPos::at(x as f64, z as f64)) + .or_default(); + chunk.set_biome( (x / 4).rem_euclid(4) as usize, - y, + (y - min_y / 4) as usize, (z / 4).rem_euclid(4) as usize, biome, ); From 8ee6c3fcd9a97602114c6e8560f8298dcb7df786 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 26 Feb 2023 15:40:49 +0100 Subject: [PATCH 03/22] add entities to schematics --- crates/valence_schem/src/lib.rs | 101 +++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 6b4cbe4d8..fa43592e1 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -25,7 +25,7 @@ use std::path::PathBuf; use std::str::FromStr; use flate2::bufread::GzDecoder; -use glam::IVec3; +use glam::{DVec3, IVec3}; use thiserror::Error; use valence_biome::BiomeId; use valence_block::{BlockEntityKind, BlockState, ParseBlockStateError}; @@ -45,7 +45,7 @@ pub struct Schematic { pub offset: IVec3, blocks: Option>, biomes: Option, - // TODO: pub entities: Option>, + pub entities: Option>, } #[derive(Debug, Clone, PartialEq)] @@ -57,7 +57,7 @@ pub struct Block { #[derive(Debug, Clone, PartialEq)] pub struct BlockEntity { pub kind: BlockEntityKind, - pub nbt: Compound, + pub data: Compound, } #[derive(Debug, Clone, PartialEq)] @@ -66,6 +66,14 @@ pub struct Biomes { data: Box<[usize]>, } +#[derive(Debug, Clone, PartialEq)] +pub struct Entity { + pub pos: DVec3, + /// The id of the entity type + pub id: Ident, + pub data: Option, +} + #[derive(Debug, Error)] pub enum LoadSchematicError { #[error(transparent)] @@ -116,6 +124,9 @@ pub enum LoadSchematicError { #[error("missing block entity pos")] MissingBlockEntityPos, + #[error("invalid block entity pos {0:?}")] + InvalidBlockEntityPos(Vec), + #[error("missing block entity id")] MissingBlockEntityId, @@ -125,9 +136,6 @@ pub enum LoadSchematicError { #[error("unknown block entity '{0}'")] UnknownBlockEntity(String), - #[error("missing block entity data")] - MissingBlockEntityData, - #[error("missing biome palette")] MissingBiomePalette, @@ -139,6 +147,21 @@ pub enum LoadSchematicError { #[error("missing biome data")] MissingBiomeData, + + #[error("invalid biome count")] + InvalidBiomeCount, + + #[error("missing entity pos")] + MissingEntityPos, + + #[error("invalid entity pos {0:?}")] + InvalidEntityPos(Vec), + + #[error("missing entity id")] + MissingEntityId, + + #[error("invalid entity id '{0}'")] + InvalidEntityId(String), } struct VarIntReader>(I); @@ -250,26 +273,28 @@ impl Schematic { blocks.get("BlockEntities") { for block_entity in block_entities { - let Some(&[x, y, z]) = block_entity.get("Pos").and_then(|val| val.as_int_array()).map(|arr| arr.as_slice()) else { + let Some(Value::IntArray(pos)) = block_entity.get("Pos") else { return Err(LoadSchematicError::MissingBlockEntityPos); }; + let [x, y, z] = pos[..] else { + return Err(LoadSchematicError::InvalidBlockEntityPos(pos.clone())); + }; + let Some(Value::String(id)) = block_entity.get("Id") else { return Err(LoadSchematicError::MissingBlockEntityId); }; - let Ok(id) = Ident::new(&id[..]) else { return Err(LoadSchematicError::InvalidBlockEntityId(id.clone())); }; let Some(kind) = BlockEntityKind::from_ident(id.as_str_ident()) else { return Err(LoadSchematicError::UnknownBlockEntity(id.to_string())); }; - let Some(Value::Compound(nbt)) = block_entity.get("Data") else { - return Err(LoadSchematicError::MissingBlockEntityData); - }; - let block_entity = BlockEntity { - kind, - nbt: nbt.clone(), + + let nbt = match block_entity.get("Data") { + Some(Value::Compound(nbt)) => nbt.clone(), + _ => Compound::with_capacity(0), }; + let block_entity = BlockEntity { kind, data: nbt }; data[(x + z * width as i32 + y * width as i32 * length as i32) as usize] .block_entity .replace(block_entity); @@ -322,6 +347,10 @@ impl Schematic { }) .collect(); + if u16::try_from(data.len()) != Ok(width * height * length) { + return Err(LoadSchematicError::InvalidBiomeCount); + } + let biomes = Biomes { palette: palette.into_boxed_slice(), data: data.into_boxed_slice(), @@ -331,6 +360,43 @@ impl Schematic { _ => None, }; + let entities: Option> = match root.get("Entities") { + Some(Value::List(List::Compound(entities))) => { + let entities: Result, _> = entities + .iter() + .map(|entity| { + let Some(Value::List(List::Double(pos))) = entity.get("Pos") else { + return Err(LoadSchematicError::MissingEntityPos); + }; + let [x, y, z] = pos[..] else { + return Err(LoadSchematicError::InvalidEntityPos(pos.clone())); + }; + let pos = DVec3::new(x, y, z); + + let Some(Value::String(id)) = entity.get("Id") else { + return Err(LoadSchematicError::MissingEntityId); + }; + let Ok(id) = Ident::new(id.clone()) else { + return Err(LoadSchematicError::InvalidEntityId(id.clone())); + }; + + let data = match entity.get("Data") { + Some(Value::Compound(data)) => Some(data.clone()), + _ => None, + }; + + Ok(Entity { + pos, + id: id.to_string_ident(), + data, + }) + }) + .collect(); + Some(entities?.into_boxed_slice()) + } + _ => None, + }; + Ok(Self { metadata, width, @@ -339,6 +405,7 @@ impl Schematic { offset, blocks, biomes, + entities, }) } @@ -374,7 +441,9 @@ impl Schematic { .chunk_entry(ChunkPos::from_block_pos(block_pos)) .or_default(); let block = match block_entity { - Some(BlockEntity { kind, nbt }) if Some(*kind) == state.block_entity_kind() => { + Some(BlockEntity { kind, data: nbt }) + if Some(*kind) == state.block_entity_kind() => + { ValenceBlock::with_nbt(*state, nbt.clone()) } _ => ValenceBlock::new(*state), @@ -418,5 +487,7 @@ impl Schematic { ); } } + + // TODO: Spawn entities } } From b7d671aa896d7a9e89a80e954e1597e7ad312439 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Wed, 1 Mar 2023 11:59:15 +0100 Subject: [PATCH 04/22] Implement schem saving --- crates/valence/examples/schem_saving.rs | 392 ++++++++++++++++++++++++ crates/valence_schem/src/lib.rs | 353 ++++++++++++++++++++- 2 files changed, 743 insertions(+), 2 deletions(-) create mode 100644 crates/valence/examples/schem_saving.rs diff --git a/crates/valence/examples/schem_saving.rs b/crates/valence/examples/schem_saving.rs new file mode 100644 index 000000000..80f71b784 --- /dev/null +++ b/crates/valence/examples/schem_saving.rs @@ -0,0 +1,392 @@ +use std::fs::File; +use std::path::PathBuf; + +use valence::packet::Encode; +use valence::prelude::*; +use valence_client::misc::InteractBlock; +use valence_inventory::ClientInventoryState; +use valence_nbt::compound; +use valence_schem::Schematic; + +const FLOOR_Y: i32 = 64; +const SPAWN_POS: DVec3 = DVec3::new(0.5, FLOOR_Y as f64 + 1.0, 0.5); + +pub fn main() { + tracing_subscriber::fmt().init(); + + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_systems(( + init_clients, + first_pos, + second_pos, + origin, + copy_schem, + paste_schem, + save_schem, + place_blocks, + break_blocks, + )) + .add_system(despawn_disconnected_clients) + .run(); +} + +#[derive(Debug, Clone, Copy, Component)] +struct FirstPos(BlockPos); +#[derive(Debug, Clone, Copy, Component)] +struct SecondPos(BlockPos); +#[derive(Debug, Clone, Copy, Component)] +struct Origin(BlockPos); +#[derive(Debug, Clone, Component)] +struct Clipboard(Schematic); + +fn first_pos( + mut clients: Query<( + &mut Client, + &Inventory, + Option<&mut FirstPos>, + &ClientInventoryState, + )>, + mut block_breaks: EventReader, + mut commands: Commands, +) { + for Digging { + client: entity, + position, + .. + } in block_breaks.iter() + { + let Ok((mut client, inv, pos, inv_state)) = clients.get_mut(*entity) else { + continue; + }; + let slot = inv.slot(inv_state.held_item_slot()); + if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { + let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); + if changed { + client.send_message(format!( + "Set the primary pos to ({}, {}, {})", + position.x, position.y, position.z, + )); + } + commands.entity(*entity).insert(FirstPos(*position)); + } + } +} + +fn second_pos( + mut clients: Query<( + &mut Client, + &Inventory, + Option<&mut SecondPos>, + &ClientInventoryState, + )>, + mut interacts: EventReader, + mut commands: Commands, +) { + for InteractBlock { + client: entity, + hand, + position, + .. + } in interacts.iter() + { + if *hand == Hand::Main { + let Ok((mut client, inv, pos, inv_state)) = clients.get_mut(*entity) else { + continue; + }; + let slot = inv.slot(inv_state.held_item_slot()); + if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { + let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); + if changed { + client.send_message(format!( + "Set the secondary pos to ({}, {}, {})", + position.x, position.y, position.z, + )); + } + commands.entity(*entity).insert(SecondPos(*position)); + } + } + } +} + +fn origin( + mut clients: Query<( + &mut Client, + &Inventory, + Option<&mut Origin>, + &ClientInventoryState, + )>, + mut interacts: EventReader, + mut commands: Commands, +) { + for InteractBlock { + client: entity, + hand, + position, + .. + } in interacts.iter() + { + if *hand == Hand::Main { + let Ok((mut client, inv, pos, inv_state)) = clients.get_mut(*entity) else { + continue; + }; + let slot = inv.slot(inv_state.held_item_slot()); + if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Stick) { + let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); + if changed { + client.send_message(format!( + "Set the origin to ({}, {}, {})", + position.x, position.y, position.z, + )); + } + commands.entity(*entity).insert(Origin(*position)); + } + } + } +} + +#[allow(clippy::type_complexity)] +fn copy_schem( + mut clients: Query<( + &mut Client, + &Inventory, + Option<&FirstPos>, + Option<&SecondPos>, + Option<&Origin>, + &ClientInventoryState, + &Position, + &Location, + &Username, + )>, + instances: Query<&Instance>, + mut interacts: EventReader, + biome_registry: Res, + biomes: Query<&Biome>, + mut commands: Commands, +) { + for InteractBlock { + client: entity, + hand, + .. + } in interacts.iter() + { + if *hand != Hand::Main { + continue; + } + let Ok((mut client, inv, pos1, pos2, origin, inv_state, &Position(pos), &Location(instance), Username(username))) = clients.get_mut(*entity) else { + continue; + }; + let slot = inv.slot(inv_state.held_item_slot()); + if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Paper) { + let Some((FirstPos(pos1), SecondPos(pos2))) = pos1.zip(pos2) else { + client.send_message("Specify both positions first"); + continue; + }; + let origin = origin.map(|pos| pos.0).unwrap_or(BlockPos::at(pos)); + + let Ok(instance) = instances.get(instance) else { + continue; + }; + let mut schematic = Schematic::copy(instance, (*pos1, *pos2), origin, |id| { + let biome = biome_registry.get_by_id(id).unwrap(); + biomes.get(biome).unwrap().name.clone() + }); + schematic.metadata.replace(compound! {"Author" => username}); + commands.entity(*entity).insert(Clipboard(schematic)); + client.send_message("Copied"); + } + } +} + +fn paste_schem( + mut instances: Query<&mut Instance>, + mut clients: Query<( + &mut Client, + &Inventory, + Option<&Clipboard>, + &Location, + &ClientInventoryState, + &Position, + )>, + mut interacts: EventReader, +) { + for InteractBlock { + client: entity, + hand, + .. + } in interacts.iter() + { + if *hand != Hand::Main { + continue; + } + let Ok((mut client, inv, clipboard, &Location(instance), inv_state, &Position(position))) = clients.get_mut(*entity) else { + continue; + }; + let Ok(mut instance) = instances.get_mut(instance) else { + continue; + }; + let slot = inv.slot(inv_state.held_item_slot()); + if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Feather) { + let Some(Clipboard(schematic)) = clipboard else { + client.send_message("Copy something to clipboard first!"); + continue; + }; + let pos = BlockPos::at(position); + schematic.paste(&mut instance, pos, |_| BiomeId::default()); + client.send_message(format!( + "Pasted schematic at ({} {} {})", + pos.x, pos.y, pos.z + )); + } + } +} + +fn save_schem( + mut clients: Query<( + &mut Client, + &Inventory, + Option<&Clipboard>, + &ClientInventoryState, + &Username, + )>, + mut interacts: EventReader, +) { + for InteractBlock { + client: entity, + hand, + .. + } in interacts.iter() + { + if *hand != Hand::Main { + continue; + } + let Ok((mut client, inv, clipboard, inv_state, Username(username))) = clients.get_mut(*entity) else { + continue; + }; + let slot = inv.slot(inv_state.held_item_slot()); + if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::MusicDiscStal) { + let Some(Clipboard(schematic)) = clipboard else { + client.send_message("Copy something to clipboard first!"); + continue; + }; + let nbt = schematic.serialize(); + let path = PathBuf::from(format!("{username}.schem")); + let file = File::create(&path).unwrap(); + nbt.encode(file).unwrap(); + client.send_message(format!("Saved schem to {}", path.display())); + } + } +} + +fn place_blocks( + clients: Query<(&Inventory, &ClientInventoryState), With>, + mut instances: Query<&mut Instance>, + mut events: EventReader, +) { + let mut instance = instances.single_mut(); + + for event in events.iter() { + let Ok((inventory, inv_state)) = clients.get(event.client) else { + continue; + }; + if event.hand != Hand::Main { + continue; + } + + let Some(stack) = inventory.slot(inv_state.held_item_slot()) else { + continue; + }; + + let Some(block_kind) = BlockKind::from_item_kind(stack.item) else { + continue; + }; + + let pos = event.position.get_in_direction(event.face); + instance.set_block(pos, block_kind.to_state()); + } +} + +fn break_blocks(mut instances: Query<&mut Instance>, mut events: EventReader) { + let mut instance = instances.single_mut(); + + for event in events.iter() { + instance.set_block(event.position, BlockState::AIR); + } +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Query<&DimensionType>, + biomes: Query<&Biome>, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + + for x in -16..=16 { + for z in -16..=16 { + let pos = BlockPos::new(SPAWN_POS.x as i32 + x, FLOOR_Y, SPAWN_POS.z as i32 + z); + instance + .chunk_entry(ChunkPos::from_block_pos(pos)) + .or_default(); + instance.set_block(pos, BlockState::QUARTZ_BLOCK); + } + } + + commands.spawn(instance); +} + +fn init_clients( + mut clients: Query< + (&mut Inventory, &mut Location, &mut Position, &mut GameMode), + Added, + >, + instances: Query>, +) { + for (mut inv, mut loc, mut pos, mut game_mode) in &mut clients { + *game_mode = GameMode::Creative; + loc.0 = instances.single(); + pos.set(SPAWN_POS); + + inv.set_slot( + 36, + Some(ItemStack::new( + ItemKind::WoodenAxe, + 1, + Some(compound! {"display" => compound! {"Name" => "Position Setter".not_italic()}}), + )), + ); + inv.set_slot( + 37, + Some(ItemStack::new( + ItemKind::Stick, + 1, + Some(compound! {"display" => compound! {"Name" => "Origin Setter".not_italic()}}), + )), + ); + inv.set_slot( + 38, + Some(ItemStack::new( + ItemKind::Paper, + 1, + Some(compound! {"display" => compound! {"Name" => "Copy Schematic".not_italic()}}), + )), + ); + inv.set_slot( + 39, + Some(ItemStack::new( + ItemKind::Feather, + 1, + Some(compound! {"display" => compound! {"Name" => "Paste Schematic".not_italic()}}), + )), + ); + inv.set_slot( + 40, + Some(ItemStack::new( + ItemKind::MusicDiscStal, + 1, + Some(compound! {"display" => compound! {"Name" => "Save Schematic".not_italic().color(Color::WHITE)}}), + )), + ); + } +} diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index fa43592e1..356a2a8ba 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -20,7 +20,7 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fs::File; -use std::io::{self, BufReader, Read}; +use std::io::{self, BufReader, Read, Write}; use std::path::PathBuf; use std::str::FromStr; @@ -33,8 +33,9 @@ use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_core::packet::var_int::{VarInt, VarIntDecodeError}; +use valence_core::packet::Encode; use valence_instance::{Block as ValenceBlock, Instance}; -use valence_nbt::{Compound, List, Value}; +use valence_nbt::{compound, Compound, List, Value}; #[derive(Debug, Clone, PartialEq)] pub struct Schematic { @@ -190,6 +191,18 @@ impl> Iterator for VarIntReader { } } +struct VarIntWriteWrapper<'a>(&'a mut Vec); +impl<'a> Write for VarIntWriteWrapper<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend(buf.iter().map(|byte| *byte as i8)); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + impl Schematic { pub fn load(path: PathBuf) -> Result { let file = File::open(path)?; @@ -409,6 +422,125 @@ impl Schematic { }) } + pub fn serialize(&self) -> Compound { + let mut compound = compound! { + "Version" => 3, + "DataVersion" => 3218, + "Width" => self.width as i16, + "Height" => self.height as i16, + "Length" => self.length as i16, + }; + if let Some(metadata) = &self.metadata { + compound.insert("Metadata", metadata.clone()); + } + match self.offset { + IVec3::ZERO => {} + IVec3 { x, y, z } => { + compound.insert("Offset", vec![x, y, z]); + } + } + if let Some(blocks) = &self.blocks { + let blocks: Compound = { + let mut palette = HashMap::new(); + let mut data: Vec = vec![]; + let mut block_entities = vec![]; + for (idx, block) in blocks.iter().enumerate() { + let palette_len = palette.len(); + let i = *palette.entry(block.state).or_insert(palette_len); + struct WriteWrapper<'a>(&'a mut Vec); + impl<'a> Write for WriteWrapper<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend(buf.iter().map(|byte| *byte as i8)); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + VarInt(i as i32).encode(WriteWrapper(&mut data)).unwrap(); + if let Some(BlockEntity { kind, data }) = &block.block_entity { + let idx = u16::try_from(idx).unwrap(); + let y = idx / (self.width * self.length); + let z = (idx % (self.width * self.length)) / self.width; + let x = (idx % (self.width * self.length)) % self.width; + + let mut block_entity = compound! { + "Pos" => vec![x as i32, y as i32, z as i32], + "Id" => kind.ident(), + }; + if !data.is_empty() { + block_entity.insert("Data", data.clone()); + } + block_entities.push(block_entity); + } + } + + let palette: Compound = palette + .into_iter() + .map(|(state, idx)| (state.to_string(), Value::Int(idx as i32))) + .collect(); + let mut blocks = compound! { + "Palette" => palette, + "Data" => data, + }; + if !block_entities.is_empty() { + blocks.insert("BlockEntities", Value::List(List::Compound(block_entities))); + } + blocks + }; + compound.insert("Blocks", blocks); + } + + if let Some(biomes) = &self.biomes { + let palette: Compound = biomes + .palette + .iter() + .enumerate() + .map(|(idx, val)| (val.to_string(), Value::Int(idx as i32))) + .collect(); + let mut data = vec![]; + for i in biomes.data.iter() { + VarInt(*i as i32) + .encode(VarIntWriteWrapper(&mut data)) + .unwrap(); + } + compound.insert( + "Biomes", + compound! { + "Palette" => palette, + "Data" => data, + }, + ); + } + + if let Some(entities) = &self.entities { + let entities = entities + .iter() + .map( + |Entity { + pos: DVec3 { x, y, z }, + id, + data, + }| { + let mut compound = compound! { + "Pos" => Value::List(List::Double(vec![*x, *y, *z])), + "Id" => id.clone(), + }; + if let Some(data) = data { + compound.insert("Data", data.clone()); + } + compound + }, + ) + .collect(); + compound.insert("Entities", Value::List(List::Compound(entities))); + } + + compound + } + pub fn paste(&self, instance: &mut Instance, origin: BlockPos, map_biome: F) where F: FnMut(Ident<&str>) -> BiomeId, @@ -490,4 +622,221 @@ impl Schematic { // TODO: Spawn entities } + + pub fn copy( + instance: &Instance, + corners: (BlockPos, BlockPos), + origin: BlockPos, + mut map_biome: F, + ) -> Self + where + F: FnMut(BiomeId) -> Ident, + { + let min = BlockPos::new( + corners.0.x.min(corners.1.x), + corners.0.y.min(corners.1.y), + corners.0.z.min(corners.1.z), + ); + let max = BlockPos::new( + corners.0.x.max(corners.1.x), + corners.0.y.max(corners.1.y), + corners.0.z.max(corners.1.z), + ); + let width = u16::try_from(max.x - min.x + 1).expect("width too large"); + let height = u16::try_from(max.y - min.y + 1).expect("height too large"); + let length = u16::try_from(max.z - min.z + 1).expect("length too large"); + let offset = IVec3::new(min.x - origin.x, min.y - origin.y, min.z - origin.z); + let blocks: Vec<_> = (min.y..=max.y) + .flat_map(|y| { + (min.z..=max.z).flat_map(move |z| { + (min.x..=max.x).map(move |x| { + let Some(block) = instance.block([x, y, z]) else { + todo!("asd"); + }; + let state = block.state(); + let block_entity = block + .nbt() + .and_then(|data| Some((state.block_entity_kind()?, data.clone()))) + .map(|(kind, data)| BlockEntity { kind, data }); + Block { + state, + block_entity, + } + }) + }) + }) + .collect(); + let biomes = { + let mut palette = vec![]; + let mut map = HashMap::new(); + let data: Vec<_> = (min.x..=max.x) + .flat_map(|x| { + (min.z..=max.z).flat_map(move |z| { + (min.y..=max.y).map(move |y| { + instance + .chunk(ChunkPos::from_block_pos(BlockPos::new(x, y, z))) + .unwrap() + .biome( + x.rem_euclid(16) as usize / 4, + (y - instance.min_y()) as usize / 4, + z.rem_euclid(16) as usize / 4, + ) + }) + }) + }) + .map(|biome| match map.entry(biome) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let idx = palette.len(); + palette.push(map_biome(biome)); + entry.insert(idx); + idx + } + }) + .collect(); + + Biomes { + palette: palette.into_boxed_slice(), + data: data.into_boxed_slice(), + } + }; + Self { + metadata: None, + width, + height, + length, + offset, + blocks: Some(blocks.into_boxed_slice()), + biomes: Some(biomes), + entities: None, // TODO + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn schematic() { + let mut app = App::new(); + app.add_plugin(ServerPlugin::new(())); + let mut instance = app + .world + .resource::() + .new_instance(DimensionId::default()); + + for x in -1..=0 { + for z in -1..=0 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + instance.set_block([5, 1, -1], BlockState::GLOWSTONE); + instance.set_block([5, 2, -1], BlockState::STONE); + instance.set_block([5, 2, -2], BlockState::GLOWSTONE); + instance.set_block([4, 2, -1], BlockState::LAPIS_BLOCK); + instance.set_block([6, 2, -1], BlockState::STONE); + instance.set_block( + [5, 3, -1], + Block::with_nbt( + BlockState::OAK_SIGN, + compound! {"Text1" => "abc".into_text()}, + ), + ); + instance.set_block( + [5, 2, 0], + BlockState::ANDESITE_WALL + .set(PropName::Up, PropValue::True) + .set(PropName::North, PropValue::Low), + ); + + let schematic = Schematic::copy( + &instance, + (BlockPos::new(4, 3, -1), BlockPos::new(6, 1, 0)), + BlockPos::new(5, 3, 0), + |_| ident!("minecraft:plains"), + ); + + schematic.paste(&mut instance, BlockPos::new(15, 18, 16), |_| { + BiomeId::default() + }); + + let block = instance.block([15, 18, 15]).unwrap(); + assert_eq!(block.state(), BlockState::OAK_SIGN); + assert_eq!(block.nbt(), Some(&compound! {"Text1" => "abc".into_text()})); + + let block = instance.block([15, 17, 16]).unwrap(); + assert_eq!( + block.state(), + BlockState::ANDESITE_WALL + .set(PropName::Up, PropValue::True) + .set(PropName::North, PropValue::Low) + ); + assert_eq!(block.nbt(), None); + + let block = instance.block([15, 17, 15]).unwrap(); + assert_eq!(block.state(), BlockState::STONE); + assert_eq!(block.nbt(), None); + + let block = instance.block([15, 17, 14]).unwrap(); + assert_eq!(block.state(), BlockState::AIR); + assert_eq!(block.nbt(), None); + + let block = instance.block([14, 17, 15]).unwrap(); + assert_eq!(block.state(), BlockState::LAPIS_BLOCK); + assert_eq!(block.nbt(), None); + + let block = instance.block([16, 17, 15]).unwrap(); + assert_eq!(block.state(), BlockState::STONE); + assert_eq!(block.nbt(), None); + + let block = instance.block([15, 16, 15]).unwrap(); + assert_eq!(block.state(), BlockState::GLOWSTONE); + assert_eq!(block.nbt(), None); + + let mut schematic = schematic; + schematic.metadata.replace(compound! {"A" => 123}); + let nbt = schematic.serialize(); + assert_eq!( + nbt, + compound! { + "Version" => 3, + "DataVersion" => 3218, + "Metadata" => compound! { + "A" => 123, + }, + "Width" => 3i16, + "Height" => 3i16, + "Length" => 2i16, + "Offset" => vec![-1, -2, -1], + "Blocks" => compound! { + "Data" => vec![0i8, 1, 0, 0, 0, 0, 2, 3, 3, 0, 4, 0, 0, 5, 0, 0, 0, 0], + "Palette" => compound! { + "air" => 0, + "glowstone" => 1, + "lapis_block" => 2, + "stone" => 3, + "andesite_wall[east=none, north=low, south=none, up=true, waterlogged=false, west=none]" => 4, + "oak_sign[rotation=0, waterlogged=false]" => 5, + }, + "BlockEntities" => Value::List(List::Compound(vec![ + compound! { + "Data" => compound!{ + "Text1" => "abc".into_text(), + }, + "Id" => "sign", + "Pos" => vec![1, 2, 0], + }, + ])) + }, + "Biomes" => compound! { + "Data" => vec![0i8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "Palette" => compound! { + "minecraft:plains" => 0, + } + }, + } + ); + } } From 86f7b9c30ec7958034e55791283d2c96da27ef63 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Fri, 3 Mar 2023 19:03:19 +0100 Subject: [PATCH 05/22] Make setup not exclusive systems --- crates/valence/examples/schem_loading.rs | 75 ++++++++++++------------ 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/crates/valence/examples/schem_loading.rs b/crates/valence/examples/schem_loading.rs index a3dc83262..0666689ab 100644 --- a/crates/valence/examples/schem_loading.rs +++ b/crates/valence/examples/schem_loading.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; use clap::Parser; +use valence::bevy_app::AppExit; +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; use valence::prelude::*; use valence_schem::Schematic; @@ -13,66 +16,66 @@ struct Cli { path: PathBuf, } -#[derive(Resource)] -struct SchemRes(Schematic); - pub fn main() { tracing_subscriber::fmt().init(); + App::new() + .add_plugin(ServerPlugin::new(())) + .add_startup_system(setup) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_set(PlayerList::default_system_set()) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); +} + +fn setup(mut commands: Commands, server: Res, mut exit: EventWriter) { let Cli { path } = Cli::parse(); if !path.exists() { eprintln!("File `{}` does not exist. Exiting.", path.display()); - return; + exit.send_default(); } else if !path.is_file() { eprintln!("`{}` is not a file. Exiting.", path.display()); - return; + exit.send_default(); } - let schem = match Schematic::load(path) { - Ok(schem) => schem, + let mut instance = server.new_instance(DimensionId::default()); + + match Schematic::load(path) { + Ok(schem) => { + schem.paste(&mut instance, SPAWN_POS, |_| BiomeId::default()); + } Err(err) => { eprintln!("Error loading schematic: {err}"); - return; + exit.send_default(); } - }; - - App::new() - .insert_resource(SchemRes(schem)) - .add_plugins(DefaultPlugins) - .add_startup_system(setup) - .add_system(init_clients) - .add_system(despawn_disconnected_clients) - .run(); -} - -fn setup( - mut commands: Commands, - dimensions: Query<&DimensionType>, - biomes: Query<&Biome>, - server: Res, - schem: Res, -) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); - - schem.0.paste(&mut instance, BlockPos::new(0, 0, 0), |_| { - BiomeId::default() - }); + } commands.spawn(instance); } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + mut clients: Query<&mut Client, Added>, instances: Query>, + mut commands: Commands, ) { - for (mut loc, mut pos, mut game_mode) in &mut clients { - *game_mode = GameMode::Creative; - loc.0 = instances.single(); - pos.set([ + for mut client in &mut clients { + let instance = instances.single(); + + client.set_flat(true); + client.set_game_mode(GameMode::Creative); + client.set_position([ SPAWN_POS.x as f64 + 0.5, SPAWN_POS.y as f64, SPAWN_POS.z as f64 + 0.5, ]); + client.set_instance(instance); + + commands.spawn(McEntity::with_uuid( + EntityKind::Player, + instance, + client.uuid(), + )); } } From 2a96178175d412a212d76ae15f3d439720a7f37a Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sat, 4 Mar 2023 08:37:51 +0100 Subject: [PATCH 06/22] Make select pos1 not break block --- crates/valence/examples/schem_saving.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/valence/examples/schem_saving.rs b/crates/valence/examples/schem_saving.rs index 80f71b784..95956a72f 100644 --- a/crates/valence/examples/schem_saving.rs +++ b/crates/valence/examples/schem_saving.rs @@ -307,11 +307,25 @@ fn place_blocks( } } -fn break_blocks(mut instances: Query<&mut Instance>, mut events: EventReader) { +fn break_blocks( + mut instances: Query<&mut Instance>, + inventories: Query<(&Inventory, &ClientInventoryState)>, + mut events: EventReader, +) { let mut instance = instances.single_mut(); - for event in events.iter() { - instance.set_block(event.position, BlockState::AIR); + for Digging { + client, position, .. + } in events.iter() + { + let Ok((inv, inv_state)) = inventories.get(*client) else { + continue; + }; + + let slot = inv.slot(inv_state.held_item_slot()); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { + instance.set_block(*position, BlockState::AIR); + } } } From 4ae80baee09ca7a15f31cffa271b94d392223e9a Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sat, 4 Mar 2023 08:50:00 +0100 Subject: [PATCH 07/22] Improve saving --- crates/valence/examples/schem_saving.rs | 6 +---- crates/valence_schem/src/lib.rs | 35 ++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/crates/valence/examples/schem_saving.rs b/crates/valence/examples/schem_saving.rs index 95956a72f..41db41c65 100644 --- a/crates/valence/examples/schem_saving.rs +++ b/crates/valence/examples/schem_saving.rs @@ -1,7 +1,5 @@ -use std::fs::File; use std::path::PathBuf; -use valence::packet::Encode; use valence::prelude::*; use valence_client::misc::InteractBlock; use valence_inventory::ClientInventoryState; @@ -270,10 +268,8 @@ fn save_schem( client.send_message("Copy something to clipboard first!"); continue; }; - let nbt = schematic.serialize(); let path = PathBuf::from(format!("{username}.schem")); - let file = File::create(&path).unwrap(); - nbt.encode(file).unwrap(); + schematic.save(&path).unwrap(); client.send_message(format!("Saved schem to {}", path.display())); } } diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 356a2a8ba..5b7c3d350 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -21,10 +21,12 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fs::File; use std::io::{self, BufReader, Read, Write}; -use std::path::PathBuf; +use std::path::Path; use std::str::FromStr; use flate2::bufread::GzDecoder; +use flate2::write::GzEncoder; +use flate2::Compression; use glam::{DVec3, IVec3}; use thiserror::Error; use valence_biome::BiomeId; @@ -203,8 +205,17 @@ impl<'a> Write for VarIntWriteWrapper<'a> { } } +#[derive(Debug, Error)] +pub enum SaveSchematicError { + #[error(transparent)] + Io(#[from] io::Error), + + #[error(transparent)] + Nbt(#[from] valence_nbt::Error), +} + impl Schematic { - pub fn load(path: PathBuf) -> Result { + pub fn load(path: impl AsRef) -> Result { let file = File::open(path)?; let mut buf = vec![]; @@ -212,6 +223,10 @@ impl Schematic { z.read_to_end(&mut buf)?; let root = valence_nbt::from_binary_slice(&mut buf.as_slice())?.0; + Self::deserialize(&root) + } + + pub fn deserialize(root: &Compound) -> Result { let Some(Value::Compound(root)) = root.get("Schematic") else { return Err(LoadSchematicError::MissingSchematic); }; @@ -422,6 +437,7 @@ impl Schematic { }) } + /// When saving make sure to use gzip pub fn serialize(&self) -> Compound { let mut compound = compound! { "Version" => 3, @@ -538,7 +554,18 @@ impl Schematic { compound.insert("Entities", Value::List(List::Compound(entities))); } - compound + compound! { + "Schematic" => compound, + } + } + + pub fn save(&self, path: impl AsRef) -> Result<(), SaveSchematicError> { + let nbt = self.serialize(); + let file = File::create(path)?; + let mut z = GzEncoder::new(file, Compression::best()); + valence_nbt::to_binary_writer(&mut z, &nbt, "")?; + z.flush()?; + Ok(()) } pub fn paste(&self, instance: &mut Instance, origin: BlockPos, map_biome: F) @@ -613,7 +640,7 @@ impl Schematic { chunk.set_biome( (x / 4).rem_euclid(4) as usize, - (y - min_y / 4) as usize, + ((y - min_y) / 4) as usize, (z / 4).rem_euclid(4) as usize, biome, ); From 314eb0c711f8b034976a60c10378ecef135d366a Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sat, 4 Mar 2023 10:42:12 +0100 Subject: [PATCH 08/22] Fix BlockEntityKind::from_ident --- crates/valence_block/build.rs | 32 ++-- extracted/blocks.json | 160 +++++++++--------- .../valence/extractor/extractors/Blocks.java | 4 +- 3 files changed, 100 insertions(+), 96 deletions(-) diff --git a/crates/valence_block/build.rs b/crates/valence_block/build.rs index ddae3b704..92b472f43 100644 --- a/crates/valence_block/build.rs +++ b/crates/valence_block/build.rs @@ -38,8 +38,8 @@ impl Block { #[derive(Deserialize, Clone, Debug)] struct BlockEntityKind { id: u32, - ident: String, - name: String, + namespace: String, + path: String, } #[derive(Deserialize, Clone, Debug)] @@ -423,10 +423,10 @@ fn build() -> anyhow::Result { let block_entity_kind_variants = block_entity_types .iter() .map(|block_entity| { - let name = ident(block_entity.name.to_pascal_case()); + let name = ident(block_entity.path.to_pascal_case()); let doc = format!( - "The block entity type `{}` (ID {}).", - block_entity.name, block_entity.id + "The block entity type `{}:{}` (ID {}).", + block_entity.namespace, block_entity.path, block_entity.id ); quote! { #[doc = #doc] @@ -439,7 +439,7 @@ fn build() -> anyhow::Result { .iter() .map(|block_entity| { let id = block_entity.id; - let name = ident(block_entity.name.to_pascal_case()); + let name = ident(block_entity.path.to_pascal_case()); quote! { #id => Some(Self::#name), @@ -451,7 +451,7 @@ fn build() -> anyhow::Result { .iter() .map(|block_entity| { let id = block_entity.id; - let name = ident(block_entity.name.to_pascal_case()); + let name = ident(block_entity.path.to_pascal_case()); quote! { Self::#name => #id, @@ -462,11 +462,12 @@ fn build() -> anyhow::Result { let block_entity_kind_from_ident_arms = block_entity_types .iter() .map(|block_entity| { - let name = ident(block_entity.name.to_pascal_case()); - let ident = &block_entity.ident; + let name = ident(block_entity.path.to_pascal_case()); + let namespace = &block_entity.namespace; + let path = &block_entity.path; quote! { - #ident => Some(Self::#name), + (#namespace, #path) => Some(Self::#name), } }) .collect::(); @@ -474,8 +475,11 @@ fn build() -> anyhow::Result { let block_entity_kind_to_ident_arms = block_entity_types .iter() .map(|block_entity| { - let name = ident(block_entity.name.to_pascal_case()); - let ident = &block_entity.ident; + let name = ident(block_entity.path.to_pascal_case()); + let namespace = &block_entity.namespace; + let path = &block_entity.path; + + let ident = format!("{namespace}:{path}"); quote! { Self::#name => ident!(#ident), @@ -939,13 +943,13 @@ fn build() -> anyhow::Result { } pub fn from_ident(ident: Ident<&str>) -> Option { - match ident.as_str() { + match (ident.namespace(), ident.path()) { #block_entity_kind_from_ident_arms _ => None } } - pub fn ident(self) -> Ident<&'static str> { + pub const fn ident(self) -> Ident<&'static str> { match self { #block_entity_kind_to_ident_arms } diff --git a/extracted/blocks.json b/extracted/blocks.json index 59caa8b86..514e496a2 100644 --- a/extracted/blocks.json +++ b/extracted/blocks.json @@ -2,203 +2,203 @@ "block_entity_types": [ { "id": 0, - "ident": "minecraft:furnace", - "name": "furnace" + "namespace": "minecraft", + "path": "furnace" }, { "id": 1, - "ident": "minecraft:chest", - "name": "chest" + "namespace": "minecraft", + "path": "chest" }, { "id": 2, - "ident": "minecraft:trapped_chest", - "name": "trapped_chest" + "namespace": "minecraft", + "path": "trapped_chest" }, { "id": 3, - "ident": "minecraft:ender_chest", - "name": "ender_chest" + "namespace": "minecraft", + "path": "ender_chest" }, { "id": 4, - "ident": "minecraft:jukebox", - "name": "jukebox" + "namespace": "minecraft", + "path": "jukebox" }, { "id": 5, - "ident": "minecraft:dispenser", - "name": "dispenser" + "namespace": "minecraft", + "path": "dispenser" }, { "id": 6, - "ident": "minecraft:dropper", - "name": "dropper" + "namespace": "minecraft", + "path": "dropper" }, { "id": 7, - "ident": "minecraft:sign", - "name": "sign" + "namespace": "minecraft", + "path": "sign" }, { "id": 8, - "ident": "minecraft:hanging_sign", - "name": "hanging_sign" + "namespace": "minecraft", + "path": "hanging_sign" }, { "id": 9, - "ident": "minecraft:mob_spawner", - "name": "mob_spawner" + "namespace": "minecraft", + "path": "mob_spawner" }, { "id": 10, - "ident": "minecraft:piston", - "name": "piston" + "namespace": "minecraft", + "path": "piston" }, { "id": 11, - "ident": "minecraft:brewing_stand", - "name": "brewing_stand" + "namespace": "minecraft", + "path": "brewing_stand" }, { "id": 12, - "ident": "minecraft:enchanting_table", - "name": "enchanting_table" + "namespace": "minecraft", + "path": "enchanting_table" }, { "id": 13, - "ident": "minecraft:end_portal", - "name": "end_portal" + "namespace": "minecraft", + "path": "end_portal" }, { "id": 14, - "ident": "minecraft:beacon", - "name": "beacon" + "namespace": "minecraft", + "path": "beacon" }, { "id": 15, - "ident": "minecraft:skull", - "name": "skull" + "namespace": "minecraft", + "path": "skull" }, { "id": 16, - "ident": "minecraft:daylight_detector", - "name": "daylight_detector" + "namespace": "minecraft", + "path": "daylight_detector" }, { "id": 17, - "ident": "minecraft:hopper", - "name": "hopper" + "namespace": "minecraft", + "path": "hopper" }, { "id": 18, - "ident": "minecraft:comparator", - "name": "comparator" + "namespace": "minecraft", + "path": "comparator" }, { "id": 19, - "ident": "minecraft:banner", - "name": "banner" + "namespace": "minecraft", + "path": "banner" }, { "id": 20, - "ident": "minecraft:structure_block", - "name": "structure_block" + "namespace": "minecraft", + "path": "structure_block" }, { "id": 21, - "ident": "minecraft:end_gateway", - "name": "end_gateway" + "namespace": "minecraft", + "path": "end_gateway" }, { "id": 22, - "ident": "minecraft:command_block", - "name": "command_block" + "namespace": "minecraft", + "path": "command_block" }, { "id": 23, - "ident": "minecraft:shulker_box", - "name": "shulker_box" + "namespace": "minecraft", + "path": "shulker_box" }, { "id": 24, - "ident": "minecraft:bed", - "name": "bed" + "namespace": "minecraft", + "path": "bed" }, { "id": 25, - "ident": "minecraft:conduit", - "name": "conduit" + "namespace": "minecraft", + "path": "conduit" }, { "id": 26, - "ident": "minecraft:barrel", - "name": "barrel" + "namespace": "minecraft", + "path": "barrel" }, { "id": 27, - "ident": "minecraft:smoker", - "name": "smoker" + "namespace": "minecraft", + "path": "smoker" }, { "id": 28, - "ident": "minecraft:blast_furnace", - "name": "blast_furnace" + "namespace": "minecraft", + "path": "blast_furnace" }, { "id": 29, - "ident": "minecraft:lectern", - "name": "lectern" + "namespace": "minecraft", + "path": "lectern" }, { "id": 30, - "ident": "minecraft:bell", - "name": "bell" + "namespace": "minecraft", + "path": "bell" }, { "id": 31, - "ident": "minecraft:jigsaw", - "name": "jigsaw" + "namespace": "minecraft", + "path": "jigsaw" }, { "id": 32, - "ident": "minecraft:campfire", - "name": "campfire" + "namespace": "minecraft", + "path": "campfire" }, { "id": 33, - "ident": "minecraft:beehive", - "name": "beehive" + "namespace": "minecraft", + "path": "beehive" }, { "id": 34, - "ident": "minecraft:sculk_sensor", - "name": "sculk_sensor" + "namespace": "minecraft", + "path": "sculk_sensor" }, { "id": 35, - "ident": "minecraft:sculk_catalyst", - "name": "sculk_catalyst" + "namespace": "minecraft", + "path": "sculk_catalyst" }, { "id": 36, - "ident": "minecraft:sculk_shrieker", - "name": "sculk_shrieker" + "namespace": "minecraft", + "path": "sculk_shrieker" }, { "id": 37, - "ident": "minecraft:chiseled_bookshelf", - "name": "chiseled_bookshelf" + "namespace": "minecraft", + "path": "chiseled_bookshelf" }, { "id": 38, - "ident": "minecraft:suspicious_sand", - "name": "suspicious_sand" + "namespace": "minecraft", + "path": "suspicious_sand" }, { "id": 39, - "ident": "minecraft:decorated_pot", - "name": "decorated_pot" + "namespace": "minecraft", + "path": "decorated_pot" } ], "shapes": [ diff --git a/extractor/src/main/java/rs/valence/extractor/extractors/Blocks.java b/extractor/src/main/java/rs/valence/extractor/extractors/Blocks.java index b514ea2a3..6727772b1 100644 --- a/extractor/src/main/java/rs/valence/extractor/extractors/Blocks.java +++ b/extractor/src/main/java/rs/valence/extractor/extractors/Blocks.java @@ -102,8 +102,8 @@ public JsonElement extract() { for (var blockEntity : Registries.BLOCK_ENTITY_TYPE) { var blockEntityJson = new JsonObject(); blockEntityJson.addProperty("id", Registries.BLOCK_ENTITY_TYPE.getRawId(blockEntity)); - blockEntityJson.addProperty("ident", Registries.BLOCK_ENTITY_TYPE.getId(blockEntity).toString()); - blockEntityJson.addProperty("name", Registries.BLOCK_ENTITY_TYPE.getId(blockEntity).getPath()); + blockEntityJson.addProperty("namespace", Registries.BLOCK_ENTITY_TYPE.getId(blockEntity).getNamespace()); + blockEntityJson.addProperty("path", Registries.BLOCK_ENTITY_TYPE.getId(blockEntity).getPath()); blockEntitiesJson.add(blockEntityJson); } From 4c37f600a91d5a1f6b3e50d420bedde1e76b046e Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sat, 4 Mar 2023 10:55:15 +0100 Subject: [PATCH 09/22] Add example schematic to assets --- assets/example_schem.schem | Bin 0 -> 452 bytes crates/valence_schem/src/lib.rs | 14 +++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 assets/example_schem.schem diff --git a/assets/example_schem.schem b/assets/example_schem.schem new file mode 100644 index 0000000000000000000000000000000000000000..296cc28c4025cd4317ed6cb293ba78a701658729 GIT binary patch literal 452 zcmV;#0XzO5iwFP!000001ErJQi_|a>z^6^O-Rqv&KjA;D$;#0S^bIu`;sZv$2;-+dG%|yr*hyPvF5$d8t{kLYq2ckp7N0huM4l z#!n%;$=NCSS(abmHeY6WMLijRzV0W?Z($DMs~RG!TD<^qMLvx1D7V7h{;wQ@Fc8r! zhyyQ4urK>L2&s(s7VxQ-W?hn8mu%!%6PZ;bE2Y~4rR8PT$-XJBiJ69FMkDHi&4kNr zqzxLZi$?a=9XFEE^whR@S6>`{CgAfyye7llas2Q;zNP6{etchfHh64#gn&oGw!&Ub z!^&#fZ2#I^tDpF&zWpmh?1qr5Wq8Hfpp?8duz@YOT@Qg4MXpzM^qI zK=x&=3|2edG#&s({s9X=T_4a`=L&H8BZT+@RogK{PJM&Ahin$2Mh~{-!_d!_Idr>69LH|K8O%zA64y{0KWiE!9Z-d0{{RoRNf2# literal 0 HcmV?d00001 diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 5b7c3d350..99c705c12 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -742,10 +742,12 @@ impl Schematic { #[cfg(test)] mod test { + use std::fs; + use super::*; #[test] - fn schematic() { + fn schematic_copy_paste() { let mut app = App::new(); app.add_plugin(ServerPlugin::new(())); let mut instance = app @@ -866,4 +868,14 @@ mod test { } ); } + + #[test] + fn schematic_load_save() { + let schem1 = Schematic::load("../../assets/example_schem.schem").unwrap(); + const TEST_PATH: &str = "test.schem"; + schem1.save(TEST_PATH, Compression::best()).unwrap(); + let schem2 = Schematic::load(TEST_PATH).unwrap(); + assert_eq!(schem1, schem2); + fs::remove_file(TEST_PATH).unwrap(); + } } From ada8da4a64397b544d9ec1a931bfccb0d4a9f278 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sat, 4 Mar 2023 15:37:42 +0100 Subject: [PATCH 10/22] Fixed test --- crates/valence_schem/src/lib.rs | 68 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 99c705c12..230a391c5 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -830,41 +830,43 @@ mod test { assert_eq!( nbt, compound! { - "Version" => 3, - "DataVersion" => 3218, - "Metadata" => compound! { - "A" => 123, - }, - "Width" => 3i16, - "Height" => 3i16, - "Length" => 2i16, - "Offset" => vec![-1, -2, -1], - "Blocks" => compound! { - "Data" => vec![0i8, 1, 0, 0, 0, 0, 2, 3, 3, 0, 4, 0, 0, 5, 0, 0, 0, 0], - "Palette" => compound! { - "air" => 0, - "glowstone" => 1, - "lapis_block" => 2, - "stone" => 3, - "andesite_wall[east=none, north=low, south=none, up=true, waterlogged=false, west=none]" => 4, - "oak_sign[rotation=0, waterlogged=false]" => 5, + "Schematic" => compound! { + "Version" => 3, + "DataVersion" => 3218, + "Metadata" => compound! { + "A" => 123, }, - "BlockEntities" => Value::List(List::Compound(vec![ - compound! { - "Data" => compound!{ - "Text1" => "abc".into_text(), - }, - "Id" => "sign", - "Pos" => vec![1, 2, 0], + "Width" => 3i16, + "Height" => 3i16, + "Length" => 2i16, + "Offset" => vec![-1, -2, -1], + "Blocks" => compound! { + "Data" => vec![0i8, 1, 0, 0, 0, 0, 2, 3, 3, 0, 4, 0, 0, 5, 0, 0, 0, 0], + "Palette" => compound! { + "air" => 0, + "glowstone" => 1, + "lapis_block" => 2, + "stone" => 3, + "andesite_wall[east=none, north=low, south=none, up=true, waterlogged=false, west=none]" => 4, + "oak_sign[rotation=0, waterlogged=false]" => 5, }, - ])) - }, - "Biomes" => compound! { - "Data" => vec![0i8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "Palette" => compound! { - "minecraft:plains" => 0, - } - }, + "BlockEntities" => Value::List(List::Compound(vec![ + compound! { + "Data" => compound!{ + "Text1" => "abc".into_text(), + }, + "Id" => "sign", + "Pos" => vec![1, 2, 0], + }, + ])) + }, + "Biomes" => compound! { + "Data" => vec![0i8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "Palette" => compound! { + "minecraft:plains" => 0, + } + }, + } } ); } From a2e32be4067876e30defdf838a979afd8f79e168 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 5 Mar 2023 08:55:10 +0100 Subject: [PATCH 11/22] fix rebase --- crates/valence/examples/schem_loading.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/valence/examples/schem_loading.rs b/crates/valence/examples/schem_loading.rs index 0666689ab..5b0e2723e 100644 --- a/crates/valence/examples/schem_loading.rs +++ b/crates/valence/examples/schem_loading.rs @@ -22,10 +22,12 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) .add_startup_system(setup) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_set(PlayerList::default_system_set()) - .add_system(init_clients) - .add_system(despawn_disconnected_clients) + .add_systems(( + default_event_handler.in_schedule(EventLoopSchedule), + init_clients, + despawn_disconnected_clients, + )) + .add_systems(PlayerList::default_systems()) .run(); } From 8f474eeb289a3701a548373793d4af6bedaa8826 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 5 Mar 2023 11:35:24 +0100 Subject: [PATCH 12/22] Implement version 1 and 2 --- crates/valence_schem/src/lib.rs | 341 ++++++++++++++++++++++++++------ 1 file changed, 277 insertions(+), 64 deletions(-) diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 230a391c5..97d1b8dc5 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -66,7 +66,17 @@ pub struct BlockEntity { #[derive(Debug, Clone, PartialEq)] pub struct Biomes { palette: Box<[Ident]>, - data: Box<[usize]>, + data: BiomeData, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum BiomeData { + /// One biome per y-column. Used in spec version 2. + /// Indexed by x + z * Width + Columns(Box<[usize]>), + /// One biome per block. Used in spec version 3. + /// Indexed by x + z * Width + y * Width * Length + Blocks(Box<[usize]>), } #[derive(Debug, Clone, PartialEq)] @@ -91,7 +101,7 @@ pub enum LoadSchematicError { #[error("missing version")] MissingVersion, - #[error("unknown version {0} (only version 3 is supported)")] + #[error("unknown version {0} (only versions 1 through 3 are supported)")] UnknownVersion(i32), #[error("missing width")] @@ -121,6 +131,12 @@ pub enum LoadSchematicError { #[error(transparent)] VarIntDecodeError(#[from] VarIntDecodeError), + #[error("block {0} not in palette {1:?}")] + BlockNotInPalette(i32, HashMap), + + #[error("unknown block state id {0}")] + UnknownBlockStateId(i32), + #[error("invalid block count")] InvalidBlockCount, @@ -145,6 +161,9 @@ pub enum LoadSchematicError { #[error("invalid biome palette")] InvalidBiomePalette, + #[error("biome {0} not in palette {1:?}")] + BiomeNotInPalette(i32, HashMap>), + #[error("invalid biome ident '{0}'")] InvalidBiomeIdent(String), @@ -239,8 +258,7 @@ impl Schematic { let Some(&Value::Int(version)) = root.get("Version") else { return Err(LoadSchematicError::MissingVersion); }; - //TODO: Allow version 1 and 2 - if version != 3 { + if !(1..=3).contains(&version) { return Err(LoadSchematicError::UnknownVersion(version)); } let Some(&Value::Short(width)) = root.get("Width") else { @@ -261,32 +279,51 @@ impl Schematic { }; IVec3::new(x, y, z) }; - let blocks: Option> = match root.get("Blocks") { - Some(Value::Compound(blocks)) => { - let Some(Value::Compound(palette)) = blocks.get("Palette") else { - return Err(LoadSchematicError::MissingBlockPalette); + let blocks = match version { + 1 | 2 => { + let palette = match root.get("Palette") { + Some(Value::Compound(palette)) => { + let palette: Result, _> = palette + .into_iter() + .map(|(state, value)| { + let &Value::Int(i) = value else { + return Err(LoadSchematicError::InvalidBlockPalette); + }; + let state = BlockState::from_str( + state.strip_prefix("minecraft:").unwrap_or(state), + )?; + Ok((i, state)) + }) + .collect(); + Some(palette?) + } + _ => None, }; - let palette: Result, _> = palette - .into_iter() - .map(|(state, value)| { - let &Value::Int(i) = value else { - return Err(LoadSchematicError::InvalidBlockPalette); - }; - let state = BlockState::from_str( - state.strip_prefix("minecraft:").unwrap_or(state), - )?; - Ok((i, state)) - }) - .collect(); - let palette = palette?; - let Some(Value::ByteArray(data)) = blocks.get("Data") else { + let Some(Value::ByteArray(data)) = root.get("BlockData") else { return Err(LoadSchematicError::MissingBlockData); }; let data: Result, LoadSchematicError> = VarIntReader(data.iter().map(|byte| *byte as u8)) .map(|val| { - let state = palette[&val?]; + let val = val?; + let state = match &palette { + Some(palette) => match palette.get(&val) { + Some(val) => *val, + None => { + return Err(LoadSchematicError::BlockNotInPalette( + val, + palette.clone(), + )) + } + }, + None => match BlockState::from_raw(val.try_into().unwrap()) { + Some(val) => val, + None => { + return Err(LoadSchematicError::UnknownBlockStateId(val)) + } + }, + }; Ok(Block { state, block_entity: None, @@ -297,9 +334,12 @@ impl Schematic { if u16::try_from(data.len()) != Ok(width * height * length) { return Err(LoadSchematicError::InvalidBlockCount); } - if let Some(Value::List(List::Compound(block_entities))) = - blocks.get("BlockEntities") - { + + if let Some(Value::List(List::Compound(block_entities))) = root.get(match version { + 1 => "TileEntities", + 2 => "BlockEntities", + _ => unreachable!(), + }) { for block_entity in block_entities { let Some(Value::IntArray(pos)) = block_entity.get("Pos") else { return Err(LoadSchematicError::MissingBlockEntityPos); @@ -307,7 +347,6 @@ impl Schematic { let [x, y, z] = pos[..] else { return Err(LoadSchematicError::InvalidBlockEntityPos(pos.clone())); }; - let Some(Value::String(id)) = block_entity.get("Id") else { return Err(LoadSchematicError::MissingBlockEntityId); }; @@ -318,45 +357,136 @@ impl Schematic { return Err(LoadSchematicError::UnknownBlockEntity(id.to_string())); }; - let nbt = match block_entity.get("Data") { - Some(Value::Compound(nbt)) => nbt.clone(), - _ => Compound::with_capacity(0), - }; + let mut nbt = block_entity.clone(); + nbt.remove("Pos"); + nbt.remove("Id"); let block_entity = BlockEntity { kind, data: nbt }; data[(x + z * width as i32 + y * width as i32 * length as i32) as usize] .block_entity .replace(block_entity); } } + Some(data.into_boxed_slice()) } - _ => None, + 3 => match root.get("Blocks") { + Some(Value::Compound(blocks)) => { + let Some(Value::Compound(palette)) = blocks.get("Palette") else { + return Err(LoadSchematicError::MissingBlockPalette); + }; + let palette: Result, _> = palette + .into_iter() + .map(|(state, value)| { + let &Value::Int(i) = value else { + return Err(LoadSchematicError::InvalidBlockPalette); + }; + let state = BlockState::from_str( + state.strip_prefix("minecraft:").unwrap_or(state), + )?; + Ok((i, state)) + }) + .collect(); + let palette = palette?; + + let Some(Value::ByteArray(data)) = blocks.get("Data") else { + return Err(LoadSchematicError::MissingBlockData); + }; + let data: Result, LoadSchematicError> = + VarIntReader(data.iter().map(|byte| *byte as u8)) + .map(|val| { + let val = val?; + let state = match palette.get(&val) { + Some(val) => *val, + None => { + return Err(LoadSchematicError::BlockNotInPalette( + val, + palette.clone(), + )) + } + }; + Ok(Block { + state, + block_entity: None, + }) + }) + .collect(); + let mut data = data?; + if u16::try_from(data.len()) != Ok(width * height * length) { + return Err(LoadSchematicError::InvalidBlockCount); + } + if let Some(Value::List(List::Compound(block_entities))) = + blocks.get("BlockEntities") + { + for block_entity in block_entities { + let Some(Value::IntArray(pos)) = block_entity.get("Pos") else { + return Err(LoadSchematicError::MissingBlockEntityPos); + }; + let [x, y, z] = pos[..] else { + return Err(LoadSchematicError::InvalidBlockEntityPos(pos.clone())); + }; + + let Some(Value::String(id)) = block_entity.get("Id") else { + return Err(LoadSchematicError::MissingBlockEntityId); + }; + let Ok(id) = Ident::new(&id[..]) else { + return Err(LoadSchematicError::InvalidBlockEntityId(id.clone())); + }; + let Some(kind) = BlockEntityKind::from_ident(id) else { + return Err(LoadSchematicError::UnknownBlockEntity(id.to_string())); + }; + + let nbt = match block_entity.get("Data") { + Some(Value::Compound(nbt)) => nbt.clone(), + _ => Compound::with_capacity(0), + }; + let block_entity = BlockEntity { kind, data: nbt }; + data[(x + z * width as i32 + y * width as i32 * length as i32) + as usize] + .block_entity + .replace(block_entity); + } + } + Some(data.into_boxed_slice()) + } + _ => None, + }, + _ => unreachable!(), }; - let biomes = match root.get("Biomes") { - Some(Value::Compound(biomes)) => { - let Some(Value::Compound(palette)) = biomes.get("Palette") else { + let biomes = match version { + 1 => None, + 2 => { + let Some(Value::Compound(palette)) = root.get("BiomePalette") else { return Err(LoadSchematicError::MissingBiomePalette); }; let palette: Result, _> = palette .iter() .map(|(biome, value)| { let &Value::Int(i) = value else { - return Err(LoadSchematicError::InvalidBiomePalette); - }; + return Err(LoadSchematicError::InvalidBiomePalette); + }; let Ok(ident) = Ident::new(biome.clone()) else { - return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); - }; + return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); + }; Ok((i, ident)) }) .collect(); let palette = palette?; - let Some(Value::ByteArray(data)) = biomes.get("Data") else { + + let Some(Value::ByteArray(data)) = root.get("BiomesData") else { return Err(LoadSchematicError::MissingBiomeData); }; let data: Result, LoadSchematicError> = VarIntReader(data.iter().map(|byte| *byte as u8)) - .map(|val| Ok(&palette[&val?])) + .map(|val| { + let val = val?; + match palette.get(&val) { + Some(val) => Ok(val), + None => { + Err(LoadSchematicError::BiomeNotInPalette(val, palette.clone())) + } + } + }) .collect(); let data = data?; @@ -375,17 +505,71 @@ impl Schematic { }) .collect(); - if u16::try_from(data.len()) != Ok(width * height * length) { + if u16::try_from(data.len()) != Ok(width * length) { return Err(LoadSchematicError::InvalidBiomeCount); } let biomes = Biomes { palette: palette.into_boxed_slice(), - data: data.into_boxed_slice(), + data: BiomeData::Columns(data.into_boxed_slice()), }; Some(biomes) } - _ => None, + 3 => match root.get("Biomes") { + Some(Value::Compound(biomes)) => { + let Some(Value::Compound(palette)) = biomes.get("Palette") else { + return Err(LoadSchematicError::MissingBiomePalette); + }; + let palette: Result, _> = palette + .iter() + .map(|(biome, value)| { + let &Value::Int(i) = value else { + return Err(LoadSchematicError::InvalidBiomePalette); + }; + let Ok(ident) = Ident::new(biome.clone()) else { + return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); + }; + Ok((i, ident)) + }) + .collect(); + let palette = palette?; + let Some(Value::ByteArray(data)) = biomes.get("Data") else { + return Err(LoadSchematicError::MissingBiomeData); + }; + let data: Result, LoadSchematicError> = + VarIntReader(data.iter().map(|byte| *byte as u8)) + .map(|val| Ok(&palette[&val?])) + .collect(); + let data = data?; + + let mut palette = vec![]; + let mut map = HashMap::new(); + let data: Vec<_> = data + .into_iter() + .map(|biome| match map.entry(biome) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let idx = palette.len(); + palette.push(biome.clone()); + entry.insert(idx); + idx + } + }) + .collect(); + + if u16::try_from(data.len()) != Ok(width * height * length) { + return Err(LoadSchematicError::InvalidBiomeCount); + } + + let biomes = Biomes { + palette: palette.into_boxed_slice(), + data: BiomeData::Blocks(data.into_boxed_slice()), + }; + Some(biomes) + } + _ => None, + }, + _ => unreachable!(), }; let entities: Option> = match root.get("Entities") { @@ -517,10 +701,23 @@ impl Schematic { .map(|(idx, val)| (val.to_string(), Value::Int(idx as i32))) .collect(); let mut data = vec![]; - for i in biomes.data.iter() { - VarInt(*i as i32) - .encode(VarIntWriteWrapper(&mut data)) - .unwrap(); + match &biomes.data { + BiomeData::Columns(biome_data) => { + for _ in 0..self.height { + for i in biome_data.iter() { + VarInt(*i as i32) + .encode(VarIntWriteWrapper(&mut data)) + .unwrap(); + } + } + } + BiomeData::Blocks(biome_data) => { + for i in biome_data.iter() { + VarInt(*i as i32) + .encode(VarIntWriteWrapper(&mut data)) + .unwrap(); + } + } } compound.insert( "Biomes", @@ -617,20 +814,36 @@ impl Schematic { } if let Some(Biomes { palette, data }) = &self.biomes { - for ([x, y, z], biome) in data - .iter() - .map(|biome| palette[*biome].as_str_ident()) - .map(map_biome) - .enumerate() - .map(|(idx, biome)| { - let idx = u16::try_from(idx).unwrap(); - let y = idx / (self.width * self.length); - let z = (idx % (self.width * self.length)) / self.width; - let x = (idx % (self.width * self.length)) % self.width; - - ([x, y, z], biome) - }) - { + let data: Box> = match data { + BiomeData::Columns(data) => Box::new( + data.iter() + .map(|biome| palette[*biome].as_str_ident()) + .map(map_biome) + .enumerate() + .flat_map(|(idx, biome)| { + let idx = u16::try_from(idx).unwrap(); + let z = idx / self.width; + let x = idx % self.width; + + (0..self.height).map(move |y| ([x, y, z], biome)) + }), + ), + BiomeData::Blocks(data) => Box::new( + data.iter() + .map(|biome| palette[*biome].as_str_ident()) + .map(map_biome) + .enumerate() + .map(|(idx, biome)| { + let idx = u16::try_from(idx).unwrap(); + let y = idx / (self.width * self.length); + let z = (idx % (self.width * self.length)) / self.width; + let x = (idx % (self.width * self.length)) % self.width; + + ([x, y, z], biome) + }), + ), + }; + for ([x, y, z], biome) in data { let x = x as i32 + origin.x + self.offset.x; let y = y as i32 + origin.y + self.offset.y; let z = z as i32 + origin.z + self.offset.z; @@ -678,7 +891,7 @@ impl Schematic { (min.z..=max.z).flat_map(move |z| { (min.x..=max.x).map(move |x| { let Some(block) = instance.block([x, y, z]) else { - todo!("asd"); + panic!("coordinates ({x} {y} {z}) are out of bounds"); }; let state = block.state(); let block_entity = block @@ -724,7 +937,7 @@ impl Schematic { Biomes { palette: palette.into_boxed_slice(), - data: data.into_boxed_slice(), + data: BiomeData::Blocks(data.into_boxed_slice()), } }; Self { From a076977fc9a03fe52864e3129dba4eeef3aa9311 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Mon, 6 Mar 2023 15:54:00 +0100 Subject: [PATCH 13/22] add module documentation --- crates/valence_schem/README.md | 40 +++++++++++++++++++++++++++++++++ crates/valence_schem/src/lib.rs | 16 ++++++------- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/crates/valence_schem/README.md b/crates/valence_schem/README.md index f91e3fab7..301ee06ae 100644 --- a/crates/valence_schem/README.md +++ b/crates/valence_schem/README.md @@ -1,3 +1,43 @@ # valence_schem Support for the [Sponge schematic file format](https://github.com/SpongePowered/Schematic-Specification). + +This crate implements [Sponge schematics] + +Loading schematics (version 1 through 3) from [`Compounds`](Compound) is +supported. Saving schematics to [`Compounds`](Compound) (version 3 only) is +supported. + +# Examples + +An example that shows how to load and save [schematics] from and to the +filesystem + +```rust +# use valence_schem::Schematic; +use flate2::Compression; +fn schem_from_file(path: &str) -> Schematic { + Schematic::load(path).unwrap() +} +fn schem_to_file(schematic: &Schematic, path: &str) { + schematic.save(path); +} +``` + +There are also methods to serialize and deserialize [schematics] from and to +[`Compounds`](Compound): +```rust +# use valence_schem::Schematic; +use valence_nbt::Compound; +fn schem_from_compound(compound: &Compound) { + let schematic = Schematic::deserialize(compound).unwrap(); + let comp = schematic.serialize(); +} +``` + +### See also + +Examples in the `examples/` directory + +[Sponge schematics]: +[schematics]: Schematic \ No newline at end of file diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 97d1b8dc5..9e748538b 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -48,7 +48,7 @@ pub struct Schematic { pub offset: IVec3, blocks: Option>, biomes: Option, - pub entities: Option>, + pub entities: Option>, } #[derive(Debug, Clone, PartialEq)] @@ -431,7 +431,7 @@ impl Schematic { let Ok(id) = Ident::new(&id[..]) else { return Err(LoadSchematicError::InvalidBlockEntityId(id.clone())); }; - let Some(kind) = BlockEntityKind::from_ident(id) else { + let Some(kind) = BlockEntityKind::from_ident(id.as_str_ident()) else { return Err(LoadSchematicError::UnknownBlockEntity(id.to_string())); }; @@ -465,10 +465,10 @@ impl Schematic { let &Value::Int(i) = value else { return Err(LoadSchematicError::InvalidBiomePalette); }; - let Ok(ident) = Ident::new(biome.clone()) else { + let Ok(ident) = Ident::new(biome) else { return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); }; - Ok((i, ident)) + Ok((i, ident.to_string_ident())) }) .collect(); let palette = palette?; @@ -550,7 +550,7 @@ impl Schematic { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { let idx = palette.len(); - palette.push(biome.clone()); + palette.push(biome.to_string_ident()); entry.insert(idx); idx } @@ -572,7 +572,7 @@ impl Schematic { _ => unreachable!(), }; - let entities: Option> = match root.get("Entities") { + let entities = match root.get("Entities") { Some(Value::List(List::Compound(entities))) => { let entities: Result, _> = entities .iter() @@ -604,7 +604,7 @@ impl Schematic { }) }) .collect(); - Some(entities?.into_boxed_slice()) + Some(entities?) } _ => None, }; @@ -1088,7 +1088,7 @@ mod test { fn schematic_load_save() { let schem1 = Schematic::load("../../assets/example_schem.schem").unwrap(); const TEST_PATH: &str = "test.schem"; - schem1.save(TEST_PATH, Compression::best()).unwrap(); + schem1.save(TEST_PATH).unwrap(); let schem2 = Schematic::load(TEST_PATH).unwrap(); assert_eq!(schem1, schem2); fs::remove_file(TEST_PATH).unwrap(); From 8c0054c4b2345ec3b6222f413bd4efa9a9dd308a Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 23 Apr 2023 01:24:03 +0200 Subject: [PATCH 14/22] Fix unit tests --- crates/valence_schem/Cargo.toml | 3 +++ crates/valence_schem/src/lib.rs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/valence_schem/Cargo.toml b/crates/valence_schem/Cargo.toml index 2d43c7098..3c7df536b 100644 --- a/crates/valence_schem/Cargo.toml +++ b/crates/valence_schem/Cargo.toml @@ -18,3 +18,6 @@ valence_block.workspace = true valence_core.workspace = true valence_instance.workspace = true valence_nbt.workspace = true + +[dev-dependencies] +valence.workspace = true diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 9e748538b..b8859bba3 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -957,16 +957,16 @@ impl Schematic { mod test { use std::fs; + use valence::prelude::*; + use valence_core::ident; + use super::*; #[test] fn schematic_copy_paste() { let mut app = App::new(); - app.add_plugin(ServerPlugin::new(())); - let mut instance = app - .world - .resource::() - .new_instance(DimensionId::default()); + app.add_plugins(DefaultPlugins); + let mut instance = Instance::new_unit_testing(ident!("overworld"), app.world.resource()); for x in -1..=0 { for z in -1..=0 { @@ -981,7 +981,7 @@ mod test { instance.set_block([6, 2, -1], BlockState::STONE); instance.set_block( [5, 3, -1], - Block::with_nbt( + ValenceBlock::with_nbt( BlockState::OAK_SIGN, compound! {"Text1" => "abc".into_text()}, ), @@ -997,7 +997,7 @@ mod test { &instance, (BlockPos::new(4, 3, -1), BlockPos::new(6, 1, 0)), BlockPos::new(5, 3, 0), - |_| ident!("minecraft:plains"), + |_| ident!("minecraft:plains").to_string_ident(), ); schematic.paste(&mut instance, BlockPos::new(15, 18, 16), |_| { @@ -1068,7 +1068,7 @@ mod test { "Data" => compound!{ "Text1" => "abc".into_text(), }, - "Id" => "sign", + "Id" => "minecraft:sign", "Pos" => vec![1, 2, 0], }, ])) From 953a851cc1688afbb2b83e896f5e576d4fa601fc Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 23 Apr 2023 01:34:16 +0200 Subject: [PATCH 15/22] Fix formatting --- crates/valence_block/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valence_block/src/lib.rs b/crates/valence_block/src/lib.rs index b1544c609..4a83c5668 100644 --- a/crates/valence_block/src/lib.rs +++ b/crates/valence_block/src/lib.rs @@ -24,8 +24,8 @@ use std::io::Write; use std::iter::FusedIterator; use std::str::FromStr; -use thiserror::Error; use anyhow::Context; +use thiserror::Error; use valence_core::ident; use valence_core::ident::Ident; use valence_core::item::ItemKind; From 58001c972a4d311e7137b13473609823e738d2d9 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 23 Apr 2023 11:26:28 +0200 Subject: [PATCH 16/22] Fix schem_loading example --- crates/valence/examples/schem_loading.rs | 76 ++++++++++-------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/crates/valence/examples/schem_loading.rs b/crates/valence/examples/schem_loading.rs index 5b0e2723e..bbf09f33e 100644 --- a/crates/valence/examples/schem_loading.rs +++ b/crates/valence/examples/schem_loading.rs @@ -1,9 +1,6 @@ use std::path::PathBuf; use clap::Parser; -use valence::bevy_app::AppExit; -use valence::client::despawn_disconnected_clients; -use valence::client::event::default_event_handler; use valence::prelude::*; use valence_schem::Schematic; @@ -16,68 +13,61 @@ struct Cli { path: PathBuf, } +#[derive(Resource)] +struct SchemRes(Schematic); + pub fn main() { tracing_subscriber::fmt().init(); - App::new() - .add_plugin(ServerPlugin::new(())) - .add_startup_system(setup) - .add_systems(( - default_event_handler.in_schedule(EventLoopSchedule), - init_clients, - despawn_disconnected_clients, - )) - .add_systems(PlayerList::default_systems()) - .run(); -} - -fn setup(mut commands: Commands, server: Res, mut exit: EventWriter) { let Cli { path } = Cli::parse(); - if !path.exists() { eprintln!("File `{}` does not exist. Exiting.", path.display()); - exit.send_default(); + return; } else if !path.is_file() { eprintln!("`{}` is not a file. Exiting.", path.display()); - exit.send_default(); + return; } - - let mut instance = server.new_instance(DimensionId::default()); - - match Schematic::load(path) { - Ok(schem) => { - schem.paste(&mut instance, SPAWN_POS, |_| BiomeId::default()); - } + let schem = match Schematic::load(path) { + Ok(schem) => schem, Err(err) => { eprintln!("Error loading schematic: {err}"); - exit.send_default(); + return; } - } + }; + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(SchemRes(schem)) + .add_startup_system(setup) + .add_systems((init_clients, despawn_disconnected_clients)) + .run(); +} + +fn setup( + mut commands: Commands, + dimensions: Query<&DimensionType>, + biomes: Query<&Biome>, + server: Res, + schem: Res, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + schem + .0 + .paste(&mut instance, SPAWN_POS, |_| BiomeId::default()); commands.spawn(instance); } fn init_clients( - mut clients: Query<&mut Client, Added>, + mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - mut commands: Commands, ) { - for mut client in &mut clients { - let instance = instances.single(); - - client.set_flat(true); - client.set_game_mode(GameMode::Creative); - client.set_position([ + for (mut loc, mut pos, mut game_mode) in &mut clients { + *game_mode = GameMode::Creative; + pos.set([ SPAWN_POS.x as f64 + 0.5, SPAWN_POS.y as f64, SPAWN_POS.z as f64 + 0.5, ]); - client.set_instance(instance); - - commands.spawn(McEntity::with_uuid( - EntityKind::Player, - instance, - client.uuid(), - )); + loc.0 = instances.single(); } } From ae387ff573b6cbdf1ee9d0707cfdeebf6efbdc79 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 23 Apr 2023 11:32:18 +0200 Subject: [PATCH 17/22] Allow type complexity in schem_saving example because of bevy queries --- crates/valence/examples/schem_saving.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/valence/examples/schem_saving.rs b/crates/valence/examples/schem_saving.rs index 41db41c65..54f201114 100644 --- a/crates/valence/examples/schem_saving.rs +++ b/crates/valence/examples/schem_saving.rs @@ -1,3 +1,5 @@ +#![allow(clippy::type_complexity)] + use std::path::PathBuf; use valence::prelude::*; From 13e0594d3c62a51d8e2d97e8d962f381cf68144f Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sun, 11 Jun 2023 15:58:30 +0200 Subject: [PATCH 18/22] Fix after merge --- crates/valence/examples/schem_saving.rs | 97 +++++++++++-------------- crates/valence_schem/src/lib.rs | 12 +-- 2 files changed, 47 insertions(+), 62 deletions(-) diff --git a/crates/valence/examples/schem_saving.rs b/crates/valence/examples/schem_saving.rs index 54f201114..f8b8ce1f9 100644 --- a/crates/valence/examples/schem_saving.rs +++ b/crates/valence/examples/schem_saving.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use valence::prelude::*; -use valence_client::misc::InteractBlock; -use valence_inventory::ClientInventoryState; +use valence_client::interact_block::InteractBlockEvent; +use valence_inventory::HeldItem; use valence_nbt::compound; use valence_schem::Schematic; @@ -42,25 +42,20 @@ struct Origin(BlockPos); struct Clipboard(Schematic); fn first_pos( - mut clients: Query<( - &mut Client, - &Inventory, - Option<&mut FirstPos>, - &ClientInventoryState, - )>, - mut block_breaks: EventReader, + mut clients: Query<(&mut Client, &Inventory, Option<&mut FirstPos>, &HeldItem)>, + mut block_breaks: EventReader, mut commands: Commands, ) { - for Digging { + for DiggingEvent { client: entity, position, .. } in block_breaks.iter() { - let Ok((mut client, inv, pos, inv_state)) = clients.get_mut(*entity) else { + let Ok((mut client, inv, pos, held_item)) = clients.get_mut(*entity) else { continue; }; - let slot = inv.slot(inv_state.held_item_slot()); + let slot = inv.slot(held_item.slot()); if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); if changed { @@ -75,16 +70,11 @@ fn first_pos( } fn second_pos( - mut clients: Query<( - &mut Client, - &Inventory, - Option<&mut SecondPos>, - &ClientInventoryState, - )>, - mut interacts: EventReader, + mut clients: Query<(&mut Client, &Inventory, Option<&mut SecondPos>, &HeldItem)>, + mut interacts: EventReader, mut commands: Commands, ) { - for InteractBlock { + for InteractBlockEvent { client: entity, hand, position, @@ -92,10 +82,10 @@ fn second_pos( } in interacts.iter() { if *hand == Hand::Main { - let Ok((mut client, inv, pos, inv_state)) = clients.get_mut(*entity) else { + let Ok((mut client, inv, pos, held_item)) = clients.get_mut(*entity) else { continue; }; - let slot = inv.slot(inv_state.held_item_slot()); + let slot = inv.slot(held_item.slot()); if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); if changed { @@ -111,16 +101,11 @@ fn second_pos( } fn origin( - mut clients: Query<( - &mut Client, - &Inventory, - Option<&mut Origin>, - &ClientInventoryState, - )>, - mut interacts: EventReader, + mut clients: Query<(&mut Client, &Inventory, Option<&mut Origin>, &HeldItem)>, + mut interacts: EventReader, mut commands: Commands, ) { - for InteractBlock { + for InteractBlockEvent { client: entity, hand, position, @@ -128,10 +113,10 @@ fn origin( } in interacts.iter() { if *hand == Hand::Main { - let Ok((mut client, inv, pos, inv_state)) = clients.get_mut(*entity) else { + let Ok((mut client, inv, pos, held_item)) = clients.get_mut(*entity) else { continue; }; - let slot = inv.slot(inv_state.held_item_slot()); + let slot = inv.slot(held_item.slot()); if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Stick) { let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); if changed { @@ -154,18 +139,18 @@ fn copy_schem( Option<&FirstPos>, Option<&SecondPos>, Option<&Origin>, - &ClientInventoryState, + &HeldItem, &Position, &Location, &Username, )>, instances: Query<&Instance>, - mut interacts: EventReader, + mut interacts: EventReader, biome_registry: Res, biomes: Query<&Biome>, mut commands: Commands, ) { - for InteractBlock { + for InteractBlockEvent { client: entity, hand, .. @@ -174,10 +159,10 @@ fn copy_schem( if *hand != Hand::Main { continue; } - let Ok((mut client, inv, pos1, pos2, origin, inv_state, &Position(pos), &Location(instance), Username(username))) = clients.get_mut(*entity) else { + let Ok((mut client, inv, pos1, pos2, origin, held_item, &Position(pos), &Location(instance), Username(username))) = clients.get_mut(*entity) else { continue; }; - let slot = inv.slot(inv_state.held_item_slot()); + let slot = inv.slot(held_item.slot()); if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Paper) { let Some((FirstPos(pos1), SecondPos(pos2))) = pos1.zip(pos2) else { client.send_message("Specify both positions first"); @@ -206,12 +191,12 @@ fn paste_schem( &Inventory, Option<&Clipboard>, &Location, - &ClientInventoryState, + &HeldItem, &Position, )>, - mut interacts: EventReader, + mut interacts: EventReader, ) { - for InteractBlock { + for InteractBlockEvent { client: entity, hand, .. @@ -220,13 +205,13 @@ fn paste_schem( if *hand != Hand::Main { continue; } - let Ok((mut client, inv, clipboard, &Location(instance), inv_state, &Position(position))) = clients.get_mut(*entity) else { + let Ok((mut client, inv, clipboard, &Location(instance), held_item, &Position(position))) = clients.get_mut(*entity) else { continue; }; let Ok(mut instance) = instances.get_mut(instance) else { continue; }; - let slot = inv.slot(inv_state.held_item_slot()); + let slot = inv.slot(held_item.slot()); if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Feather) { let Some(Clipboard(schematic)) = clipboard else { client.send_message("Copy something to clipboard first!"); @@ -247,12 +232,12 @@ fn save_schem( &mut Client, &Inventory, Option<&Clipboard>, - &ClientInventoryState, + &HeldItem, &Username, )>, - mut interacts: EventReader, + mut interacts: EventReader, ) { - for InteractBlock { + for InteractBlockEvent { client: entity, hand, .. @@ -261,10 +246,10 @@ fn save_schem( if *hand != Hand::Main { continue; } - let Ok((mut client, inv, clipboard, inv_state, Username(username))) = clients.get_mut(*entity) else { + let Ok((mut client, inv, clipboard, held_item, Username(username))) = clients.get_mut(*entity) else { continue; }; - let slot = inv.slot(inv_state.held_item_slot()); + let slot = inv.slot(held_item.slot()); if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::MusicDiscStal) { let Some(Clipboard(schematic)) = clipboard else { client.send_message("Copy something to clipboard first!"); @@ -278,21 +263,21 @@ fn save_schem( } fn place_blocks( - clients: Query<(&Inventory, &ClientInventoryState), With>, + clients: Query<(&Inventory, &HeldItem), With>, mut instances: Query<&mut Instance>, - mut events: EventReader, + mut events: EventReader, ) { let mut instance = instances.single_mut(); for event in events.iter() { - let Ok((inventory, inv_state)) = clients.get(event.client) else { + let Ok((inventory, held_item)) = clients.get(event.client) else { continue; }; if event.hand != Hand::Main { continue; } - let Some(stack) = inventory.slot(inv_state.held_item_slot()) else { + let Some(stack) = inventory.slot(held_item.slot()) else { continue; }; @@ -307,20 +292,20 @@ fn place_blocks( fn break_blocks( mut instances: Query<&mut Instance>, - inventories: Query<(&Inventory, &ClientInventoryState)>, - mut events: EventReader, + inventories: Query<(&Inventory, &HeldItem)>, + mut events: EventReader, ) { let mut instance = instances.single_mut(); - for Digging { + for DiggingEvent { client, position, .. } in events.iter() { - let Ok((inv, inv_state)) = inventories.get(*client) else { + let Ok((inv, held_item)) = inventories.get(*client) else { continue; }; - let slot = inv.slot(inv_state.held_item_slot()); + let slot = inv.slot(held_item.slot()); if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { instance.set_block(*position, BlockState::AIR); } diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index b8859bba3..308661683 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -34,8 +34,8 @@ use valence_block::{BlockEntityKind, BlockState, ParseBlockStateError}; use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; -use valence_core::packet::var_int::{VarInt, VarIntDecodeError}; -use valence_core::packet::Encode; +use valence_core::protocol::var_int::{VarInt, VarIntDecodeError}; +use valence_core::protocol::Encode; use valence_instance::{Block as ValenceBlock, Instance}; use valence_nbt::{compound, Compound, List, Value}; @@ -93,7 +93,7 @@ pub enum LoadSchematicError { Io(#[from] io::Error), #[error(transparent)] - Nbt(#[from] valence_nbt::Error), + Nbt(#[from] valence_nbt::binary::Error), #[error("missing schematic")] MissingSchematic, @@ -230,7 +230,7 @@ pub enum SaveSchematicError { Io(#[from] io::Error), #[error(transparent)] - Nbt(#[from] valence_nbt::Error), + Nbt(#[from] valence_nbt::binary::Error), } impl Schematic { @@ -241,7 +241,7 @@ impl Schematic { let mut z = GzDecoder::new(BufReader::new(file)); z.read_to_end(&mut buf)?; - let root = valence_nbt::from_binary_slice(&mut buf.as_slice())?.0; + let root = Compound::from_binary(&mut buf.as_slice())?.0; Self::deserialize(&root) } @@ -760,7 +760,7 @@ impl Schematic { let nbt = self.serialize(); let file = File::create(path)?; let mut z = GzEncoder::new(file, Compression::best()); - valence_nbt::to_binary_writer(&mut z, &nbt, "")?; + nbt.to_binary(&mut z, "")?; z.flush()?; Ok(()) } From d4eb42dafd29a3099f8db54287b2d959c3540fcc Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Sat, 29 Jul 2023 23:56:08 +0200 Subject: [PATCH 19/22] Fix unit test --- crates/valence_schem/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 2ac171dfa..9c0c5dee5 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -972,6 +972,7 @@ mod test { fn schematic_copy_paste() { let mut app = App::new(); app.add_plugins(DefaultPlugins); + app.update(); let mut instance = Instance::new( ident!("overworld"), app.world.resource(), From e4c20b91debcbb9be3cb31bade45704111dfc512 Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Mon, 14 Aug 2023 21:49:06 +0200 Subject: [PATCH 20/22] Use chunk layers instead of instances --- crates/valence_schem/Cargo.toml | 2 +- crates/valence_schem/src/lib.rs | 69 ++++----- examples/schem_loading.rs | 36 +++-- examples/schem_saving.rs | 239 ++++++++++++++++++-------------- 4 files changed, 204 insertions(+), 142 deletions(-) diff --git a/crates/valence_schem/Cargo.toml b/crates/valence_schem/Cargo.toml index 3c7df536b..b4df7dd18 100644 --- a/crates/valence_schem/Cargo.toml +++ b/crates/valence_schem/Cargo.toml @@ -16,7 +16,7 @@ thiserror.workspace = true valence_biome.workspace = true valence_block.workspace = true valence_core.workspace = true -valence_instance.workspace = true +valence_layer.workspace = true valence_nbt.workspace = true [dev-dependencies] diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index 9c0c5dee5..a93a98777 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -36,8 +36,8 @@ use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_core::protocol::var_int::{VarInt, VarIntDecodeError}; use valence_core::protocol::Encode; -use valence_instance::chunk::Chunk; -use valence_instance::{Block as ValenceBlock, Instance}; +use valence_layer::chunk::{Block as ValenceBlock, Chunk}; +use valence_layer::ChunkLayer; use valence_nbt::{compound, Compound, List, Value}; #[derive(Debug, Clone, PartialEq)] @@ -253,7 +253,10 @@ impl Schematic { let metadata = root .get("Metadata") - .and_then(|val| val.as_compound()) + .and_then(|val| match val { + Value::Compound(val) => Some(val), + _ => None, + }) .cloned(); let Some(&Value::Int(version)) = root.get("Version") else { @@ -277,7 +280,10 @@ impl Schematic { let offset = { let &[x, y, z] = root .get("Offset") - .and_then(|val| val.as_int_array()) + .and_then(|val| match val { + Value::IntArray(val) => Some(val), + _ => None, + }) .map(|arr| arr.as_slice()) .unwrap_or(&[0; 3]) else { @@ -771,11 +777,11 @@ impl Schematic { Ok(()) } - pub fn paste(&self, instance: &mut Instance, origin: BlockPos, map_biome: F) + pub fn paste(&self, layer: &mut ChunkLayer, origin: BlockPos, map_biome: F) where F: FnMut(Ident<&str>) -> BiomeId, { - let min_y = instance.min_y(); + let min_y = layer.min_y(); if let Some(blocks) = &self.blocks { let blocks = blocks.iter().enumerate().map(|(idx, block)| { let idx = u16::try_from(idx).unwrap(); @@ -799,7 +805,7 @@ impl Schematic { y as i32 + origin.y + self.offset.y, z as i32 + origin.z + self.offset.z, ); - let chunk = instance + let chunk = layer .chunk_entry(ChunkPos::from_block_pos(block_pos)) .or_default(); let block = ValenceBlock::new( @@ -851,8 +857,8 @@ impl Schematic { let x = x as i32 + origin.x + self.offset.x; let y = y as i32 + origin.y + self.offset.y; let z = z as i32 + origin.z + self.offset.z; - let chunk = instance - .chunk_entry(ChunkPos::at(x as f64, z as f64)) + let chunk = layer + .chunk_entry(ChunkPos::from_block_pos(BlockPos::new(x, y, z))) .or_default(); chunk.set_biome( @@ -868,7 +874,7 @@ impl Schematic { } pub fn copy( - instance: &Instance, + layer: &ChunkLayer, corners: (BlockPos, BlockPos), origin: BlockPos, mut map_biome: F, @@ -894,7 +900,7 @@ impl Schematic { .flat_map(|y| { (min.z..=max.z).flat_map(move |z| { (min.x..=max.x).map(move |x| { - let Some(block) = instance.block([x, y, z]) else { + let Some(block) = layer.block([x, y, z]) else { panic!("coordinates ({x} {y} {z}) are out of bounds"); }; let state = block.state; @@ -919,12 +925,12 @@ impl Schematic { .flat_map(|x| { (min.z..=max.z).flat_map(move |z| { (min.y..=max.y).map(move |y| { - instance + layer .chunk(ChunkPos::from_block_pos(BlockPos::new(x, y, z))) .unwrap() .biome( x.rem_euclid(16) as u32 / 4, - (y - instance.min_y()) as u32 / 4, + (y - layer.min_y()) as u32 / 4, z.rem_euclid(16) as u32 / 4, ) }) @@ -965,6 +971,7 @@ mod test { use valence::prelude::*; use valence_core::ident; + use valence_layer::LayerBundle; use super::*; @@ -973,7 +980,7 @@ mod test { let mut app = App::new(); app.add_plugins(DefaultPlugins); app.update(); - let mut instance = Instance::new( + let mut layer = LayerBundle::new( ident!("overworld"), app.world.resource(), app.world.resource(), @@ -982,23 +989,23 @@ mod test { for x in -1..=0 { for z in -1..=0 { - instance.insert_chunk([x, z], UnloadedChunk::default()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::default()); } } - instance.set_block([5, 1, -1], BlockState::GLOWSTONE); - instance.set_block([5, 2, -1], BlockState::STONE); - instance.set_block([5, 2, -2], BlockState::GLOWSTONE); - instance.set_block([4, 2, -1], BlockState::LAPIS_BLOCK); - instance.set_block([6, 2, -1], BlockState::STONE); - instance.set_block( + layer.chunk.set_block([5, 1, -1], BlockState::GLOWSTONE); + layer.chunk.set_block([5, 2, -1], BlockState::STONE); + layer.chunk.set_block([5, 2, -2], BlockState::GLOWSTONE); + layer.chunk.set_block([4, 2, -1], BlockState::LAPIS_BLOCK); + layer.chunk.set_block([6, 2, -1], BlockState::STONE); + layer.chunk.set_block( [5, 3, -1], ValenceBlock::new( BlockState::OAK_SIGN, Some(compound! {"Text1" => "abc".into_text()}), ), ); - instance.set_block( + layer.chunk.set_block( [5, 2, 0], BlockState::ANDESITE_WALL .set(PropName::Up, PropValue::True) @@ -1006,21 +1013,21 @@ mod test { ); let schematic = Schematic::copy( - &instance, + &layer.chunk, (BlockPos::new(4, 3, -1), BlockPos::new(6, 1, 0)), BlockPos::new(5, 3, 0), |_| ident!("minecraft:plains").to_string_ident(), ); - schematic.paste(&mut instance, BlockPos::new(15, 18, 16), |_| { + schematic.paste(&mut layer.chunk, BlockPos::new(15, 18, 16), |_| { BiomeId::default() }); - let block = instance.block([15, 18, 15]).unwrap(); + let block = layer.chunk.block([15, 18, 15]).unwrap(); assert_eq!(block.state, BlockState::OAK_SIGN); assert_eq!(block.nbt, Some(&compound! {"Text1" => "abc".into_text()})); - let block = instance.block([15, 17, 16]).unwrap(); + let block = layer.chunk.block([15, 17, 16]).unwrap(); assert_eq!( block.state, BlockState::ANDESITE_WALL @@ -1029,23 +1036,23 @@ mod test { ); assert_eq!(block.nbt, None); - let block = instance.block([15, 17, 15]).unwrap(); + let block = layer.chunk.block([15, 17, 15]).unwrap(); assert_eq!(block.state, BlockState::STONE); assert_eq!(block.nbt, None); - let block = instance.block([15, 17, 14]).unwrap(); + let block = layer.chunk.block([15, 17, 14]).unwrap(); assert_eq!(block.state, BlockState::AIR); assert_eq!(block.nbt, None); - let block = instance.block([14, 17, 15]).unwrap(); + let block = layer.chunk.block([14, 17, 15]).unwrap(); assert_eq!(block.state, BlockState::LAPIS_BLOCK); assert_eq!(block.nbt, None); - let block = instance.block([16, 17, 15]).unwrap(); + let block = layer.chunk.block([16, 17, 15]).unwrap(); assert_eq!(block.state, BlockState::STONE); assert_eq!(block.nbt, None); - let block = instance.block([15, 16, 15]).unwrap(); + let block = layer.chunk.block([15, 16, 15]).unwrap(); assert_eq!(block.state, BlockState::GLOWSTONE); assert_eq!(block.nbt, None); diff --git a/examples/schem_loading.rs b/examples/schem_loading.rs index cf4b2458b..81b3ca253 100644 --- a/examples/schem_loading.rs +++ b/examples/schem_loading.rs @@ -48,24 +48,44 @@ fn setup( server: Res, schem: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); schem .0 - .paste(&mut instance, SPAWN_POS, |_| BiomeId::default()); - commands.spawn(instance); + .paste(&mut layer.chunk, SPAWN_POS, |_| BiomeId::default()); + commands.spawn(layer); } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut game_mode) in &mut clients { - *game_mode = GameMode::Creative; + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([ SPAWN_POS.x as f64 + 0.5, SPAWN_POS.y as f64, SPAWN_POS.z as f64 + 0.5, ]); - loc.0 = instances.single(); + *game_mode = GameMode::Creative; } } diff --git a/examples/schem_saving.rs b/examples/schem_saving.rs index 331c41e1a..0e9f16f46 100644 --- a/examples/schem_saving.rs +++ b/examples/schem_saving.rs @@ -58,16 +58,17 @@ fn first_pos( continue; }; let slot = inv.slot(held_item.slot()); - if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { - let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); - if changed { - client.send_chat_message(format!( - "Set the primary pos to ({}, {}, {})", - position.x, position.y, position.z, - )); - } - commands.entity(*entity).insert(FirstPos(*position)); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { + continue; } + let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); + if changed { + client.send_chat_message(format!( + "Set the primary pos to ({}, {}, {})", + position.x, position.y, position.z, + )); + } + commands.entity(*entity).insert(FirstPos(*position)); } } @@ -83,22 +84,25 @@ fn second_pos( .. } in interacts.iter() { - if *hand == Hand::Main { - let Ok((mut client, inv, pos, held_item)) = clients.get_mut(*entity) else { - continue; - }; - let slot = inv.slot(held_item.slot()); - if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { - let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); - if changed { - client.send_chat_message(format!( - "Set the secondary pos to ({}, {}, {})", - position.x, position.y, position.z, - )); - } - commands.entity(*entity).insert(SecondPos(*position)); - } + if *hand != Hand::Main { + continue; + } + let Ok((mut client, inv, pos, held_item)) = clients.get_mut(*entity) else { + continue; + }; + let slot = inv.slot(held_item.slot()); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { + continue; } + println!("So this is secondary pos"); + let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); + if changed { + client.send_chat_message(format!( + "Set the secondary pos to ({}, {}, {})", + position.x, position.y, position.z, + )); + } + commands.entity(*entity).insert(SecondPos(*position)); } } @@ -114,22 +118,24 @@ fn origin( .. } in interacts.iter() { - if *hand == Hand::Main { - let Ok((mut client, inv, pos, held_item)) = clients.get_mut(*entity) else { - continue; - }; - let slot = inv.slot(held_item.slot()); - if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Stick) { - let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); - if changed { - client.send_chat_message(format!( - "Set the origin to ({}, {}, {})", - position.x, position.y, position.z, - )); - } - commands.entity(*entity).insert(Origin(*position)); - } + if *hand != Hand::Main { + continue; } + let Ok((mut client, inv, pos, held_item)) = clients.get_mut(*entity) else { + continue; + }; + let slot = inv.slot(held_item.slot()); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Stick) { + continue; + } + let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); + if changed { + client.send_chat_message(format!( + "Set the origin to ({}, {}, {})", + position.x, position.y, position.z, + )); + } + commands.entity(*entity).insert(Origin(*position)); } } @@ -143,10 +149,10 @@ fn copy_schem( Option<&Origin>, &HeldItem, &Position, - &Location, + &VisibleChunkLayer, &Username, )>, - instances: Query<&Instance>, + layers: Query<&ChunkLayer>, mut interacts: EventReader, biome_registry: Res, mut commands: Commands, @@ -168,45 +174,46 @@ fn copy_schem( origin, held_item, &Position(pos), - &Location(instance), + &VisibleChunkLayer(layer), Username(username), )) = clients.get_mut(*entity) else { continue; }; let slot = inv.slot(held_item.slot()); - if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Paper) { - let Some((FirstPos(pos1), SecondPos(pos2))) = pos1.zip(pos2) else { - client.send_chat_message("Specify both positions first"); - continue; - }; - let origin = origin.map(|pos| pos.0).unwrap_or(BlockPos::at(pos)); - - let Ok(instance) = instances.get(instance) else { - continue; - }; - let mut schematic = Schematic::copy(instance, (*pos1, *pos2), origin, |id| { - biome_registry - .iter() - .find(|biome| biome.0 == id) - .unwrap() - .1 - .to_string_ident() - }); - schematic.metadata.replace(compound! {"Author" => username}); - commands.entity(*entity).insert(Clipboard(schematic)); - client.send_chat_message("Copied"); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Paper) { + continue; } + let Some((FirstPos(pos1), SecondPos(pos2))) = pos1.zip(pos2) else { + client.send_chat_message("Specify both positions first"); + continue; + }; + let origin = origin.map(|pos| pos.0).unwrap_or(BlockPos::from_pos(pos)); + + let Ok(layer) = layers.get(layer) else { + continue; + }; + let mut schematic = Schematic::copy(layer, (*pos1, *pos2), origin, |id| { + biome_registry + .iter() + .find(|biome| biome.0 == id) + .unwrap() + .1 + .to_string_ident() + }); + schematic.metadata.replace(compound! {"Author" => username}); + commands.entity(*entity).insert(Clipboard(schematic)); + client.send_chat_message("Copied"); } } fn paste_schem( - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, mut clients: Query<( &mut Client, &Inventory, Option<&Clipboard>, - &Location, + &VisibleChunkLayer, &HeldItem, &Position, )>, @@ -221,27 +228,34 @@ fn paste_schem( if *hand != Hand::Main { continue; } - let Ok((mut client, inv, clipboard, &Location(instance), held_item, &Position(position))) = - clients.get_mut(*entity) + let Ok(( + mut client, + inv, + clipboard, + &VisibleChunkLayer(layer), + held_item, + &Position(position), + )) = clients.get_mut(*entity) else { continue; }; - let Ok(mut instance) = instances.get_mut(instance) else { + let Ok(mut instance) = layers.get_mut(layer) else { continue; }; let slot = inv.slot(held_item.slot()); - if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Feather) { - let Some(Clipboard(schematic)) = clipboard else { - client.send_chat_message("Copy something to clipboard first!"); - continue; - }; - let pos = BlockPos::at(position); - schematic.paste(&mut instance, pos, |_| BiomeId::default()); - client.send_chat_message(format!( - "Pasted schematic at ({} {} {})", - pos.x, pos.y, pos.z - )); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::Feather) { + continue; } + let Some(Clipboard(schematic)) = clipboard else { + client.send_chat_message("Copy something to clipboard first!"); + continue; + }; + let pos = BlockPos::from_pos(position); + schematic.paste(&mut instance, pos, |_| BiomeId::default()); + client.send_chat_message(format!( + "Pasted schematic at ({} {} {})", + pos.x, pos.y, pos.z + )); } } @@ -270,24 +284,25 @@ fn save_schem( continue; }; let slot = inv.slot(held_item.slot()); - if matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::MusicDiscStal) { - let Some(Clipboard(schematic)) = clipboard else { - client.send_chat_message("Copy something to clipboard first!"); - continue; - }; - let path = PathBuf::from(format!("{username}.schem")); - schematic.save(&path).unwrap(); - client.send_chat_message(format!("Saved schem to {}", path.display())); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::MusicDiscStal) { + continue; } + let Some(Clipboard(schematic)) = clipboard else { + client.send_chat_message("Copy something to clipboard first!"); + continue; + }; + let path = PathBuf::from(format!("{username}.schem")); + schematic.save(&path).unwrap(); + client.send_chat_message(format!("Saved schem to {}", path.display())); } } fn place_blocks( clients: Query<(&Inventory, &HeldItem), With>, - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, mut events: EventReader, ) { - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); for event in events.iter() { let Ok((inventory, held_item)) = clients.get(event.client) else { @@ -306,16 +321,16 @@ fn place_blocks( }; let pos = event.position.get_in_direction(event.face); - instance.set_block(pos, block_kind.to_state()); + layer.set_block(pos, block_kind.to_state()); } } fn break_blocks( - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, inventories: Query<(&Inventory, &HeldItem)>, mut events: EventReader, ) { - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); for DiggingEvent { client, position, .. @@ -327,7 +342,7 @@ fn break_blocks( let slot = inv.slot(held_item.slot()); if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { - instance.set_block(*position, BlockState::AIR); + layer.set_block(*position, BlockState::AIR); } } } @@ -338,32 +353,52 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for x in -16..=16 { for z in -16..=16 { let pos = BlockPos::new(SPAWN_POS.x as i32 + x, FLOOR_Y, SPAWN_POS.z as i32 + z); - instance + layer + .chunk .chunk_entry(ChunkPos::from_block_pos(pos)) .or_default(); - instance.set_block(pos, BlockState::QUARTZ_BLOCK); + layer.chunk.set_block(pos, BlockState::QUARTZ_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( mut clients: Query< - (&mut Inventory, &mut Location, &mut Position, &mut GameMode), + ( + &mut Inventory, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), Added, >, - instances: Query>, + layers: Query, With)>, ) { - for (mut inv, mut loc, mut pos, mut game_mode) in &mut clients { - *game_mode = GameMode::Creative; - loc.0 = instances.single(); + for ( + mut inv, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set(SPAWN_POS); + *game_mode = GameMode::Creative; inv.set_slot( 36, From e646c632aff960db5db3f166e53d0ec5c68f69ea Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Mon, 14 Aug 2023 22:41:48 +0200 Subject: [PATCH 21/22] Fix after merge --- crates/valence_generated/Cargo.toml | 1 + crates/valence_generated/src/block.rs | 31 +-------------------------- crates/valence_schem/Cargo.toml | 7 ++---- crates/valence_schem/src/lib.rs | 16 ++++++-------- examples/schem_saving.rs | 6 ++---- 5 files changed, 13 insertions(+), 48 deletions(-) diff --git a/crates/valence_generated/Cargo.toml b/crates/valence_generated/Cargo.toml index df92bdf32..c19606cbe 100644 --- a/crates/valence_generated/Cargo.toml +++ b/crates/valence_generated/Cargo.toml @@ -12,6 +12,7 @@ build = "build/main.rs" [dependencies] valence_math.workspace = true valence_ident.workspace = true +thiserror.workspace = true [build-dependencies] anyhow.workspace = true diff --git a/crates/valence_generated/src/block.rs b/crates/valence_generated/src/block.rs index c05d9ecef..761ab8949 100644 --- a/crates/valence_generated/src/block.rs +++ b/crates/valence_generated/src/block.rs @@ -4,6 +4,7 @@ use std::fmt; use std::fmt::Display; use std::iter::FusedIterator; use std::str::FromStr; +use thiserror::Error; use valence_ident::{ident, Ident}; @@ -49,36 +50,6 @@ fn fmt_block_state(bs: BlockState, f: &mut fmt::Formatter) -> fmt::Result { } } -impl Encode for BlockState { - fn encode(&self, w: impl Write) -> anyhow::Result<()> { - VarInt(self.to_raw() as i32).encode(w) - } -} - -impl Decode<'_> for BlockState { - fn decode(r: &mut &[u8]) -> anyhow::Result { - let id = VarInt::decode(r)?.0; - let errmsg = "invalid block state ID"; - - BlockState::from_raw(id.try_into().context(errmsg)?).context(errmsg) - } -} - -impl Encode for BlockKind { - fn encode(&self, w: impl Write) -> anyhow::Result<()> { - VarInt(self.to_raw() as i32).encode(w) - } -} - -impl Decode<'_> for BlockKind { - fn decode(r: &mut &[u8]) -> anyhow::Result { - let id = VarInt::decode(r)?.0; - let errmsg = "invalid block kind ID"; - - BlockKind::from_raw(id.try_into().context(errmsg)?).context(errmsg) - } -} - #[derive(Debug, Error, PartialEq, Eq)] pub enum ParseBlockStateError { #[error("unknown block kind '{0}'")] diff --git a/crates/valence_schem/Cargo.toml b/crates/valence_schem/Cargo.toml index b4df7dd18..595195fa5 100644 --- a/crates/valence_schem/Cargo.toml +++ b/crates/valence_schem/Cargo.toml @@ -13,11 +13,8 @@ edition.workspace = true flate2.workspace = true glam.workspace = true thiserror.workspace = true -valence_biome.workspace = true -valence_block.workspace = true -valence_core.workspace = true -valence_layer.workspace = true -valence_nbt.workspace = true +valence_server.workspace = true +valence_nbt = {workspace = true, features = ["valence_ident"]} [dev-dependencies] valence.workspace = true diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index a93a98777..afb764b9d 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -29,16 +29,14 @@ use flate2::write::GzEncoder; use flate2::Compression; use glam::{DVec3, IVec3}; use thiserror::Error; -use valence_biome::BiomeId; -use valence_block::{BlockEntityKind, BlockState, ParseBlockStateError}; -use valence_core::block_pos::BlockPos; -use valence_core::chunk_pos::ChunkPos; -use valence_core::ident::Ident; -use valence_core::protocol::var_int::{VarInt, VarIntDecodeError}; -use valence_core::protocol::Encode; -use valence_layer::chunk::{Block as ValenceBlock, Chunk}; -use valence_layer::ChunkLayer; use valence_nbt::{compound, Compound, List, Value}; +use valence_server::block::{BlockEntityKind, ParseBlockStateError}; +use valence_server::ident::Ident; +use valence_server::layer::chunk::{Block as ValenceBlock, Chunk}; +use valence_server::protocol::var_int::{VarInt, VarIntDecodeError}; +use valence_server::protocol::Encode; +use valence_server::registry::biome::BiomeId; +use valence_server::{BlockPos, BlockState, ChunkLayer, ChunkPos}; #[derive(Debug, Clone, PartialEq)] pub struct Schematic { diff --git a/examples/schem_saving.rs b/examples/schem_saving.rs index 0e9f16f46..bf24c6f41 100644 --- a/examples/schem_saving.rs +++ b/examples/schem_saving.rs @@ -3,11 +3,10 @@ use std::path::PathBuf; use valence::prelude::*; -use valence_client::interact_block::InteractBlockEvent; -use valence_client::message::SendMessage; use valence_inventory::HeldItem; -use valence_nbt::compound; use valence_schem::Schematic; +use valence_server::interact_block::InteractBlockEvent; +use valence_server::nbt::compound; const FLOOR_Y: i32 = 64; const SPAWN_POS: DVec3 = DVec3::new(0.5, FLOOR_Y as f64 + 1.0, 0.5); @@ -94,7 +93,6 @@ fn second_pos( if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { continue; } - println!("So this is secondary pos"); let changed = !matches!(pos.map(|pos| pos.0), Some(pos) if pos == *position); if changed { client.send_chat_message(format!( From b24fb5ab0dea8f74b7ced6cb5e1daa2ae2b038af Mon Sep 17 00:00:00 2001 From: MrlnHi Date: Mon, 14 Aug 2023 22:47:13 +0200 Subject: [PATCH 22/22] Fix unit test and formatting --- crates/valence_generated/src/block.rs | 2 +- crates/valence_schem/src/lib.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/valence_generated/src/block.rs b/crates/valence_generated/src/block.rs index 761ab8949..566418687 100644 --- a/crates/valence_generated/src/block.rs +++ b/crates/valence_generated/src/block.rs @@ -4,8 +4,8 @@ use std::fmt; use std::fmt::Display; use std::iter::FusedIterator; use std::str::FromStr; -use thiserror::Error; +use thiserror::Error; use valence_ident::{ident, Ident}; use crate::item::ItemKind; diff --git a/crates/valence_schem/src/lib.rs b/crates/valence_schem/src/lib.rs index afb764b9d..5dfe0595c 100644 --- a/crates/valence_schem/src/lib.rs +++ b/crates/valence_schem/src/lib.rs @@ -968,8 +968,7 @@ mod test { use std::fs; use valence::prelude::*; - use valence_core::ident; - use valence_layer::LayerBundle; + use valence_server::{ident, LayerBundle}; use super::*;