Add: Interactive blocks (#333)

* WIP interactive blocks

* Added jukebox and crafting table

* Making feature more dynamic allowing every block action to be registered here

* Renamed interactive_block_manager to block_manager

* Simplifying register functions

* Added on placed

* Fix master merge

* Added pumpkin_block macro

* Removed junk

* Added the option to block placing in on_use_with_item

* Fixed inventory lock

* Adding on_broken and implementing it for the jukebox

* Fixed merge
This commit is contained in:
Nico Göbel
2024-12-07 04:00:03 -08:00
committed by GitHub
parent cd6ac147ec
commit b82a4e130b
17 changed files with 440 additions and 35 deletions

View File

@@ -42,6 +42,32 @@ pub fn server_packet(input: TokenStream, item: TokenStream) -> TokenStream {
gen.into()
}
#[proc_macro_attribute]
pub fn pumpkin_block(input: TokenStream, item: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(item.clone()).unwrap();
let name = &ast.ident;
let (impl_generics, ty_generics, _) = ast.generics.split_for_impl();
let input_string = input.to_string();
let packet_name = input_string.trim_matches('"');
let packet_name_split: Vec<&str> = packet_name.split(":").collect();
let namespace = packet_name_split[0];
let id = packet_name_split[1];
let item: proc_macro2::TokenStream = item.into();
let gen = quote! {
#item
impl #impl_generics crate::block::pumpkin_block::BlockMetadata for #name #ty_generics {
const NAMESPACE: &'static str = #namespace;
const ID: &'static str = #id;
}
};
gen.into()
}
mod screen;
#[proc_macro]
pub fn screen(item: TokenStream) -> TokenStream {

View File

@@ -0,0 +1,28 @@
use pumpkin_core::math::position::WorldPosition;
use pumpkin_macros::client_packet;
use serde::Serialize;
#[derive(Serialize)]
#[client_packet("play:level_event")]
pub struct CLevelEvent {
event: i32,
location: WorldPosition,
data: i32,
disable_relative_volume: bool,
}
impl CLevelEvent {
pub fn new(
event: i32,
location: WorldPosition,
data: i32,
disable_relative_volume: bool,
) -> Self {
Self {
event,
location,
data,
disable_relative_volume,
}
}
}

View File

@@ -25,6 +25,7 @@ mod c_head_rot;
mod c_hurt_animation;
mod c_initialize_world_border;
mod c_keep_alive;
mod c_level_event;
mod c_login;
mod c_open_screen;
mod c_particle;
@@ -94,6 +95,7 @@ pub use c_head_rot::*;
pub use c_hurt_animation::*;
pub use c_initialize_world_border::*;
pub use c_keep_alive::*;
pub use c_level_event::*;
pub use c_login::*;
pub use c_open_screen::*;
pub use c_particle::*;

View File

@@ -3,7 +3,12 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JukeboxSong {
sound_event: String,
// description: TextComponent<'static>,
description: Description,
length_in_seconds: f32,
comparator_output: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Description {
translate: String,
}

View File

@@ -58,7 +58,7 @@ pub struct SyncedRegistry {
damage_type: IndexMap<String, DamageType>,
banner_pattern: IndexMap<String, BannerPattern>,
enchantment: IndexMap<String, Enchantment>,
jukebox_song: IndexMap<String, JukeboxSong>,
pub jukebox_song: IndexMap<String, JukeboxSong>,
instrument: IndexMap<String, Instrument>,
}
@@ -225,18 +225,18 @@ impl Registry {
// registry_entries,
// };
// let registry_entries = SYNCED_REGISTRIES
// .jukebox_song
// .iter()
// .map(|s| RegistryEntry {
// entry_id: s.0,
// data: pumpkin_nbt::serializer::to_bytes_unnamed(&s.1).unwrap(),
// })
// .collect();
// let jukebox_song = Registry {
// registry_id: "minecraft:jukebox_song".to_string(),
// registry_entries,
// };
let registry_entries = SYNCED_REGISTRIES
.jukebox_song
.iter()
.map(|s| RegistryEntry {
entry_id: s.0,
data: pumpkin_nbt::serializer::to_bytes_unnamed(&s.1).unwrap(),
})
.collect();
let jukebox_song = Registry {
registry_id: "minecraft:jukebox_song".to_string(),
registry_entries,
};
// let registry_entries = SYNCED_REGISTRIES
// .instrument
@@ -262,7 +262,7 @@ impl Registry {
damage_type,
banner_pattern,
// enchantment,
// jukebox_song,
jukebox_song,
// instrument,
]
}

View File

@@ -1,4 +1,5 @@
use std::{collections::HashMap, sync::LazyLock};
use std::collections::HashMap;
use std::sync::LazyLock;
use serde::Deserialize;
@@ -12,6 +13,14 @@ pub fn get_item(name: &str) -> Option<&Item> {
ITEMS.get(&name.replace("minecraft:", ""))
}
pub fn get_item_by_id<'a>(id: u16) -> Option<&'a Item> {
let item = ITEMS.iter().find(|item| item.1.id == id);
if let Some(item) = item {
return Some(item.1);
}
None
}
#[derive(Deserialize, Clone, Debug)]
pub struct Item {
pub id: u16,
@@ -22,4 +31,11 @@ pub struct Item {
pub struct ItemComponents {
#[serde(rename = "minecraft:max_stack_size")]
pub max_stack_size: u8,
#[serde(rename = "minecraft:jukebox_playable")]
pub jukebox_playable: Option<JukeboxPlayable>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct JukeboxPlayable {
pub song: String,
}

View File

@@ -0,0 +1,89 @@
use crate::block::pumpkin_block::{BlockMetadata, PumpkinBlock};
use crate::entity::player::Player;
use crate::server::Server;
use pumpkin_core::math::position::WorldPosition;
use pumpkin_world::block::block_registry::Block;
use pumpkin_world::item::item_registry::Item;
use std::collections::HashMap;
use std::sync::Arc;
pub enum BlockActionResult {
/// Allow other actions to be executed
Continue,
/// Block other actions
Consume,
}
#[derive(Default)]
pub struct BlockManager {
blocks: HashMap<String, Arc<dyn PumpkinBlock>>,
}
impl BlockManager {
pub fn register<T: PumpkinBlock + BlockMetadata + 'static>(&mut self, block: T) {
self.blocks
.insert(block.name().to_string(), Arc::new(block));
}
pub async fn on_use(
&self,
block: &Block,
player: &Player,
location: WorldPosition,
server: &Server,
) {
let pumpkin_block = self.get_pumpkin_block(block);
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block.on_use(player, location, server).await;
}
}
pub async fn on_use_with_item(
&self,
block: &Block,
player: &Player,
location: WorldPosition,
item: &Item,
server: &Server,
) -> BlockActionResult {
let pumpkin_block = self.get_pumpkin_block(block);
if let Some(pumpkin_block) = pumpkin_block {
return pumpkin_block
.on_use_with_item(player, location, item, server)
.await;
}
BlockActionResult::Continue
}
pub async fn on_placed(
&self,
block: &Block,
player: &Player,
location: WorldPosition,
server: &Server,
) {
let pumpkin_block = self.get_pumpkin_block(block);
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block.on_placed(player, location, server).await;
}
}
pub async fn on_broken(
&self,
block: &Block,
player: &Player,
location: WorldPosition,
server: &Server,
) {
let pumpkin_block = self.get_pumpkin_block(block);
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block.on_broken(player, location, server).await;
}
}
#[must_use]
pub fn get_pumpkin_block(&self, block: &Block) -> Option<&Arc<dyn PumpkinBlock>> {
self.blocks
.get(format!("minecraft:{}", block.name).as_str())
}
}

View File

@@ -0,0 +1,50 @@
use crate::block::block_manager::BlockActionResult;
use crate::block::pumpkin_block::PumpkinBlock;
use crate::entity::player::Player;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_core::math::position::WorldPosition;
use pumpkin_inventory::{CraftingTable, OpenContainer, WindowType};
use pumpkin_macros::pumpkin_block;
use pumpkin_world::item::item_registry::Item;
#[pumpkin_block("minecraft:crafting_table")]
pub struct CraftingTableBlock;
#[async_trait]
impl PumpkinBlock for CraftingTableBlock {
async fn on_use<'a>(&self, player: &Player, _location: WorldPosition, server: &Server) {
self.open_crafting_screen(player, server).await;
}
async fn on_use_with_item<'a>(
&self,
player: &Player,
_location: WorldPosition,
_item: &Item,
server: &Server,
) -> BlockActionResult {
self.open_crafting_screen(player, server).await;
BlockActionResult::Consume
}
}
impl CraftingTableBlock {
pub async fn open_crafting_screen(&self, player: &Player, server: &Server) {
//TODO: Adjust /craft command to real crafting table
let entity_id = player.entity_id();
player.open_container.store(Some(1));
{
let mut open_containers = server.open_containers.write().await;
if let Some(ender_chest) = open_containers.get_mut(&1) {
ender_chest.add_player(entity_id);
} else {
let open_container = OpenContainer::new_empty_container::<CraftingTable>(entity_id);
open_containers.insert(1, open_container);
}
}
player
.open_container(server, WindowType::CraftingTable)
.await;
}
}

