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:
Mrln 2023-02-18 19:16:01 +01:00 committed by GitHub
parent bcd686990d
commit 1ceafe0ce0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 6626 additions and 2782 deletions

View file

@ -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);
}
}

View file

@ -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"

View file

@ -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);
}
}

View 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(),
}])
}
}
};
}
}
}

View file

@ -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());
}
}

View file

@ -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,
);

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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! {}
})
);
}
}
}
}
}

View file

@ -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};

View file

@ -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(())
}

View file

@ -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.

View file

@ -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)

View file

@ -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))
}
}
})
}

View file

@ -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

View file

@ -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| {

View file

@ -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,

View file

@ -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)

View file

@ -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)]

View 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)
})
}

View file

@ -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

View file

@ -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);