mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 22:41:30 +11:00
Add block entities (#32)
This PR aims to add block entities. Fixes #5 --------- Co-authored-by: Ryan Johnson <ryanj00a@gmail.com>
This commit is contained in:
parent
bcd686990d
commit
1ceafe0ce0
|
@ -28,7 +28,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,8 +39,13 @@ tokio = { version = "1.25.0", features = ["full"] }
|
|||
tracing = "0.1.37"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
uuid = { version = "1.1.2", features = ["serde"] }
|
||||
valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
|
||||
valence_protocol = { version = "0.1.0", path = "../valence_protocol", features = ["encryption", "compression"] }
|
||||
valence_nbt = { version = "0.5.0", path = "../valence_nbt", features = [
|
||||
"uuid",
|
||||
] }
|
||||
valence_protocol = { version = "0.1.0", path = "../valence_protocol", features = [
|
||||
"encryption",
|
||||
"compression",
|
||||
] }
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11.12"
|
||||
|
|
|
@ -60,7 +60,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -50..50 {
|
||||
for x in -50..50 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
126
crates/valence/examples/block_entities.rs
Normal file
126
crates/valence/examples/block_entities.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use valence::client::despawn_disconnected_clients;
|
||||
use valence::client::event::{default_event_handler, ChatMessage, UseItemOnBlock};
|
||||
use valence::prelude::*;
|
||||
use valence_nbt::{compound, List};
|
||||
use valence_protocol::types::Hand;
|
||||
|
||||
const FLOOR_Y: i32 = 64;
|
||||
const SIGN_POS: [i32; 3] = [3, FLOOR_Y + 1, 2];
|
||||
const SKULL_POS: BlockPos = BlockPos::new(3, FLOOR_Y + 1, 3);
|
||||
|
||||
pub fn main() {
|
||||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_system_to_stage(EventLoop, default_event_handler)
|
||||
.add_system_to_stage(EventLoop, event_handler)
|
||||
.add_system_set(PlayerList::default_system_set())
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(world: &mut World) {
|
||||
let mut instance = world
|
||||
.resource::<Server>()
|
||||
.new_instance(DimensionId::default());
|
||||
|
||||
for z in -5..5 {
|
||||
for x in -5..5 {
|
||||
instance.insert_chunk([x, z], Chunk::default());
|
||||
}
|
||||
}
|
||||
|
||||
for z in 0..16 {
|
||||
for x in 0..8 {
|
||||
instance.set_block([x, FLOOR_Y, z], BlockState::WHITE_CONCRETE);
|
||||
}
|
||||
}
|
||||
|
||||
instance.set_block(
|
||||
[3, FLOOR_Y + 1, 1],
|
||||
BlockState::CHEST.set(PropName::Facing, PropValue::West),
|
||||
);
|
||||
instance.set_block(
|
||||
SIGN_POS,
|
||||
Block::with_nbt(
|
||||
BlockState::OAK_SIGN.set(PropName::Rotation, PropValue::_4),
|
||||
compound! {
|
||||
"Text1" => "Type in chat:".color(Color::RED),
|
||||
},
|
||||
),
|
||||
);
|
||||
instance.set_block(
|
||||
SKULL_POS,
|
||||
BlockState::PLAYER_HEAD.set(PropName::Rotation, PropValue::_12),
|
||||
);
|
||||
|
||||
world.spawn(instance);
|
||||
}
|
||||
|
||||
fn init_clients(
|
||||
mut clients: Query<&mut Client, Added<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
for mut client in &mut clients {
|
||||
client.set_position([1.5, FLOOR_Y as f64 + 1.0, 1.5]);
|
||||
client.set_yaw(-90.0);
|
||||
client.set_instance(instances.single());
|
||||
client.set_game_mode(GameMode::Creative);
|
||||
}
|
||||
}
|
||||
|
||||
fn event_handler(
|
||||
clients: Query<&Client>,
|
||||
mut messages: EventReader<ChatMessage>,
|
||||
mut block_interacts: EventReader<UseItemOnBlock>,
|
||||
mut instances: Query<&mut Instance>,
|
||||
) {
|
||||
let mut instance = instances.single_mut();
|
||||
for ChatMessage {
|
||||
client, message, ..
|
||||
} in messages.iter()
|
||||
{
|
||||
let Ok(client) = clients.get(*client) else {
|
||||
continue
|
||||
};
|
||||
|
||||
let mut sign = instance.block_mut(SIGN_POS).unwrap();
|
||||
let nbt = sign.nbt_mut().unwrap();
|
||||
nbt.insert("Text2", message.to_string().color(Color::DARK_GREEN));
|
||||
nbt.insert("Text3", format!("~{}", client.username()).italic());
|
||||
}
|
||||
|
||||
for UseItemOnBlock {
|
||||
client,
|
||||
position,
|
||||
hand,
|
||||
..
|
||||
} in block_interacts.iter()
|
||||
{
|
||||
if *hand == Hand::Main && *position == SKULL_POS {
|
||||
let Ok(client) = clients.get(*client) else {
|
||||
continue
|
||||
};
|
||||
|
||||
let Some(textures) = client.properties().iter().find(|prop| prop.name == "textures") else {
|
||||
continue
|
||||
};
|
||||
|
||||
let mut skull = instance.block_mut(SKULL_POS).unwrap();
|
||||
let nbt = skull.nbt_mut().unwrap();
|
||||
*nbt = compound! {
|
||||
"SkullOwner" => compound! {
|
||||
"Id" => client.uuid(),
|
||||
"Properties" => compound! {
|
||||
"textures" => List::Compound(vec![compound! {
|
||||
"Value" => textures.value.clone(),
|
||||
}])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ fn digging_creative_mode(
|
|||
continue;
|
||||
};
|
||||
if client.game_mode() == GameMode::Creative {
|
||||
instance.set_block_state(event.position, BlockState::AIR);
|
||||
instance.set_block(event.position, BlockState::AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ fn digging_survival_mode(
|
|||
continue;
|
||||
};
|
||||
if client.game_mode() == GameMode::Survival {
|
||||
instance.set_block_state(event.position, BlockState::AIR);
|
||||
instance.set_block(event.position, BlockState::AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,6 @@ fn place_blocks(
|
|||
inventory.replace_slot(slot_id, slot);
|
||||
}
|
||||
let real_pos = event.position.get_in_direction(event.face);
|
||||
instance.set_block_state(real_pos, block_kind.to_state());
|
||||
instance.set_block(real_pos, block_kind.to_state());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,11 +34,11 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
instance.set_block_state(CHEST_POS, BlockState::CHEST);
|
||||
instance.set_block_state(
|
||||
instance.set_block(CHEST_POS, BlockState::CHEST);
|
||||
instance.set_block(
|
||||
[CHEST_POS[0], CHEST_POS[1] - 1, CHEST_POS[2]],
|
||||
BlockState::STONE,
|
||||
);
|
||||
|
|
|
@ -58,7 +58,7 @@ fn setup(world: &mut World) {
|
|||
};
|
||||
|
||||
for y in 0..SPAWN_Y {
|
||||
instance.set_block_state([x, y, z], block);
|
||||
instance.set_block([x, y, z], block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in BOARD_MIN_Z..=BOARD_MAX_Z {
|
||||
for x in BOARD_MIN_X..=BOARD_MAX_X {
|
||||
instance.set_block_state([x, BOARD_Y, z], BlockState::DIRT);
|
||||
instance.set_block([x, BOARD_Y, z], BlockState::DIRT);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,7 +173,7 @@ fn update_board(
|
|||
BlockState::DIRT
|
||||
};
|
||||
|
||||
instance.set_block_state([x, BOARD_Y, z], block);
|
||||
instance.set_block([x, BOARD_Y, z], block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ fn setup(world: &mut World) {
|
|||
}
|
||||
}
|
||||
|
||||
instance.set_block_state(SPAWN_POS, BlockState::BEDROCK);
|
||||
instance.set_block(SPAWN_POS, BlockState::BEDROCK);
|
||||
|
||||
let instance_id = world.spawn(instance).id();
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], block);
|
||||
instance.set_block([x, SPAWN_Y, z], block);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -189,11 +189,11 @@ fn reset(client: &mut Client, state: &mut GameState, instance: &mut Instance) {
|
|||
state.combo = 0;
|
||||
|
||||
for block in &state.blocks {
|
||||
instance.set_block_state(*block, BlockState::AIR);
|
||||
instance.set_block(*block, BlockState::AIR);
|
||||
}
|
||||
state.blocks.clear();
|
||||
state.blocks.push_back(START_POS);
|
||||
instance.set_block_state(START_POS, BlockState::STONE);
|
||||
instance.set_block(START_POS, BlockState::STONE);
|
||||
|
||||
for _ in 0..10 {
|
||||
generate_next_block(state, instance, false);
|
||||
|
@ -212,7 +212,7 @@ fn reset(client: &mut Client, state: &mut GameState, instance: &mut Instance) {
|
|||
fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game: bool) {
|
||||
if in_game {
|
||||
let removed_block = state.blocks.pop_front().unwrap();
|
||||
instance.set_block_state(removed_block, BlockState::AIR);
|
||||
instance.set_block(removed_block, BlockState::AIR);
|
||||
|
||||
state.score += 1
|
||||
}
|
||||
|
@ -228,7 +228,7 @@ fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game:
|
|||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
instance.set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap());
|
||||
instance.set_block(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap());
|
||||
state.blocks.push_back(block_pos);
|
||||
|
||||
// Combo System
|
||||
|
|
|
@ -49,7 +49,7 @@ fn setup(world: &mut World) {
|
|||
}
|
||||
}
|
||||
|
||||
instance.set_block_state([0, SPAWN_Y, 0], BlockState::BEDROCK);
|
||||
instance.set_block([0, SPAWN_Y, 0], BlockState::BEDROCK);
|
||||
|
||||
world.spawn(instance);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::LIGHT_GRAY_WOOL);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::LIGHT_GRAY_WOOL);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ fn setup(world: &mut World) {
|
|||
|
||||
for z in -25..25 {
|
||||
for x in -25..25 {
|
||||
instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ pub use chunk_entry::*;
|
|||
use glam::{DVec3, Vec3};
|
||||
use num::integer::div_ceil;
|
||||
use rustc_hash::FxHashMap;
|
||||
use valence_protocol::block::BlockState;
|
||||
use valence_protocol::packets::s2c::particle::{Particle, ParticleS2c};
|
||||
use valence_protocol::packets::s2c::play::{SetActionBarText, SoundEffect};
|
||||
use valence_protocol::types::SoundCategory;
|
||||
|
@ -15,7 +14,7 @@ use valence_protocol::{BlockPos, EncodePacket, LengthPrefixedArray, Sound, Text}
|
|||
|
||||
use crate::dimension::DimensionId;
|
||||
use crate::entity::McEntity;
|
||||
pub use crate::instance::chunk::Chunk;
|
||||
pub use crate::instance::chunk::{Block, BlockMut, BlockRef, Chunk};
|
||||
use crate::packet::{PacketWriter, WritePacket};
|
||||
use crate::server::{Server, SharedServer};
|
||||
use crate::view::ChunkPos;
|
||||
|
@ -218,59 +217,90 @@ impl Instance {
|
|||
self.packet_buf.shrink_to_fit();
|
||||
}
|
||||
|
||||
/// Gets the block state at an absolute block position in world space. Only
|
||||
/// works for blocks in loaded chunks.
|
||||
/// Gets a reference to the block at an absolute block position in world
|
||||
/// space. Only works for blocks in loaded chunks.
|
||||
///
|
||||
/// If the position is not inside of a chunk, then [`BlockState::AIR`] is
|
||||
/// If the position is not inside of a chunk, then [`Option::None`] is
|
||||
/// returned.
|
||||
pub fn block_state(&self, pos: impl Into<BlockPos>) -> BlockState {
|
||||
pub fn block(&self, pos: impl Into<BlockPos>) -> Option<BlockRef> {
|
||||
let pos = pos.into();
|
||||
|
||||
let Some(y) = pos.y.checked_sub(self.info.min_y).and_then(|y| y.try_into().ok()) else {
|
||||
return BlockState::AIR;
|
||||
return None;
|
||||
};
|
||||
|
||||
if y >= self.info.section_count * 16 {
|
||||
return BlockState::AIR;
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(chunk) = self.chunk(ChunkPos::from_block_pos(pos)) else {
|
||||
return BlockState::AIR;
|
||||
return None;
|
||||
};
|
||||
|
||||
chunk.block_state(
|
||||
Some(chunk.block(
|
||||
pos.x.rem_euclid(16) as usize,
|
||||
y,
|
||||
pos.z.rem_euclid(16) as usize,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
/// Sets the block state at an absolute block position in world space. The
|
||||
/// previous block state at the position is returned.
|
||||
/// Gets a mutable reference to the block at an absolute block position in
|
||||
/// world space. Only works for blocks in loaded chunks.
|
||||
///
|
||||
/// If the position is not within a loaded chunk or otherwise out of bounds,
|
||||
/// then [`BlockState::AIR`] is returned with no effect.
|
||||
pub fn set_block_state(&mut self, pos: impl Into<BlockPos>, block: BlockState) -> BlockState {
|
||||
/// If the position is not inside of a chunk, then [`Option::None`] is
|
||||
/// returned.
|
||||
pub fn block_mut(&mut self, pos: impl Into<BlockPos>) -> Option<BlockMut> {
|
||||
let pos = pos.into();
|
||||
|
||||
let Some(y) = pos.y.checked_sub(self.info.min_y).and_then(|y| y.try_into().ok()) else {
|
||||
return BlockState::AIR;
|
||||
return None;
|
||||
};
|
||||
|
||||
if y >= self.info.section_count * 16 {
|
||||
return BlockState::AIR;
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(chunk) = self.chunk_mut(ChunkPos::from_block_pos(pos)) else {
|
||||
return BlockState::AIR;
|
||||
return None;
|
||||
};
|
||||
|
||||
chunk.set_block_state(
|
||||
Some(chunk.block_mut(
|
||||
pos.x.rem_euclid(16) as usize,
|
||||
y,
|
||||
pos.z.rem_euclid(16) as usize,
|
||||
))
|
||||
}
|
||||
|
||||
/// Sets the block at an absolute block position in world space. The
|
||||
/// previous block at the position is returned.
|
||||
///
|
||||
/// If the position is not within a loaded chunk or otherwise out of bounds,
|
||||
/// then [`Option::None`] is returned with no effect.
|
||||
pub fn set_block(
|
||||
&mut self,
|
||||
pos: impl Into<BlockPos>,
|
||||
block: impl Into<Block>,
|
||||
) -> Option<Block> {
|
||||
let pos = pos.into();
|
||||
|
||||
let Some(y) = pos.y.checked_sub(self.info.min_y).and_then(|y| y.try_into().ok()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if y >= self.info.section_count * 16 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(chunk) = self.chunk_mut(ChunkPos::from_block_pos(pos)) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(chunk.set_block(
|
||||
pos.x.rem_euclid(16) as usize,
|
||||
y,
|
||||
pos.z.rem_euclid(16) as usize,
|
||||
block,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
/// Writes a packet into the global packet buffer of this instance. All
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
// Using nonstandard mutex to avoid poisoning API.
|
||||
use parking_lot::Mutex;
|
||||
use valence_nbt::compound;
|
||||
use valence_protocol::block::BlockState;
|
||||
use valence_nbt::{compound, Compound};
|
||||
use valence_protocol::block::{BlockEntity, BlockState};
|
||||
use valence_protocol::packets::s2c::play::{
|
||||
BlockUpdate, ChunkDataAndUpdateLightEncode, UpdateSectionBlocksEncode,
|
||||
BlockEntityData, BlockUpdate, ChunkDataAndUpdateLightEncode, UpdateSectionBlocksEncode,
|
||||
};
|
||||
use valence_protocol::types::ChunkDataBlockEntity;
|
||||
use valence_protocol::{BlockPos, Encode, VarInt, VarLong};
|
||||
|
||||
use crate::biome::BiomeId;
|
||||
|
@ -32,6 +36,9 @@ pub struct Chunk<const LOADED: bool = false> {
|
|||
/// Tracks if any clients are in view of this (loaded) chunk. Useful for
|
||||
/// knowing when a chunk should be unloaded.
|
||||
viewed: AtomicBool,
|
||||
/// Block entities in this chunk
|
||||
block_entities: BTreeMap<u32, BlockEntity>,
|
||||
modified_block_entities: BTreeSet<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
|
@ -46,6 +53,124 @@ struct Section {
|
|||
section_updates: Vec<VarLong>,
|
||||
}
|
||||
|
||||
/// Represents a block with an optional block entity
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Block {
|
||||
state: BlockState,
|
||||
/// Nbt of the block entity
|
||||
nbt: Option<Compound>,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub const AIR: Self = Self {
|
||||
state: BlockState::AIR,
|
||||
nbt: None,
|
||||
};
|
||||
|
||||
pub fn new(state: BlockState) -> Self {
|
||||
Self {
|
||||
state,
|
||||
nbt: state.block_entity_kind().map(|_| Compound::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_nbt(state: BlockState, nbt: Compound) -> Self {
|
||||
Self {
|
||||
state,
|
||||
nbt: state.block_entity_kind().map(|_| nbt),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn state(&self) -> BlockState {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockState> for Block {
|
||||
fn from(value: BlockState) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockRef<'_>> for Block {
|
||||
fn from(BlockRef { state, nbt }: BlockRef<'_>) -> Self {
|
||||
Self {
|
||||
state,
|
||||
nbt: nbt.cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockMut<'_>> for Block {
|
||||
fn from(value: BlockMut<'_>) -> Self {
|
||||
Self {
|
||||
state: value.state,
|
||||
nbt: value.nbt().cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Block> for Block {
|
||||
fn from(value: &Block) -> Self {
|
||||
value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mut Block> for Block {
|
||||
fn from(value: &mut Block) -> Self {
|
||||
value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable reference to a block in a chunk
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct BlockRef<'a> {
|
||||
state: BlockState,
|
||||
nbt: Option<&'a Compound>,
|
||||
}
|
||||
|
||||
impl<'a> BlockRef<'a> {
|
||||
pub const fn state(&self) -> BlockState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub const fn nbt(&self) -> Option<&'a Compound> {
|
||||
self.nbt
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable reference to a block in a chunk
|
||||
#[derive(Debug)]
|
||||
pub struct BlockMut<'a> {
|
||||
state: BlockState,
|
||||
/// Entry into the block entity map.
|
||||
entry: Entry<'a, u32, BlockEntity>,
|
||||
modified: &'a mut BTreeSet<u32>,
|
||||
}
|
||||
|
||||
impl<'a> BlockMut<'a> {
|
||||
pub const fn state(&self) -> BlockState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn nbt(&self) -> Option<&Compound> {
|
||||
match &self.entry {
|
||||
Entry::Occupied(entry) => Some(&entry.get().nbt),
|
||||
Entry::Vacant(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nbt_mut(&mut self) -> Option<&mut Compound> {
|
||||
match &mut self.entry {
|
||||
Entry::Occupied(entry) => {
|
||||
self.modified.insert(*entry.key());
|
||||
Some(&mut entry.get_mut().nbt)
|
||||
}
|
||||
Entry::Vacant(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SECTION_BLOCK_COUNT: usize = 16 * 16 * 16;
|
||||
const SECTION_BIOME_COUNT: usize = 4 * 4 * 4;
|
||||
|
||||
|
@ -59,6 +184,8 @@ impl Chunk<false> {
|
|||
cached_init_packets: Mutex::new(vec![]),
|
||||
refresh: true,
|
||||
viewed: AtomicBool::new(false),
|
||||
block_entities: BTreeMap::new(),
|
||||
modified_block_entities: BTreeSet::new(),
|
||||
};
|
||||
|
||||
chunk.resize(section_count);
|
||||
|
@ -84,12 +211,15 @@ impl Chunk<false> {
|
|||
|
||||
pub(super) fn into_loaded(self) -> Chunk<true> {
|
||||
debug_assert!(self.refresh);
|
||||
debug_assert!(self.modified_block_entities.is_empty());
|
||||
|
||||
Chunk {
|
||||
sections: self.sections,
|
||||
cached_init_packets: self.cached_init_packets,
|
||||
refresh: true,
|
||||
viewed: AtomicBool::new(false),
|
||||
block_entities: self.block_entities,
|
||||
modified_block_entities: self.modified_block_entities,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,6 +237,8 @@ impl Clone for Chunk {
|
|||
cached_init_packets: Mutex::new(vec![]),
|
||||
refresh: true,
|
||||
viewed: AtomicBool::new(false),
|
||||
block_entities: self.block_entities.clone(),
|
||||
modified_block_entities: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +264,8 @@ impl Chunk<true> {
|
|||
cached_init_packets: Mutex::new(vec![]),
|
||||
refresh: true,
|
||||
viewed: AtomicBool::new(false),
|
||||
block_entities: self.block_entities.clone(),
|
||||
modified_block_entities: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,12 +295,15 @@ impl Chunk<true> {
|
|||
for sect in &mut self.sections {
|
||||
sect.section_updates.clear();
|
||||
}
|
||||
self.modified_block_entities.clear();
|
||||
|
||||
Chunk {
|
||||
sections: self.sections,
|
||||
cached_init_packets: self.cached_init_packets,
|
||||
refresh: true,
|
||||
viewed: AtomicBool::new(false),
|
||||
block_entities: self.block_entities,
|
||||
modified_block_entities: self.modified_block_entities,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,6 +345,24 @@ impl Chunk<true> {
|
|||
});
|
||||
}
|
||||
}
|
||||
for idx in &self.modified_block_entities {
|
||||
let Some(block_entity) = self.block_entities.get(idx) else {
|
||||
continue
|
||||
};
|
||||
let x = idx % 16;
|
||||
let z = (idx / 16) % 16;
|
||||
let y = idx / 16 / 16;
|
||||
|
||||
let global_x = pos.x * 16 + x as i32;
|
||||
let global_y = info.min_y + y as i32;
|
||||
let global_z = pos.z * 16 + z as i32;
|
||||
|
||||
writer.write_packet(&BlockEntityData {
|
||||
position: BlockPos::new(global_x, global_y, global_z),
|
||||
kind: block_entity.kind,
|
||||
data: Cow::Borrowed(&block_entity.nbt),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,6 +412,23 @@ impl Chunk<true> {
|
|||
&mut compression_scratch,
|
||||
);
|
||||
|
||||
let block_entities: Vec<_> = self
|
||||
.block_entities
|
||||
.iter()
|
||||
.map(|(idx, block_entity)| {
|
||||
let x = idx % 16;
|
||||
let z = idx / 16 % 16;
|
||||
let y = (idx / 16 / 16) as i16 + info.min_y as i16;
|
||||
|
||||
ChunkDataBlockEntity {
|
||||
packed_xz: ((x << 4) | z) as i8,
|
||||
y,
|
||||
kind: block_entity.kind,
|
||||
data: Cow::Borrowed(&block_entity.nbt),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
writer.write_packet(&ChunkDataAndUpdateLightEncode {
|
||||
chunk_x: pos.x,
|
||||
chunk_z: pos.z,
|
||||
|
@ -264,7 +436,7 @@ impl Chunk<true> {
|
|||
// TODO: MOTION_BLOCKING heightmap
|
||||
},
|
||||
blocks_and_biomes: scratch,
|
||||
block_entities: &[],
|
||||
block_entities: &block_entities,
|
||||
trust_edges: true,
|
||||
sky_light_mask: &info.filler_sky_light_mask,
|
||||
block_light_mask: &[],
|
||||
|
@ -284,6 +456,7 @@ impl Chunk<true> {
|
|||
for sect in &mut self.sections {
|
||||
sect.section_updates.clear();
|
||||
}
|
||||
self.modified_block_entities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,6 +490,7 @@ impl<const LOADED: bool> Chunk<LOADED> {
|
|||
|
||||
/// Sets the block state at the provided offsets in the chunk. The previous
|
||||
/// block state at the position is returned.
|
||||
/// Also, the corresponding block entity is placed.
|
||||
///
|
||||
/// **Note**: The arguments to this function are offsets from the minimum
|
||||
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||
|
@ -359,6 +533,23 @@ impl<const LOADED: bool> Chunk<LOADED> {
|
|||
}
|
||||
}
|
||||
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
match block.block_entity_kind() {
|
||||
Some(kind) => {
|
||||
let block_entity = BlockEntity {
|
||||
kind,
|
||||
nbt: compound! {},
|
||||
};
|
||||
self.block_entities.insert(idx, block_entity);
|
||||
if LOADED && !self.refresh {
|
||||
self.modified_block_entities.insert(idx);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.block_entities.remove(&idx);
|
||||
}
|
||||
}
|
||||
|
||||
old_block
|
||||
}
|
||||
|
||||
|
@ -424,6 +615,236 @@ impl<const LOADED: bool> Chunk<LOADED> {
|
|||
}
|
||||
|
||||
sect.block_states.fill(block);
|
||||
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
for y in 0..16 {
|
||||
let y = sect_y * 16 + y;
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
match block.block_entity_kind() {
|
||||
Some(kind) => {
|
||||
let block_entity = BlockEntity {
|
||||
kind,
|
||||
nbt: compound! {},
|
||||
};
|
||||
self.block_entities.insert(idx, block_entity);
|
||||
if LOADED && !self.refresh {
|
||||
self.modified_block_entities.insert(idx);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.block_entities.remove(&idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a reference to the block entity at the provided offsets in the
|
||||
/// chunk.
|
||||
///
|
||||
/// **Note**: The arguments to this function are offsets from the minimum
|
||||
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||
#[track_caller]
|
||||
pub fn block_entity(&self, x: usize, y: usize, z: usize) -> Option<&BlockEntity> {
|
||||
assert!(
|
||||
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||
);
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
|
||||
self.block_entities.get(&idx)
|
||||
}
|
||||
|
||||
/// Sets the block entity at the provided offsets in the chunk.
|
||||
/// Returns the block entity that was there before.
|
||||
///
|
||||
/// **Note**: The arguments to this function are offsets from the minimum
|
||||
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||
#[track_caller]
|
||||
pub fn set_block_entity(
|
||||
&mut self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
z: usize,
|
||||
block_entity: BlockEntity,
|
||||
) -> Option<BlockEntity> {
|
||||
assert!(
|
||||
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||
);
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
let old = self.block_entities.insert(idx, block_entity);
|
||||
if LOADED {
|
||||
self.modified_block_entities.insert(idx);
|
||||
self.cached_init_packets.get_mut().clear();
|
||||
}
|
||||
old
|
||||
}
|
||||
|
||||
/// Edits the block entity at the provided offsets in the chunk.
|
||||
/// Does nothing if there is no block entity at the provided offsets.
|
||||
///
|
||||
/// **Note**: The arguments to this function are offsets from the minimum
|
||||
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||
#[track_caller]
|
||||
pub fn edit_block_entity(
|
||||
&mut self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
z: usize,
|
||||
f: impl FnOnce(&mut BlockEntity),
|
||||
) {
|
||||
assert!(
|
||||
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||
);
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
self.block_entities.entry(idx).and_modify(f);
|
||||
if LOADED {
|
||||
self.modified_block_entities.insert(idx);
|
||||
self.cached_init_packets.get_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the block at the provided offsets in the chunk. The previous
|
||||
/// block at the position is returned.
|
||||
///
|
||||
/// **Note**: The arguments to this function are offsets from the minimum
|
||||
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||
#[track_caller]
|
||||
pub fn set_block(&mut self, x: usize, y: usize, z: usize, block: impl Into<Block>) -> Block {
|
||||
assert!(
|
||||
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||
);
|
||||
|
||||
let Block { state, nbt } = block.into();
|
||||
let old_state = {
|
||||
let sect_y = y / 16;
|
||||
let sect = &mut self.sections[sect_y];
|
||||
let idx = x + z * 16 + y % 16 * 16 * 16;
|
||||
|
||||
let old_state = sect.block_states.set(idx, state);
|
||||
|
||||
if state != old_state {
|
||||
// Update non-air count.
|
||||
match (state.is_air(), old_state.is_air()) {
|
||||
(true, false) => sect.non_air_count -= 1,
|
||||
(false, true) => sect.non_air_count += 1,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if LOADED && !self.refresh {
|
||||
let compact =
|
||||
(state.to_raw() as i64) << 12 | (x << 8 | z << 4 | (y % 16)) as i64;
|
||||
sect.section_updates.push(VarLong(compact));
|
||||
}
|
||||
}
|
||||
old_state
|
||||
};
|
||||
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
let old_block_entity = match nbt.and_then(|nbt| {
|
||||
state
|
||||
.block_entity_kind()
|
||||
.map(|kind| BlockEntity { kind, nbt })
|
||||
}) {
|
||||
Some(block_entity) => self.block_entities.insert(idx, block_entity),
|
||||
None => self.block_entities.remove(&idx),
|
||||
};
|
||||
if LOADED && !self.refresh {
|
||||
self.modified_block_entities.insert(idx);
|
||||
self.cached_init_packets.get_mut().clear();
|
||||
}
|
||||
|
||||
Block {
|
||||
state: old_state,
|
||||
nbt: old_block_entity.map(|block_entity| block_entity.nbt),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a reference to the block at the provided offsets in the chunk.
|
||||
///
|
||||
/// **Note**: The arguments to this function are offsets from the minimum
|
||||
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||
#[track_caller]
|
||||
pub fn block(&self, x: usize, y: usize, z: usize) -> BlockRef {
|
||||
assert!(
|
||||
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||
);
|
||||
|
||||
let state = self.sections[y / 16]
|
||||
.block_states
|
||||
.get(x + z * 16 + y % 16 * 16 * 16);
|
||||
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
|
||||
let nbt = self
|
||||
.block_entities
|
||||
.get(&idx)
|
||||
.map(|block_entity| &block_entity.nbt);
|
||||
|
||||
BlockRef { state, nbt }
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the block at the provided offsets in the
|
||||
/// chunk.
|
||||
///
|
||||
/// **Note**: The arguments to this function are offsets from the minimum
|
||||
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||
#[track_caller]
|
||||
pub fn block_mut(&mut self, x: usize, y: usize, z: usize) -> BlockMut {
|
||||
assert!(
|
||||
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||
);
|
||||
|
||||
let state = self.sections[y / 16]
|
||||
.block_states
|
||||
.get(x + z * 16 + y % 16 * 16 * 16);
|
||||
|
||||
let idx = (x + z * 16 + y * 16 * 16) as _;
|
||||
|
||||
let entry = self.block_entities.entry(idx);
|
||||
|
||||
BlockMut {
|
||||
state,
|
||||
entry,
|
||||
modified: &mut self.modified_block_entities,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the biome at the provided biome offsets in the chunk.
|
||||
|
@ -522,6 +943,8 @@ impl<const LOADED: bool> Chunk<LOADED> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use valence_protocol::block::BlockEntityKind;
|
||||
|
||||
use super::*;
|
||||
use crate::protocol::block::BlockState;
|
||||
|
||||
|
@ -565,4 +988,37 @@ mod tests {
|
|||
chunk.fill_block_states(0, BlockState::AIR);
|
||||
check(&chunk, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_entity_changes() {
|
||||
let mut chunk = Chunk::new(5).into_loaded();
|
||||
chunk.refresh = false;
|
||||
|
||||
assert!(chunk.block_entity(0, 0, 0).is_none());
|
||||
chunk.set_block_state(0, 0, 0, BlockState::CHEST);
|
||||
assert_eq!(
|
||||
chunk.block_entity(0, 0, 0),
|
||||
Some(&BlockEntity {
|
||||
kind: BlockEntityKind::Chest,
|
||||
nbt: compound! {}
|
||||
})
|
||||
);
|
||||
chunk.set_block_state(0, 0, 0, BlockState::STONE);
|
||||
assert!(chunk.block_entity(0, 0, 0).is_none());
|
||||
|
||||
chunk.fill_block_states(2, BlockState::CHEST);
|
||||
for x in 0..16 {
|
||||
for z in 0..16 {
|
||||
for y in 32..47 {
|
||||
assert_eq!(
|
||||
chunk.block_entity(x, y, z),
|
||||
Some(&BlockEntity {
|
||||
kind: BlockEntityKind::Chest,
|
||||
nbt: compound! {}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ pub mod prelude {
|
|||
EntityAnimation, EntityKind, EntityStatus, McEntity, McEntityManager, TrackedData,
|
||||
};
|
||||
pub use glam::DVec3;
|
||||
pub use instance::{Chunk, Instance};
|
||||
pub use instance::{Block, BlockMut, BlockRef, Chunk, Instance};
|
||||
pub use inventory::{Inventory, InventoryKind, OpenInventory};
|
||||
pub use player_list::{PlayerList, PlayerListEntry};
|
||||
pub use protocol::block::{BlockState, PropName, PropValue};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use num_integer::div_ceil;
|
||||
use num_integer::{div_ceil, Integer};
|
||||
use thiserror::Error;
|
||||
use valence::biome::BiomeId;
|
||||
use valence::instance::Chunk;
|
||||
use valence::protocol::block::{BlockKind, PropName, PropValue};
|
||||
use valence::protocol::block::{BlockEntity, BlockEntityKind, BlockKind, PropName, PropValue};
|
||||
use valence::protocol::Ident;
|
||||
use valence_nbt::{Compound, List, Value};
|
||||
|
||||
|
@ -51,6 +51,14 @@ pub enum ToValenceError {
|
|||
BadBiomeLongCount,
|
||||
#[error("invalid biome palette index")]
|
||||
BadBiomePaletteIndex,
|
||||
#[error("missing block entities")]
|
||||
MissingBlockEntity,
|
||||
#[error("missing block entity ident")]
|
||||
MissingBlockEntityIdent,
|
||||
#[error("invalid block entity ident of \"{0}\"")]
|
||||
UnknownBlockEntityIdent(String),
|
||||
#[error("invalid block entity position")]
|
||||
InvalidBlockEntityPosition,
|
||||
}
|
||||
|
||||
/// Takes an Anvil chunk in NBT form and writes its data to a Valence [`Chunk`].
|
||||
|
@ -254,6 +262,48 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let Some(Value::List(block_entities)) = nbt.get("block_entities") else {
|
||||
return Err(ToValenceError::MissingBlockEntity);
|
||||
};
|
||||
|
||||
if let List::Compound(block_entities) = block_entities {
|
||||
for comp in block_entities {
|
||||
let Some(Value::String(ident)) = comp.get("id") else {
|
||||
return Err(ToValenceError::MissingBlockEntityIdent);
|
||||
};
|
||||
let Ok(ident) = Ident::new(&ident[..]) else {
|
||||
return Err(ToValenceError::UnknownBlockEntityIdent(ident.clone()));
|
||||
};
|
||||
let Some(kind) = BlockEntityKind::from_ident(ident) else {
|
||||
return Err(ToValenceError::UnknownBlockEntityIdent(ident.as_str().to_string()));
|
||||
};
|
||||
let block_entity = BlockEntity {
|
||||
kind,
|
||||
nbt: comp.clone(),
|
||||
};
|
||||
let Some(Value::Int(x)) = comp.get("x") else {
|
||||
return Err(ToValenceError::InvalidBlockEntityPosition);
|
||||
};
|
||||
let Ok(x) = usize::try_from(x.mod_floor(&16)) else {
|
||||
return Err(ToValenceError::InvalidBlockEntityPosition);
|
||||
};
|
||||
let Some(Value::Int(y)) = comp.get("y") else {
|
||||
return Err(ToValenceError::InvalidBlockEntityPosition);
|
||||
};
|
||||
let Ok(y) = usize::try_from(y + sect_offset * 16) else {
|
||||
return Err(ToValenceError::InvalidBlockEntityPosition);
|
||||
};
|
||||
let Some(Value::Int(z)) = comp.get("z") else {
|
||||
return Err(ToValenceError::InvalidBlockEntityPosition);
|
||||
};
|
||||
let Ok(z) = usize::try_from(z.mod_floor(&16)) else {
|
||||
return Err(ToValenceError::InvalidBlockEntityPosition);
|
||||
};
|
||||
|
||||
chunk.set_block_entity(x, y, z, block_entity);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ edition = "2021"
|
|||
byteorder = "1.4.3"
|
||||
cesu8 = "1.1.0"
|
||||
indexmap = { version = "1.9.1", optional = true }
|
||||
uuid = { version = "1.1.2", optional = true }
|
||||
|
||||
[features]
|
||||
# When enabled, the order of fields in compounds are preserved.
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::tag::Tag;
|
||||
use crate::Compound;
|
||||
|
||||
|
@ -254,6 +257,20 @@ impl From<Vec<i64>> for Value {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
impl From<Uuid> for Value {
|
||||
fn from(value: Uuid) -> Self {
|
||||
let (most, least) = value.as_u64_pair();
|
||||
|
||||
let first = (most >> 32) as i32;
|
||||
let second = most as i32;
|
||||
let third = (least >> 32) as i32;
|
||||
let fourth = least as i32;
|
||||
|
||||
Value::IntArray(vec![first, second, third, fourth])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<i8>> for List {
|
||||
fn from(v: Vec<i8>) -> Self {
|
||||
List::Byte(v)
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::ident;
|
|||
struct TopLevel {
|
||||
blocks: Vec<Block>,
|
||||
shapes: Vec<Shape>,
|
||||
block_entity_types: Vec<BlockEntityKind>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
|
@ -34,6 +35,13 @@ impl Block {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct BlockEntityKind {
|
||||
id: u32,
|
||||
ident: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct Property {
|
||||
name: String,
|
||||
|
@ -47,6 +55,7 @@ struct State {
|
|||
opaque: bool,
|
||||
replaceable: bool,
|
||||
collision_shapes: Vec<u16>,
|
||||
block_entity_type: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
|
@ -60,8 +69,11 @@ struct Shape {
|
|||
}
|
||||
|
||||
pub fn build() -> anyhow::Result<TokenStream> {
|
||||
let TopLevel { blocks, shapes } =
|
||||
serde_json::from_str(include_str!("../../../extracted/blocks.json"))?;
|
||||
let TopLevel {
|
||||
blocks,
|
||||
shapes,
|
||||
block_entity_types,
|
||||
} = serde_json::from_str(include_str!("../../../extracted/blocks.json"))?;
|
||||
|
||||
let max_state_id = blocks.iter().map(|b| b.max_state_id()).max().unwrap();
|
||||
|
||||
|
@ -285,6 +297,19 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let state_to_block_entity_type_arms = blocks
|
||||
.iter()
|
||||
.flat_map(|b| {
|
||||
b.states.iter().filter_map(|s| {
|
||||
let id = s.id;
|
||||
let block_entity_type = s.block_entity_type?;
|
||||
Some(quote! {
|
||||
#id => Some(#block_entity_type),
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let kind_to_state_arms = blocks
|
||||
.iter()
|
||||
.map(|b| {
|
||||
|
@ -373,6 +398,69 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let block_entity_kind_variants = block_entity_types
|
||||
.iter()
|
||||
.map(|block_entity| {
|
||||
let name = ident(block_entity.name.to_pascal_case());
|
||||
let doc = format!(
|
||||
"The block entity type `{}` (ID {}).",
|
||||
block_entity.name, block_entity.id
|
||||
);
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#name,
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let block_entity_kind_from_id_arms = block_entity_types
|
||||
.iter()
|
||||
.map(|block_entity| {
|
||||
let id = block_entity.id;
|
||||
let name = ident(block_entity.name.to_pascal_case());
|
||||
|
||||
quote! {
|
||||
#id => Some(Self::#name),
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let block_entity_kind_to_id_arms = block_entity_types
|
||||
.iter()
|
||||
.map(|block_entity| {
|
||||
let id = block_entity.id;
|
||||
let name = ident(block_entity.name.to_pascal_case());
|
||||
|
||||
quote! {
|
||||
Self::#name => #id,
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
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;
|
||||
|
||||
quote! {
|
||||
#ident => Some(Self::#name),
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
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;
|
||||
|
||||
quote! {
|
||||
Self::#name => ident_str!(#ident),
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let block_kind_count = blocks.len();
|
||||
|
||||
let prop_names = blocks
|
||||
|
@ -578,6 +666,18 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn block_entity_kind(self) -> Option<BlockEntityKind> {
|
||||
let kind = match self.0 {
|
||||
#state_to_block_entity_type_arms
|
||||
_ => None
|
||||
};
|
||||
|
||||
match kind {
|
||||
Some(id) => BlockEntityKind::from_id(id),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#default_block_states
|
||||
}
|
||||
|
||||
|
@ -786,5 +886,51 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
Self::from_bool(b)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub enum BlockEntityKind {
|
||||
#block_entity_kind_variants
|
||||
}
|
||||
|
||||
impl BlockEntityKind {
|
||||
pub const fn from_id(num: u32) -> Option<Self> {
|
||||
match num {
|
||||
#block_entity_kind_from_id_arms
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn id(self) -> u32 {
|
||||
match self {
|
||||
#block_entity_kind_to_id_arms
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_ident(ident: Ident<&str>) -> Option<Self> {
|
||||
match ident.as_str() {
|
||||
#block_entity_kind_from_ident_arms
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ident(self) -> Ident<&'static str> {
|
||||
match self {
|
||||
#block_entity_kind_to_ident_arms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for BlockEntityKind {
|
||||
fn encode(&self, w: impl Write) -> Result<()> {
|
||||
VarInt(self.id() as i32).encode(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decode<'a> for BlockEntityKind {
|
||||
fn decode(r: &mut &'a [u8]) -> Result<Self> {
|
||||
let id = VarInt::decode(r)?;
|
||||
Self::from_id(id.0 as u32).with_context(|| format!("id {}", id.0))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ use std::io::Write;
|
|||
use std::iter::FusedIterator;
|
||||
|
||||
use anyhow::Context;
|
||||
use valence_nbt::Compound;
|
||||
use valence_protocol_macros::ident_str;
|
||||
|
||||
use crate::{Decode, Encode, ItemKind, Result, VarInt};
|
||||
use crate::{Decode, Encode, Ident, ItemKind, Result, VarInt};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/block.rs"));
|
||||
|
||||
|
@ -79,6 +81,18 @@ impl Decode<'_> for BlockKind {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BlockEntity {
|
||||
pub kind: BlockEntityKind,
|
||||
pub nbt: Compound,
|
||||
}
|
||||
|
||||
impl BlockEntity {
|
||||
pub const fn new(kind: BlockEntityKind, nbt: Compound) -> Self {
|
||||
Self { kind, nbt }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
pub enum BlockFace {
|
||||
/// -Y
|
||||
|
|
|
@ -40,6 +40,18 @@ pub struct Ident<S> {
|
|||
path_start: usize,
|
||||
}
|
||||
|
||||
impl<S> Ident<S> {
|
||||
/// Returns an Ident with the given fields
|
||||
///
|
||||
/// # Safety
|
||||
/// This function does not check for the validity of the Ident.
|
||||
/// For a safe version use [`Ident::new`]
|
||||
#[doc(hidden)]
|
||||
pub const fn new_unchecked(string: S, path_start: usize) -> Self {
|
||||
Self { string, path_start }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> Ident<S> {
|
||||
pub fn new(string: S) -> Result<Self, IdentError<S>> {
|
||||
let check_namespace = |s: &str| {
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::borrow::Cow;
|
|||
use uuid::Uuid;
|
||||
use valence_nbt::Compound;
|
||||
|
||||
use crate::block::BlockEntityKind;
|
||||
use crate::block_pos::BlockPos;
|
||||
use crate::byte_angle::ByteAngle;
|
||||
use crate::ident::Ident;
|
||||
|
@ -189,11 +190,10 @@ pub mod play {
|
|||
|
||||
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
|
||||
#[packet_id = 0x07]
|
||||
pub struct BlockEntityData {
|
||||
pub struct BlockEntityData<'a> {
|
||||
pub position: BlockPos,
|
||||
// TODO: BlockEntityKind enum?
|
||||
pub kind: VarInt,
|
||||
pub data: Compound,
|
||||
pub kind: BlockEntityKind,
|
||||
pub data: Cow<'a, Compound>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
|
||||
|
@ -404,7 +404,7 @@ pub mod play {
|
|||
pub chunk_z: i32,
|
||||
pub heightmaps: Compound,
|
||||
pub blocks_and_biomes: &'a [u8],
|
||||
pub block_entities: Vec<ChunkDataBlockEntity>,
|
||||
pub block_entities: Vec<ChunkDataBlockEntity<'a>>,
|
||||
pub trust_edges: bool,
|
||||
pub sky_light_mask: Vec<u64>,
|
||||
pub block_light_mask: Vec<u64>,
|
||||
|
@ -421,7 +421,7 @@ pub mod play {
|
|||
pub chunk_z: i32,
|
||||
pub heightmaps: &'a Compound,
|
||||
pub blocks_and_biomes: &'a [u8],
|
||||
pub block_entities: &'a [ChunkDataBlockEntity],
|
||||
pub block_entities: &'a [ChunkDataBlockEntity<'a>],
|
||||
pub trust_edges: bool,
|
||||
pub sky_light_mask: &'a [u64],
|
||||
pub block_light_mask: &'a [u64],
|
||||
|
@ -981,7 +981,7 @@ pub mod play {
|
|||
AwardStatistics,
|
||||
AcknowledgeBlockChange,
|
||||
SetBlockDestroyStage,
|
||||
BlockEntityData,
|
||||
BlockEntityData<'a>,
|
||||
BlockAction,
|
||||
BlockUpdate,
|
||||
BossBar,
|
||||
|
|
|
@ -8,6 +8,7 @@ use anyhow::Context;
|
|||
use serde::de::Visitor;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use uuid::Uuid;
|
||||
use valence_nbt::Value;
|
||||
|
||||
use crate::{Decode, Encode, Ident, Result};
|
||||
|
||||
|
@ -829,6 +830,15 @@ impl<'a> From<&'a Text> for Cow<'a, Text> {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Text> for Value {
|
||||
fn from(value: Text) -> Self {
|
||||
Value::String(
|
||||
serde_json::to_string(&value)
|
||||
.unwrap_or_else(|err| panic!("failed to jsonify text {value:?}\n{err}")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Text {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.write_string(f)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
//! Miscellaneous type definitions used in packets.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use bitfield_struct::bitfield;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use valence_nbt::Compound;
|
||||
|
||||
use crate::block::BlockEntityKind;
|
||||
use crate::{BlockPos, Decode, Encode, Ident, ItemStack, Text, VarInt};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Encode, Decode)]
|
||||
|
@ -238,12 +241,11 @@ pub enum GameEventKind {
|
|||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Encode, Decode)]
|
||||
pub struct ChunkDataBlockEntity {
|
||||
pub struct ChunkDataBlockEntity<'a> {
|
||||
pub packed_xz: i8,
|
||||
pub y: i16,
|
||||
// TODO: block entity kind?
|
||||
pub kind: VarInt,
|
||||
pub data: Compound,
|
||||
pub kind: BlockEntityKind,
|
||||
pub data: Cow<'a, Compound>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Encode, Decode)]
|
||||
|
|
41
crates/valence_protocol_macros/src/ident_str.rs
Normal file
41
crates/valence_protocol_macros/src/ident_str.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse2, LitStr, Result};
|
||||
|
||||
fn check_namespace(s: &str) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.chars()
|
||||
.all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '.' | '-'))
|
||||
}
|
||||
|
||||
fn check_path(s: &str) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.chars()
|
||||
.all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '.' | '-' | '/'))
|
||||
}
|
||||
|
||||
pub fn ident_str(item: TokenStream) -> Result<TokenStream> {
|
||||
let ident_lit: LitStr = parse2(item)?;
|
||||
let mut ident = &ident_lit.value()[..];
|
||||
|
||||
let path_start = match ident.split_once(':') {
|
||||
Some(("minecraft", path)) if check_path(path) => {
|
||||
ident = path;
|
||||
0
|
||||
}
|
||||
Some((namespace, path)) if check_namespace(namespace) && check_path(path) => {
|
||||
namespace.len() + 1
|
||||
}
|
||||
None if check_path(ident) => 0,
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
ident_lit.span(),
|
||||
"string cannot be parsed as ident",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
::valence_protocol::ident::Ident::new_unchecked(#ident, #path_start)
|
||||
})
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
//! This crate provides derive macros for [`Encode`], [`Decode`],
|
||||
//! [`EncodePacket`], and [`DecodePacket`].
|
||||
//! It also provides the procedural macro [`ident_str!`]
|
||||
//!
|
||||
//! See `valence_protocol`'s documentation for more information.
|
||||
|
||||
|
@ -13,6 +14,7 @@ use syn::{
|
|||
|
||||
mod decode;
|
||||
mod encode;
|
||||
mod ident_str;
|
||||
|
||||
#[proc_macro_derive(Encode, attributes(tag))]
|
||||
pub fn derive_encode(item: StdTokenStream) -> StdTokenStream {
|
||||
|
@ -46,6 +48,14 @@ pub fn derive_decode_packet(item: StdTokenStream) -> StdTokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn ident_str(item: StdTokenStream) -> StdTokenStream {
|
||||
match ident_str::ident_str(item.into()) {
|
||||
Ok(tokens) => tokens.into(),
|
||||
Err(e) => e.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_packet_id_attr(attrs: &[Attribute]) -> Result<Option<LitInt>> {
|
||||
for attr in attrs {
|
||||
if let Meta::NameValue(nv) = attr.parse_meta()? {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -76,6 +76,12 @@ public class Blocks implements Main.Extractor {
|
|||
|
||||
stateJson.add("collision_shapes", collisionShapeIdxsJson);
|
||||
|
||||
for (var blockEntity : Registries.BLOCK_ENTITY_TYPE) {
|
||||
if (blockEntity.supports(state)) {
|
||||
stateJson.addProperty("block_entity_type", Registries.BLOCK_ENTITY_TYPE.getRawId(blockEntity));
|
||||
}
|
||||
}
|
||||
|
||||
statesJson.add(stateJson);
|
||||
}
|
||||
blockJson.add("states", statesJson);
|
||||
|
@ -83,6 +89,16 @@ public class Blocks implements Main.Extractor {
|
|||
blocksJson.add(blockJson);
|
||||
}
|
||||
|
||||
var blockEntitiesJson = new JsonArray();
|
||||
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());
|
||||
|
||||
blockEntitiesJson.add(blockEntityJson);
|
||||
}
|
||||
|
||||
var shapesJson = new JsonArray();
|
||||
for (var shape : shapes.keySet()) {
|
||||
var shapeJson = new JsonObject();
|
||||
|
@ -95,6 +111,7 @@ public class Blocks implements Main.Extractor {
|
|||
shapesJson.add(shapeJson);
|
||||
}
|
||||
|
||||
topLevelJson.add("block_entity_types", blockEntitiesJson);
|
||||
topLevelJson.add("shapes", shapesJson);
|
||||
topLevelJson.add("blocks", blocksJson);
|
||||
|
||||
|
|
Loading…
Reference in a new issue