View File

@@ -0,0 +1,58 @@
use crate::block::block_manager::BlockActionResult;
use crate::block::pumpkin_block::PumpkinBlock;
use crate::entity::player::Player;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_core::math::position::WorldPosition;
use pumpkin_macros::pumpkin_block;
use pumpkin_registry::SYNCED_REGISTRIES;
use pumpkin_world::item::item_registry::Item;
#[pumpkin_block("minecraft:jukebox")]
pub struct JukeboxBlock;
#[async_trait]
impl PumpkinBlock for JukeboxBlock {
async fn on_use<'a>(&self, player: &Player, location: WorldPosition, _server: &Server) {
// For now just stop the music at this position
let world = &player.living_entity.entity.world;
world.stop_record(location).await;
}
async fn on_use_with_item<'a>(
&self,
player: &Player,
location: WorldPosition,
item: &Item,
_server: &Server,
) -> BlockActionResult {
let world = &player.living_entity.entity.world;
let Some(jukebox_playable) = &item.components.jukebox_playable else {
return BlockActionResult::Continue;
};
let Some(song) = jukebox_playable.song.split(':').nth(1) else {
return BlockActionResult::Continue;
};
let Some(jukebox_song) = SYNCED_REGISTRIES.jukebox_song.get_index_of(song) else {
log::error!("Jukebox playable song not registered!");
return BlockActionResult::Continue;
};
//TODO: Update block state and block nbt
world.play_record(jukebox_song as i32, location).await;
BlockActionResult::Consume
}
async fn on_broken<'a>(&self, player: &Player, location: WorldPosition, _server: &Server) {
// For now just stop the music at this position
let world = &player.living_entity.entity.world;
world.stop_record(location).await;
}
}

