diff --git a/Cargo.toml b/Cargo.toml index 695dc2503..818fe4f04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ default = [ "player_list", "scoreboard", "world_border", + "schem", "weather", "testing", ] @@ -34,6 +35,7 @@ network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] scoreboard = ["dep:valence_scoreboard"] world_border = ["dep:valence_world_border"] +schem = ["dep:valence_schem"] weather = ["dep:valence_weather"] testing = [] @@ -56,6 +58,7 @@ valence_registry.workspace = true valence_scoreboard = { workspace = true, optional = true } valence_weather = { workspace = true, optional = true } valence_world_border = { workspace = true, optional = true } +valence_schem = { workspace = true, optional = true } valence_lang.workspace = true valence_text.workspace = true valence_ident.workspace = true @@ -169,6 +172,7 @@ uuid = "1.3.1" valence = { path = ".", version = "0.2.0-alpha.1" } valence_advancement = { path = "crates/valence_advancement", version = "0.2.0-alpha.1" } valence_anvil = { path = "crates/valence_anvil", version = "0.2.0-alpha.1" } +valence_schem = { path = "crates/valence_schem", version = "0.2.0-alpha.1" } valence_boss_bar = { path = "crates/valence_boss_bar", version = "0.2.0-alpha.1" } valence_build_utils = { path = "crates/valence_build_utils", version = "0.2.0-alpha.1" } valence_entity = { path = "crates/valence_entity", version = "0.2.0-alpha.1" } diff --git a/assets/example_schem.schem b/assets/example_schem.schem new file mode 100644 index 000000000..f732daf39 Binary files /dev/null and b/assets/example_schem.schem differ 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 500757097..566418687 100644 --- a/crates/valence_generated/src/block.rs +++ b/crates/valence_generated/src/block.rs @@ -3,7 +3,9 @@ use std::fmt; use std::fmt::Display; use std::iter::FusedIterator; +use std::str::FromStr; +use thiserror::Error; use valence_ident::{ident, Ident}; use crate::item::ItemKind; @@ -48,6 +50,53 @@ fn fmt_block_state(bs: BlockState, f: &mut fmt::Formatter) -> fmt::Result { } } +#[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_schem/Cargo.toml b/crates/valence_schem/Cargo.toml new file mode 100644 index 000000000..595195fa5 --- /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_server.workspace = true +valence_nbt = {workspace = true, features = ["valence_ident"]} + +[dev-dependencies] +valence.workspace = true diff --git a/crates/valence_schem/README.md b/crates/valence_schem/README.md new file mode 100644 index 000000000..301ee06ae --- /dev/null +++ b/crates/valence_schem/README.md @@ -0,0 +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 new file mode 100644 index 000000000..5dfe0595c --- /dev/null +++ b/crates/valence_schem/src/lib.rs @@ -0,0 +1,1112 @@ +#![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, Write}; +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_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 { + pub metadata: Option, + pub width: u16, + pub height: u16, + pub length: u16, + pub offset: IVec3, + blocks: Option>, + biomes: Option, + 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 kind: BlockEntityKind, + pub data: Compound, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Biomes { + palette: Box<[Ident]>, + 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)] +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)] + Io(#[from] io::Error), + + #[error(transparent)] + Nbt(#[from] valence_nbt::binary::Error), + + #[error("missing schematic")] + MissingSchematic, + + #[error("missing version")] + MissingVersion, + + #[error("unknown version {0} (only versions 1 through 3 are supported)")] + UnknownVersion(i32), + + #[error("missing width")] + MissingWidth, + + #[error("missing height")] + MissingHeight, + + #[error("missing length")] + MissingLength, + + #[error("invalid offset")] + InvalidOffset, + + #[error("missing block palette")] + MissingBlockPalette, + + #[error("invalid block palette")] + InvalidBlockPalette, + + #[error(transparent)] + ParseBlockStateError(#[from] ParseBlockStateError), + + #[error("missing block data")] + MissingBlockData, + + #[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, + + #[error("missing block entity pos")] + MissingBlockEntityPos, + + #[error("invalid block entity pos {0:?}")] + InvalidBlockEntityPos(Vec), + + #[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("biome {0} not in palette {1:?}")] + BiomeNotInPalette(i32, HashMap>), + + #[error("invalid biome ident '{0}'")] + InvalidBiomeIdent(String), + + #[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); +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))) + } + } +} + +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(()) + } +} + +#[derive(Debug, Error)] +pub enum SaveSchematicError { + #[error(transparent)] + Io(#[from] io::Error), + + #[error(transparent)] + Nbt(#[from] valence_nbt::binary::Error), +} + +impl Schematic { + pub fn load(path: impl AsRef) -> 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 root = Compound::from_binary(&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); + }; + + let metadata = root + .get("Metadata") + .and_then(|val| match val { + Value::Compound(val) => Some(val), + _ => None, + }) + .cloned(); + + let Some(&Value::Int(version)) = root.get("Version") else { + return Err(LoadSchematicError::MissingVersion); + }; + if !(1..=3).contains(&version) { + return Err(LoadSchematicError::UnknownVersion(version)); + } + let Some(&Value::Short(width)) = root.get("Width") else { + return Err(LoadSchematicError::MissingWidth); + }; + let width = width as u16; + let Some(&Value::Short(height)) = root.get("Height") else { + return Err(LoadSchematicError::MissingHeight); + }; + let height = height as u16; + let Some(&Value::Short(length)) = root.get("Length") else { + return Err(LoadSchematicError::MissingLength); + }; + let length = length as u16; + let offset = { + let &[x, y, z] = root + .get("Offset") + .and_then(|val| match val { + Value::IntArray(val) => Some(val), + _ => None, + }) + .map(|arr| arr.as_slice()) + .unwrap_or(&[0; 3]) + else { + return Err(LoadSchematicError::InvalidOffset); + }; + IVec3::new(x, y, z) + }; + 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 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 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, + }) + }) + .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))) = 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); + }; + 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 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()) + } + 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.as_str_ident()) 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 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); + }; + let Ok(ident) = Ident::new(biome) else { + return Err(LoadSchematicError::InvalidBiomeIdent(biome.clone())); + }; + Ok((i, ident.to_string_ident())) + }) + .collect(); + let palette = palette?; + + 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| { + let val = val?; + match palette.get(&val) { + Some(val) => Ok(val), + None => { + Err(LoadSchematicError::BiomeNotInPalette(val, palette.clone())) + } + } + }) + .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(); + + if u16::try_from(data.len()) != Ok(width * length) { + return Err(LoadSchematicError::InvalidBiomeCount); + } + + let biomes = Biomes { + palette: palette.into_boxed_slice(), + data: BiomeData::Columns(data.into_boxed_slice()), + }; + Some(biomes) + } + 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.to_string_ident()); + 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 = 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?) + } + _ => None, + }; + + Ok(Self { + metadata, + width, + height, + length, + offset, + blocks, + biomes, + entities, + }) + } + + /// When saving make sure to use gzip + 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![]; + 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", + 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! { + "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()); + nbt.to_binary(&mut z, "")?; + z.flush()?; + Ok(()) + } + + pub fn paste(&self, layer: &mut ChunkLayer, origin: BlockPos, map_biome: F) + where + F: FnMut(Ident<&str>) -> BiomeId, + { + 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(); + 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 = layer + .chunk_entry(ChunkPos::from_block_pos(block_pos)) + .or_default(); + let block = ValenceBlock::new( + *state, + block_entity + .as_ref() + .map(|block_entity| block_entity.data.clone()), + ); + chunk.set_block( + block_pos.x.rem_euclid(16) as u32, + (block_pos.y - min_y) as u32, + block_pos.z.rem_euclid(16) as u32, + block, + ); + } + } + + if let Some(Biomes { palette, data }) = &self.biomes { + 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; + let chunk = layer + .chunk_entry(ChunkPos::from_block_pos(BlockPos::new(x, y, z))) + .or_default(); + + chunk.set_biome( + (x / 4).rem_euclid(4) as u32, + ((y - min_y) / 4) as u32, + (z / 4).rem_euclid(4) as u32, + biome, + ); + } + } + + // TODO: Spawn entities + } + + pub fn copy( + layer: &ChunkLayer, + 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) = layer.block([x, y, z]) else { + panic!("coordinates ({x} {y} {z}) are out of bounds"); + }; + let state = block.state; + let block_entity = block.nbt.and_then(|data| { + Some(BlockEntity { + kind: state.block_entity_kind()?, + data: data.clone(), + }) + }); + 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| { + layer + .chunk(ChunkPos::from_block_pos(BlockPos::new(x, y, z))) + .unwrap() + .biome( + x.rem_euclid(16) as u32 / 4, + (y - layer.min_y()) as u32 / 4, + z.rem_euclid(16) as u32 / 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: BiomeData::Blocks(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 std::fs; + + use valence::prelude::*; + use valence_server::{ident, LayerBundle}; + + use super::*; + + #[test] + fn schematic_copy_paste() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins); + app.update(); + let mut layer = LayerBundle::new( + ident!("overworld"), + app.world.resource(), + app.world.resource(), + app.world.resource(), + ); + + for x in -1..=0 { + for z in -1..=0 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::default()); + } + } + + 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()}), + ), + ); + layer.chunk.set_block( + [5, 2, 0], + BlockState::ANDESITE_WALL + .set(PropName::Up, PropValue::True) + .set(PropName::North, PropValue::Low), + ); + + let schematic = Schematic::copy( + &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 layer.chunk, BlockPos::new(15, 18, 16), |_| { + BiomeId::default() + }); + + 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 = layer.chunk.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 = layer.chunk.block([15, 17, 15]).unwrap(); + assert_eq!(block.state, BlockState::STONE); + assert_eq!(block.nbt, None); + + let block = layer.chunk.block([15, 17, 14]).unwrap(); + assert_eq!(block.state, BlockState::AIR); + assert_eq!(block.nbt, None); + + let block = layer.chunk.block([14, 17, 15]).unwrap(); + assert_eq!(block.state, BlockState::LAPIS_BLOCK); + assert_eq!(block.nbt, None); + + let block = layer.chunk.block([16, 17, 15]).unwrap(); + assert_eq!(block.state, BlockState::STONE); + assert_eq!(block.nbt, None); + + let block = layer.chunk.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! { + "Schematic" => 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" => "minecraft: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, + } + }, + } + } + ); + } + + #[test] + fn schematic_load_save() { + let schem1 = Schematic::load("../../assets/example_schem.schem").unwrap(); + const TEST_PATH: &str = "test.schem"; + schem1.save(TEST_PATH).unwrap(); + let schem2 = Schematic::load(TEST_PATH).unwrap(); + assert_eq!(schem1, schem2); + fs::remove_file(TEST_PATH).unwrap(); + } +} diff --git a/examples/schem_loading.rs b/examples/schem_loading.rs new file mode 100644 index 000000000..81b3ca253 --- /dev/null +++ b/examples/schem_loading.rs @@ -0,0 +1,91 @@ +use std::path::PathBuf; + +use clap::Parser; +use valence::prelude::*; +use valence_schem::Schematic; + +const SPAWN_POS: BlockPos = BlockPos::new(0, 256, 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() { + 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() + .add_plugins(DefaultPlugins) + .insert_resource(SchemRes(schem)) + .add_systems(Startup, setup) + .add_systems(Update, (init_clients, despawn_disconnected_clients)) + .run(); +} + +fn setup( + mut commands: Commands, + dimensions: Res, + biomes: Res, + server: Res, + schem: Res, +) { + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + schem + .0 + .paste(&mut layer.chunk, SPAWN_POS, |_| BiomeId::default()); + commands.spawn(layer); +} + +fn init_clients( + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, +) { + 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, + ]); + *game_mode = GameMode::Creative; + } +} diff --git a/examples/schem_saving.rs b/examples/schem_saving.rs new file mode 100644 index 000000000..bf24c6f41 --- /dev/null +++ b/examples/schem_saving.rs @@ -0,0 +1,442 @@ +#![allow(clippy::type_complexity)] + +use std::path::PathBuf; + +use valence::prelude::*; +use valence_inventory::HeldItem; +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); + +pub fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + init_clients, + first_pos, + second_pos, + origin, + copy_schem, + paste_schem, + save_schem, + place_blocks, + break_blocks, + 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>, &HeldItem)>, + mut block_breaks: EventReader, + mut commands: Commands, +) { + for DiggingEvent { + client: entity, + position, + .. + } in block_breaks.iter() + { + 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; + } + 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)); + } +} + +fn second_pos( + mut clients: Query<(&mut Client, &Inventory, Option<&mut SecondPos>, &HeldItem)>, + mut interacts: EventReader, + mut commands: Commands, +) { + for InteractBlockEvent { + client: entity, + hand, + position, + .. + } in interacts.iter() + { + 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; + } + 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)); + } +} + +fn origin( + mut clients: Query<(&mut Client, &Inventory, Option<&mut Origin>, &HeldItem)>, + mut interacts: EventReader, + mut commands: Commands, +) { + for InteractBlockEvent { + client: entity, + hand, + position, + .. + } in interacts.iter() + { + 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)); + } +} + +#[allow(clippy::type_complexity)] +fn copy_schem( + mut clients: Query<( + &mut Client, + &Inventory, + Option<&FirstPos>, + Option<&SecondPos>, + Option<&Origin>, + &HeldItem, + &Position, + &VisibleChunkLayer, + &Username, + )>, + layers: Query<&ChunkLayer>, + mut interacts: EventReader, + biome_registry: Res, + mut commands: Commands, +) { + for InteractBlockEvent { + client: entity, + hand, + .. + } in interacts.iter() + { + if *hand != Hand::Main { + continue; + } + let Ok(( + mut client, + inv, + pos1, + pos2, + origin, + held_item, + &Position(pos), + &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) { + 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 layers: Query<&mut ChunkLayer>, + mut clients: Query<( + &mut Client, + &Inventory, + Option<&Clipboard>, + &VisibleChunkLayer, + &HeldItem, + &Position, + )>, + mut interacts: EventReader, +) { + for InteractBlockEvent { + client: entity, + hand, + .. + } in interacts.iter() + { + if *hand != Hand::Main { + continue; + } + let Ok(( + mut client, + inv, + clipboard, + &VisibleChunkLayer(layer), + held_item, + &Position(position), + )) = clients.get_mut(*entity) + else { + continue; + }; + 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) { + 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 + )); + } +} + +fn save_schem( + mut clients: Query<( + &mut Client, + &Inventory, + Option<&Clipboard>, + &HeldItem, + &Username, + )>, + mut interacts: EventReader, +) { + for InteractBlockEvent { + client: entity, + hand, + .. + } in interacts.iter() + { + if *hand != Hand::Main { + continue; + } + let Ok((mut client, inv, clipboard, held_item, Username(username))) = + clients.get_mut(*entity) + else { + continue; + }; + let slot = inv.slot(held_item.slot()); + 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 layers: Query<&mut ChunkLayer>, + mut events: EventReader, +) { + let mut layer = layers.single_mut(); + + for event in events.iter() { + let Ok((inventory, held_item)) = clients.get(event.client) else { + continue; + }; + if event.hand != Hand::Main { + continue; + } + + let Some(stack) = inventory.slot(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); + layer.set_block(pos, block_kind.to_state()); + } +} + +fn break_blocks( + mut layers: Query<&mut ChunkLayer>, + inventories: Query<(&Inventory, &HeldItem)>, + mut events: EventReader, +) { + let mut layer = layers.single_mut(); + + for DiggingEvent { + client, position, .. + } in events.iter() + { + let Ok((inv, held_item)) = inventories.get(*client) else { + continue; + }; + + let slot = inv.slot(held_item.slot()); + if !matches!(slot, Some(ItemStack {item, ..}) if *item == ItemKind::WoodenAxe) { + layer.set_block(*position, BlockState::AIR); + } + } +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Res, + biomes: Res, +) { + 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); + layer + .chunk + .chunk_entry(ChunkPos::from_block_pos(pos)) + .or_default(); + layer.chunk.set_block(pos, BlockState::QUARTZ_BLOCK); + } + } + + commands.spawn(layer); +} + +fn init_clients( + mut clients: Query< + ( + &mut Inventory, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, +) { + 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, + 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/src/lib.rs b/src/lib.rs index a94058194..fb62a1962 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,8 @@ pub use valence_network as network; #[cfg(feature = "player_list")] pub use valence_player_list as player_list; use valence_registry::RegistryPlugin; +#[cfg(feature = "schem")] +pub use valence_schem as schem; #[cfg(feature = "scoreboard")] pub use valence_scoreboard as scoreboard; use valence_server::abilities::AbilitiesPlugin;