View File

@@ -0,0 +1,2 @@
pub(crate) mod crafting_table;
pub(crate) mod jukebox;

18
pumpkin/src/block/mod.rs Normal file
View File

@@ -0,0 +1,18 @@
use crate::block::block_manager::BlockManager;
use crate::block::blocks::crafting_table::CraftingTableBlock;
use crate::block::blocks::jukebox::JukeboxBlock;
use std::sync::Arc;
pub mod block_manager;
mod blocks;
pub mod pumpkin_block;
#[must_use]
pub fn default_block_manager() -> Arc<BlockManager> {
let mut manager = BlockManager::default();
manager.register(JukeboxBlock);
manager.register(CraftingTableBlock);
Arc::new(manager)
}

View File

@@ -0,0 +1,32 @@
use crate::block::block_manager::BlockActionResult;
use crate::entity::player::Player;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_core::math::position::WorldPosition;
use pumpkin_world::item::item_registry::Item;
pub trait BlockMetadata {
const NAMESPACE: &'static str;
const ID: &'static str;
fn name(&self) -> String {
format!("{}:{}", Self::NAMESPACE, Self::ID)
}
}
#[async_trait]
pub trait PumpkinBlock: Send + Sync {
async fn on_use<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {}
async fn on_use_with_item<'a>(
&self,
_player: &Player,
_location: WorldPosition,
_item: &Item,
_server: &Server,
) -> BlockActionResult {
BlockActionResult::Continue
}
async fn on_placed<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {}
async fn on_broken<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {}
}

View File

@@ -1,5 +1,7 @@
use std::sync::Arc;
use super::PlayerConfig;
use crate::block::block_manager::BlockActionResult;
use crate::{
command::CommandSender,
entity::player::{ChatMode, Hand, Player},
@@ -35,10 +37,9 @@ use pumpkin_protocol::{
},
};
use pumpkin_world::block::{block_registry::get_block_by_item, BlockFace};
use pumpkin_world::item::item_registry::get_item_by_id;
use thiserror::Error;
use super::PlayerConfig;
fn modulus(a: f32, b: f32) -> f32 {
((a % b) + b) % b
}
@@ -48,6 +49,7 @@ pub enum BlockPlacingError {
BlockOutOfReach,
InvalidBlockFace,
BlockOutOfWorld,
InventoryInvalid,
}
impl std::fmt::Display for BlockPlacingError {
@@ -60,7 +62,7 @@ impl PumpkinError for BlockPlacingError {
fn is_kick(&self) -> bool {
match self {
Self::BlockOutOfReach | Self::BlockOutOfWorld => false,
Self::InvalidBlockFace => true,
Self::InvalidBlockFace | Self::InventoryInvalid => true,
}
}
@@ -69,6 +71,7 @@ impl PumpkinError for BlockPlacingError {
Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidBlockFace => {
log::Level::Warn
}
Self::InventoryInvalid => log::Level::Error,
}
}
@@ -76,6 +79,7 @@ impl PumpkinError for BlockPlacingError {
match self {
Self::BlockOutOfReach | Self::BlockOutOfWorld => None,
Self::InvalidBlockFace => Some("Invalid block face".into()),
Self::InventoryInvalid => Some("Held item invalid".into()),
}
}
}
@@ -523,7 +527,7 @@ impl Player {
}
}
pub async fn handle_player_action(&self, player_action: SPlayerAction) {
pub async fn handle_player_action(&self, player_action: SPlayerAction, server: &Server) {
match Status::from_i32(player_action.status.0) {
Some(status) => match status {
Status::StartedDigging => {
@@ -542,7 +546,16 @@ impl Player {
// Block break & block break sound
let entity = &self.living_entity.entity;
let world = &entity.world;
let block = world.get_block(location).await;
world.break_block(location, Some(self)).await;
if let Ok(block) = block {
server
.block_manager
.on_broken(block, self, location, server)
.await;
}
}
}
Status::CancelledDigging => {
@@ -571,7 +584,16 @@ impl Player {
// Block break & block break sound
let entity = &self.living_entity.entity;
let world = &entity.world;
let block = world.get_block(location).await;
world.break_block(location, Some(self)).await;
if let Ok(block) = block {
server
.block_manager
.on_broken(block, self, location, server)
.await;
}
// TODO: Send this every tick
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
@@ -624,6 +646,7 @@ impl Player {
pub async fn handle_use_item_on(
&self,
use_item_on: SUseItemOn,
server: &Server,
) -> Result<(), Box<dyn PumpkinError>> {
let location = use_item_on.location;
@@ -633,22 +656,46 @@ impl Player {
}
if let Some(face) = BlockFace::from_i32(use_item_on.face.0) {
let mut inventory = self.inventory().lock().await;
let item_slot = inventory.held_item_mut();
if let Some(item) = item_slot {
let block = get_block_by_item(item.item_id);
// check if item is a block, Because Not every item can be placed :D
if let Some(block) = block {
let entity = &self.living_entity.entity;
let world = &entity.world;
let inventory = self.inventory().lock().await;
let entity = &self.living_entity.entity;
let world = &entity.world;
let item_slot = inventory.held_item();
if let Some(item_stack) = item_slot {
let item_stack = *item_stack;
drop(inventory);
if let Some(item) = get_item_by_id(item_stack.item_id) {
if let Ok(block) = world.get_block(location).await {
let result = server
.block_manager
.on_use_with_item(block, self, location, item, server)
.await;
match result {
BlockActionResult::Continue => {}
BlockActionResult::Consume => {
return Ok(());
}
}
}
}
// check if item is a block, Because Not every item can be placed :D
if let Some(block) = get_block_by_item(item_stack.item_id) {
// TODO: Config
// Decrease Block count
if self.gamemode.load() != GameMode::Creative {
item.item_count -= 1;
if item.item_count == 0 {
let mut inventory = self.inventory().lock().await;
let item_slot = inventory.held_item_mut();
// This should never be possible
let Some(item_stack) = item_slot else {
return Err(BlockPlacingError::InventoryInvalid.into());
};
item_stack.item_count -= 1;
if item_stack.item_count == 0 {
*item_slot = None;
}
drop(inventory);
}
let clicked_world_pos = WorldPosition(location.0);
@@ -682,11 +729,25 @@ impl Player {
world
.set_block_state(world_pos, block.default_state_id)
.await;
server
.block_manager
.on_placed(block, self, world_pos, server)
.await;
}
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))
.await;
}
} else {
drop(inventory);
let block = world.get_block(location).await;
if let Ok(block) = block {
server
.block_manager
.on_use(block, self, location, server)
.await;
}
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))
.await;
}
Ok(())

View File

@@ -657,7 +657,7 @@ impl Player {
.await;
}
SPlayerAction::PACKET_ID => {
self.handle_player_action(SPlayerAction::read(bytebuf)?)
self.handle_player_action(SPlayerAction::read(bytebuf)?, server)
.await;
}
SPlayerCommand::PACKET_ID => {
@@ -684,7 +684,8 @@ impl Player {
self.handle_swing_arm(SSwingArm::read(bytebuf)?).await;
}
SUseItemOn::PACKET_ID => {
self.handle_use_item_on(SUseItemOn::read(bytebuf)?).await?;
self.handle_use_item_on(SUseItemOn::read(bytebuf)?, server)
.await?;
}
SUseItem::PACKET_ID => self.handle_use_item(&SUseItem::read(bytebuf)?),
SCommandSuggestion::PACKET_ID => {

View File

@@ -38,6 +38,7 @@ use rcon::RCONServer;
use std::time::Instant;
// Setup some tokens to allow us to identify which event is for which socket.
pub mod block;
pub mod client;
pub mod command;
pub mod entity;

View File

@@ -21,6 +21,8 @@ use std::{
};
use tokio::sync::{Mutex, RwLock};
use crate::block::block_manager::BlockManager;
use crate::block::default_block_manager;
use crate::client::EncryptionError;
use crate::world::custom_bossbar::CustomBossbars;
use crate::{
@@ -46,6 +48,8 @@ pub struct Server {
server_branding: CachedBranding,
/// Saves and Dispatches commands to appropriate handlers.
pub command_dispatcher: Arc<CommandDispatcher<'static>>,
/// Saves and calls blocks blocks
pub block_manager: Arc<BlockManager>,
/// Manages multiple worlds within the server.
pub worlds: Vec<Arc<World>>,
// All the dimensions that exists on the server,
@@ -112,6 +116,7 @@ impl Server {
DimensionType::TheEnd,
],
command_dispatcher,
block_manager: default_block_manager(),
auth_client,
key_store: KeyStore::new(),
server_listing: Mutex::new(CachedStatus::new()),

View File

@@ -16,6 +16,7 @@ use pumpkin_core::math::vector2::Vector2;
use pumpkin_core::math::{position::WorldPosition, vector3::Vector3};
use pumpkin_core::text::{color::NamedColor, TextComponent};
use pumpkin_entity::{entity_type::EntityType, EntityId};
use pumpkin_protocol::client::play::CLevelEvent;
use pumpkin_protocol::{
client::play::{CBlockUpdate, CRespawn, CSoundEffect, CWorldEvent},
SoundCategory,
@@ -166,6 +167,16 @@ impl World {
.await;
}
pub async fn play_record(&self, record_id: i32, position: WorldPosition) {
self.broadcast_packet_all(&CLevelEvent::new(1010, position, record_id, false))
.await;
}
pub async fn stop_record(&self, position: WorldPosition) {
self.broadcast_packet_all(&CLevelEvent::new(1011, position, 0, false))
.await;
}
pub async fn tick(&self) {
// world ticks
let mut level_time = self.level_time.lock().await;