diff --git a/pumpkin-plugin-api/src/lib.rs b/pumpkin-plugin-api/src/lib.rs index 4ecb93706..522835791 100644 --- a/pumpkin-plugin-api/src/lib.rs +++ b/pumpkin-plugin-api/src/lib.rs @@ -42,7 +42,7 @@ pub mod command { pub use wit::pumpkin::plugin::{ command as command_wit, common, context::{Context, Server}, - permission, server, text, world, + gui, permission, scoreboard, server, text, world, }; pub mod logging; diff --git a/pumpkin-plugin-wit/v0.1.0/gui.wit b/pumpkin-plugin-wit/v0.1.0/gui.wit new file mode 100644 index 000000000..4f57442f3 --- /dev/null +++ b/pumpkin-plugin-wit/v0.1.0/gui.wit @@ -0,0 +1,38 @@ +interface gui { + use text.{text-component}; + use common.{item-stack}; + + enum gui-type { + generic-9x1, + generic-9x2, + generic-9x3, + generic-9x4, + generic-9x5, + generic-9x6, + generic-3x3, + crafter-3x3, + anvil, + beacon, + blast-furnace, + brewing-stand, + crafting, + enchantment, + furnace, + grindstone, + hopper, + lectern, + loom, + merchant, + shulker-box, + smithing, + smoker, + cartography-table, + stonecutter, + } + + resource gui { + constructor(%type: gui-type, title: text-component); + set-item: func(slot: u32, item: item-stack); + get-item: func(slot: u32) -> option; + } +} diff --git a/pumpkin-plugin-wit/v0.1.0/player.wit b/pumpkin-plugin-wit/v0.1.0/player.wit index 8e08d1fb1..1c1a4e092 100644 --- a/pumpkin-plugin-wit/v0.1.0/player.wit +++ b/pumpkin-plugin-wit/v0.1.0/player.wit @@ -3,6 +3,7 @@ interface player { use %world.{%world}; use permission.{permission-level}; use text.{text-component}; + use gui.{gui}; record player-abilities { invulnerable: bool, @@ -46,6 +47,8 @@ interface player { kick: func (message: text-component); respawn: func (); + open-gui: func (gui-ref: gui); + ban: func (reason: option); ban-ip: func (reason: option); @@ -90,4 +93,4 @@ interface player { get-ip: func() -> string; } -} \ No newline at end of file +} diff --git a/pumpkin-plugin-wit/v0.1.0/plugin.wit b/pumpkin-plugin-wit/v0.1.0/plugin.wit index 487cf32e2..a38781fe4 100644 --- a/pumpkin-plugin-wit/v0.1.0/plugin.wit +++ b/pumpkin-plugin-wit/v0.1.0/plugin.wit @@ -8,6 +8,8 @@ world plugin { // This is what the host should provide to the plugin import logging; + import gui; + import scoreboard; import server; import text; import command; diff --git a/pumpkin-plugin-wit/v0.1.0/scoreboard.wit b/pumpkin-plugin-wit/v0.1.0/scoreboard.wit new file mode 100644 index 000000000..a77f20769 --- /dev/null +++ b/pumpkin-plugin-wit/v0.1.0/scoreboard.wit @@ -0,0 +1,72 @@ +interface scoreboard { + use text.{text-component}; + use common.{named-color}; + + enum render-type { + integer, + hearts + } + + enum display-slot { + player-list, + sidebar, + below-name, + sidebar-team-black, + sidebar-team-dark-blue, + sidebar-team-dark-green, + sidebar-team-dark-aqua, + sidebar-team-dark-red, + sidebar-team-dark-purple, + sidebar-team-gold, + sidebar-team-gray, + sidebar-team-dark-gray, + sidebar-team-blue, + sidebar-team-green, + sidebar-team-aqua, + sidebar-team-red, + sidebar-team-light-purple, + sidebar-team-yellow, + sidebar-team-white, + } + + enum nametag-visibility { + always, + never, + hide-for-other-teams, + hide-for-own-team + } + + enum collision-rule { + always, + never, + push-other-teams, + push-own-team + } + + record team-settings { + display-name: text-component, + friendly-fire: bool, + see-friendly-invisibles: bool, + nametag-visibility: nametag-visibility, + collision-rule: collision-rule, + color: named-color, + prefix: text-component, + suffix: text-component + } + + resource scoreboard { + add-objective: func(name: string, display-name: text-component, render-type: render-type); + remove-objective: func(name: string); + set-display-slot: func(slot: display-slot, objective-name: string); + + update-score: func(entity-name: string, objective-name: string, value: s32); + remove-score: func(entity-name: string, objective-name: string); + + create-team: func(name: string, settings: team-settings); + remove-team: func(name: string); + update-team: func(name: string, settings: team-settings); + + add-player-to-team: func(team-name: string, player-name: string); + remove-player-from-team: func(team-name: string, player-name: string); + } +} diff --git a/pumpkin-plugin-wit/v0.1.0/world.wit b/pumpkin-plugin-wit/v0.1.0/world.wit index a111622f3..4fcc4a9d6 100644 --- a/pumpkin-plugin-wit/v0.1.0/world.wit +++ b/pumpkin-plugin-wit/v0.1.0/world.wit @@ -1,6 +1,7 @@ interface %world { use common.{position}; use text.{text-component}; + use scoreboard.{scoreboard}; enum piston-behavior { normal, @@ -34,6 +35,10 @@ interface %world { has-random-ticks: bool, } + record block-state-id { + id: u16, + } + record block-pos { x: s32, y: s32, @@ -70,5 +75,7 @@ interface %world { set-thundering: func(thundering: bool); broadcast-system-message: func(message: text-component, overlay: bool); + + get-scoreboard: func() -> scoreboard; } } diff --git a/pumpkin-protocol/src/java/client/play/mod.rs b/pumpkin-protocol/src/java/client/play/mod.rs index ad71e48e3..cf2e4185e 100644 --- a/pumpkin-protocol/src/java/client/play/mod.rs +++ b/pumpkin-protocol/src/java/client/play/mod.rs @@ -69,6 +69,7 @@ mod set_health; mod set_held_item; mod set_passengers; mod set_player_inventory; +mod set_player_team; mod set_time; mod set_title; mod set_title_animation; @@ -164,6 +165,7 @@ pub use set_health::*; pub use set_held_item::*; pub use set_passengers::*; pub use set_player_inventory::*; +pub use set_player_team::*; pub use set_time::*; pub use set_title::*; pub use set_title_animation::*; diff --git a/pumpkin-protocol/src/java/client/play/set_player_team.rs b/pumpkin-protocol/src/java/client/play/set_player_team.rs new file mode 100644 index 000000000..e76490bae --- /dev/null +++ b/pumpkin-protocol/src/java/client/play/set_player_team.rs @@ -0,0 +1,81 @@ +use std::io::Write; + +use pumpkin_data::packet::clientbound::PLAY_SET_PLAYER_TEAM; +use pumpkin_macros::java_packet; +use pumpkin_util::{text::TextComponent, version::MinecraftVersion}; + +use crate::{ + ClientPacket, + codec::var_int::VarInt, + ser::{NetworkWriteExt, WritingError}, +}; + +#[repr(i8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TeamMethod { + Create = 0, + Remove = 1, + Update = 2, + AddPlayers = 3, + RemovePlayers = 4, +} + +pub struct TeamParameters<'a> { + pub display_name: &'a TextComponent, + pub options: i8, + pub nametag_visibility: &'a str, + pub collision_rule: &'a str, + pub color: i32, + pub player_prefix: &'a TextComponent, + pub player_suffix: &'a TextComponent, +} + +#[java_packet(PLAY_SET_PLAYER_TEAM)] +pub struct CSetPlayerTeam<'a> { + pub team_name: String, + pub method: TeamMethod, + pub parameters: Option>, + pub players: Vec, +} + +impl ClientPacket for CSetPlayerTeam<'_> { + fn write_packet_data( + &self, + mut write: impl Write, + _version: &MinecraftVersion, + ) -> Result<(), WritingError> { + write.write_string(&self.team_name)?; + write.write_i8(self.method as i8)?; + + match self.method { + TeamMethod::Create | TeamMethod::Update => { + if let Some(params) = &self.parameters { + write.write_slice(¶ms.display_name.encode())?; + write.write_i8(params.options)?; + write.write_string(params.nametag_visibility)?; + write.write_string(params.collision_rule)?; + write.write_var_int(&VarInt(params.color))?; + write.write_slice(¶ms.player_prefix.encode())?; + write.write_slice(¶ms.player_suffix.encode())?; + } else { + return Err(WritingError::Message( + "Parameters missing for Create/Update".into(), + )); + } + } + _ => {} + } + + match self.method { + TeamMethod::Create | TeamMethod::AddPlayers | TeamMethod::RemovePlayers => { + write.write_var_int(&VarInt(self.players.len() as i32))?; + for player in &self.players { + write.write_string(player)?; + } + } + _ => {} + } + + Ok(()) + } +} diff --git a/pumpkin-protocol/src/java/client/play/update_objectives.rs b/pumpkin-protocol/src/java/client/play/update_objectives.rs index 2a6054858..bbcaf8e1a 100644 --- a/pumpkin-protocol/src/java/client/play/update_objectives.rs +++ b/pumpkin-protocol/src/java/client/play/update_objectives.rs @@ -74,6 +74,7 @@ pub enum Mode { Update, } +#[derive(Clone, Copy)] pub enum RenderType { Integer, Hearts, diff --git a/pumpkin-protocol/src/java/client/play/update_score.rs b/pumpkin-protocol/src/java/client/play/update_score.rs index f7f5b279c..a93fd79b3 100644 --- a/pumpkin-protocol/src/java/client/play/update_score.rs +++ b/pumpkin-protocol/src/java/client/play/update_score.rs @@ -45,4 +45,15 @@ impl CUpdateScore { number_format, } } + + #[must_use] + pub const fn new_remove(entity_name: String, objective_name: String) -> Self { + Self { + entity_name, + objective_name, + value: VarInt(0), + display_name: None, + number_format: None, + } + } } diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index a98ca43a5..e73947480 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -423,7 +423,7 @@ pub struct KnownPack<'a> { pub version: &'a str, } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub enum NumberFormat { /// Show nothing. Blank, diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 7586550c9..353cd0924 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -2902,6 +2902,40 @@ impl Player { } } + pub async fn open_handled_screen_direct( + &self, + screen_handler: Arc>, + title: TextComponent, + ) { + if !self + .current_screen_handler + .lock() + .await + .lock() + .await + .as_any() + .is::() + { + self.close_handled_screen().await; + } + + let screen_handler_temp = screen_handler.lock().await; + self.client + .enqueue_packet(&COpenScreen::new( + screen_handler_temp.sync_id().into(), + (screen_handler_temp + .window_type() + .expect("Can't open PlayerScreenHandler") as i32) + .into(), + &title, + )) + .await; + drop(screen_handler_temp); + self.on_screen_handler_opened(screen_handler.clone()).await; + *self.current_screen_handler.lock().await = screen_handler; + self.open_container_pos.store(None); + } + pub async fn on_slot_click(&self, packet: SClickSlot) { self.update_last_action_time(); let screen_handler = self.current_screen_handler.lock().await; diff --git a/pumpkin/src/plugin/api/gui.rs b/pumpkin/src/plugin/api/gui.rs new file mode 100644 index 000000000..fdfa8760f --- /dev/null +++ b/pumpkin/src/plugin/api/gui.rs @@ -0,0 +1,136 @@ +use std::any::Any; +use std::sync::Arc; +use tokio::sync::Mutex; + +use pumpkin_data::{item_stack::ItemStack, screen::WindowType}; +use pumpkin_inventory::screen_handler::{ + InventoryPlayer, ItemStackFuture, ScreenHandler, ScreenHandlerBehaviour, ScreenHandlerFuture, +}; +use pumpkin_inventory::slot::NormalSlot; +use pumpkin_util::text::TextComponent; +use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture}; + +pub struct PluginGui { + pub window_type: WindowType, + pub title: TextComponent, + pub inventory: Arc, +} + +pub struct PluginInventory { + pub slots: Vec>>, +} + +impl PluginInventory { + #[must_use] + pub fn new(size: usize) -> Self { + let mut slots = Vec::with_capacity(size); + for _ in 0..size { + slots.push(Arc::new(Mutex::new(ItemStack::EMPTY.clone()))); + } + Self { slots } + } +} + +impl Clearable for PluginInventory { + fn clear(&self) -> std::pin::Pin + Send + '_>> { + Box::pin(async move { + for slot in &self.slots { + *slot.lock().await = ItemStack::EMPTY.clone(); + } + }) + } +} + +impl Inventory for PluginInventory { + fn size(&self) -> usize { + self.slots.len() + } + + fn is_empty(&self) -> InventoryFuture<'_, bool> { + Box::pin(async move { + for slot in &self.slots { + if !slot.lock().await.is_empty() { + return false; + } + } + true + }) + } + + fn get_stack(&self, slot: usize) -> InventoryFuture<'_, Arc>> { + Box::pin(async move { self.slots[slot].clone() }) + } + + fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> { + Box::pin(async move { + let mut stack = self.slots[slot].lock().await; + std::mem::replace(&mut *stack, ItemStack::EMPTY.clone()) + }) + } + + fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> { + Box::pin(async move { + let mut stack = self.slots[slot].lock().await; + stack.split(amount) + }) + } + + fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> { + Box::pin(async move { + *self.slots[slot].lock().await = stack; + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +pub struct PluginScreenHandler { + pub inventory: Arc, + behaviour: ScreenHandlerBehaviour, +} + +impl PluginScreenHandler { + #[must_use] + pub fn new(sync_id: u8, window_type: WindowType, inventory: &Arc) -> Self { + let mut handler = Self { + inventory: inventory.clone(), + behaviour: ScreenHandlerBehaviour::new(sync_id, Some(window_type)), + }; + + for i in 0..inventory.size() { + handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), i))); + } + + handler + } +} + +impl ScreenHandler for PluginScreenHandler { + fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> { + Box::pin(async move { + self.default_on_closed(player).await; + }) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn get_behaviour(&self) -> &ScreenHandlerBehaviour { + &self.behaviour + } + + fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour { + &mut self.behaviour + } + + fn quick_move<'a>( + &'a mut self, + _player: &'a dyn InventoryPlayer, + _slot_index: i32, + ) -> ItemStackFuture<'a> { + Box::pin(async move { ItemStack::EMPTY.clone() }) + } +} diff --git a/pumpkin/src/plugin/api/mod.rs b/pumpkin/src/plugin/api/mod.rs index 503657af8..114ddd5f5 100644 --- a/pumpkin/src/plugin/api/mod.rs +++ b/pumpkin/src/plugin/api/mod.rs @@ -1,5 +1,6 @@ pub mod context; pub mod events; +pub mod gui; use std::{pin::Pin, sync::Arc}; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs index 86a85570a..eea51751b 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs @@ -4,6 +4,7 @@ use std::{ }; use pumpkin_util::text::TextComponent; +use tokio::sync::Mutex; use wasmtime::component::ResourceTable; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; @@ -16,6 +17,7 @@ use crate::{ entity::player::Player, plugin::{ Context, + api::gui::PluginGui, loader::wasm::wasm_host::{WasmPlugin, args::OwnedArg}, }, server::Server, @@ -30,6 +32,8 @@ pub type ServerResource = WasmResource>; pub type ContextResource = WasmResource>; pub type PlayerResource = WasmResource>; pub type WorldResource = WasmResource>; +pub type ScoreboardResource = WasmResource>; +pub type GuiResource = WasmResource>>; pub type TextComponentResource = WasmResource; pub type CommandResource = WasmResource; pub type CommandSenderResource = WasmResource; @@ -95,6 +99,22 @@ impl PluginHostState { Ok(wasmtime::component::Resource::new_own(resource.rep())) } + pub fn add_scoreboard( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self.resource_table.push(ScoreboardResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + + pub fn add_gui( + &mut self, + provider: Arc>, + ) -> wasmtime::Result> { + let resource = self.resource_table.push(GuiResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + pub fn add_text_component( &mut self, provider: TextComponent, diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/gui.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/gui.rs new file mode 100644 index 000000000..70339b2d1 --- /dev/null +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/gui.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; +use tokio::sync::Mutex; +use wasmtime::component::Resource; + +use crate::plugin::api::gui::{PluginGui, PluginInventory}; +use crate::plugin::loader::wasm::wasm_host::{ + state::{GuiResource, PluginHostState}, + wit::v0_1_0::pumpkin::plugin::{ + common::ItemStack as WitItemStack, + gui::{self, Gui, GuiType}, + }, +}; +use pumpkin_data::item_stack::ItemStack; + +impl PluginHostState { + fn get_gui_res(&self, res: &Resource) -> wasmtime::Result<&GuiResource> { + self.resource_table + .get::(&Resource::new_own(res.rep())) + .map_err(wasmtime::Error::from) + } +} + +impl gui::Host for PluginHostState {} + +impl gui::HostGui for PluginHostState { + async fn new( + &mut self, + gui_type: GuiType, + title: Resource, + ) -> wasmtime::Result> { + let title = self.get_text_provider(&title)?; + let window_type = match gui_type { + GuiType::Generic9x1 => pumpkin_data::screen::WindowType::Generic9x1, + GuiType::Generic9x2 => pumpkin_data::screen::WindowType::Generic9x2, + GuiType::Generic9x3 => pumpkin_data::screen::WindowType::Generic9x3, + GuiType::Generic9x4 => pumpkin_data::screen::WindowType::Generic9x4, + GuiType::Generic9x5 => pumpkin_data::screen::WindowType::Generic9x5, + GuiType::Generic9x6 => pumpkin_data::screen::WindowType::Generic9x6, + GuiType::Generic3x3 => pumpkin_data::screen::WindowType::Generic3x3, + GuiType::Crafter3x3 => pumpkin_data::screen::WindowType::Crafter3x3, + GuiType::Anvil => pumpkin_data::screen::WindowType::Anvil, + GuiType::Beacon => pumpkin_data::screen::WindowType::Beacon, + GuiType::BlastFurnace => pumpkin_data::screen::WindowType::BlastFurnace, + GuiType::BrewingStand => pumpkin_data::screen::WindowType::BrewingStand, + GuiType::Crafting => pumpkin_data::screen::WindowType::Crafting, + GuiType::Enchantment => pumpkin_data::screen::WindowType::Enchantment, + GuiType::Furnace => pumpkin_data::screen::WindowType::Furnace, + GuiType::Grindstone => pumpkin_data::screen::WindowType::Grindstone, + GuiType::Hopper => pumpkin_data::screen::WindowType::Hopper, + GuiType::Lectern => pumpkin_data::screen::WindowType::Lectern, + GuiType::Loom => pumpkin_data::screen::WindowType::Loom, + GuiType::Merchant => pumpkin_data::screen::WindowType::Merchant, + GuiType::ShulkerBox => pumpkin_data::screen::WindowType::ShulkerBox, + GuiType::Smithing => pumpkin_data::screen::WindowType::Smithing, + GuiType::Smoker => pumpkin_data::screen::WindowType::Smoker, + GuiType::CartographyTable => pumpkin_data::screen::WindowType::CartographyTable, + GuiType::Stonecutter => pumpkin_data::screen::WindowType::Stonecutter, + }; + + let size = match window_type { + pumpkin_data::screen::WindowType::Generic9x2 => 18, + pumpkin_data::screen::WindowType::Generic9x4 => 36, + pumpkin_data::screen::WindowType::Generic9x5 => 45, + pumpkin_data::screen::WindowType::Generic9x6 => 54, + pumpkin_data::screen::WindowType::Generic3x3 => 9, + pumpkin_data::screen::WindowType::Generic9x1 + | pumpkin_data::screen::WindowType::Hopper => 5, + _ => 27, // Default + }; + + let gui = Arc::new(Mutex::new(PluginGui { + window_type, + title, + inventory: Arc::new(PluginInventory::new(size)), + })); + + self.add_gui(gui) + } + + async fn set_item( + &mut self, + res: Resource, + slot: u32, + item: WitItemStack, + ) -> wasmtime::Result<()> { + let gui = self.get_gui_res(&res)?.provider.lock().await; + if (slot as usize) < gui.inventory.slots.len() { + let item_id = item.registry_key; + let item_data = pumpkin_data::item::Item::from_registry_key(&item_id) + .ok_or_else(|| wasmtime::Error::msg("Invalid item"))?; + let item_stack = ItemStack::new(item.count, item_data); + *gui.inventory.slots[slot as usize].lock().await = item_stack; + } + Ok(()) + } + + async fn get_item( + &mut self, + res: Resource, + slot: u32, + ) -> wasmtime::Result> { + let gui = self.get_gui_res(&res)?.provider.lock().await; + if (slot as usize) < gui.inventory.slots.len() { + let stack = gui.inventory.slots[slot as usize].lock().await; + if stack.is_empty() { + Ok(None) + } else { + Ok(Some(WitItemStack { + registry_key: stack.get_item().registry_key.to_string(), + count: stack.item_count, + })) + } + } else { + Ok(None) + } + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table + .delete::(Resource::new_own(rep.rep())) + .map_err(wasmtime::Error::from)?; + Ok(()) + } +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs index 464d8b43d..e297ddf3f 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs @@ -11,10 +11,12 @@ pub mod common; pub mod context; pub mod entity; pub mod events; +pub mod gui; pub mod i18n; pub mod logging; pub mod permission; pub mod player; +pub mod scoreboard; pub mod server; pub mod text; pub mod world; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs index 3d4a7d468..f45e283a8 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs @@ -1,12 +1,17 @@ +use std::sync::Arc; use std::sync::atomic::Ordering; +use tokio::sync::Mutex; use wasmtime::component::Resource; +use crate::plugin::api::gui::PluginScreenHandler; use crate::{ entity::{EntityBase, player::TitleMode}, net::DisconnectReason, plugin::loader::wasm::wasm_host::{ DowncastResourceExt, - state::{PlayerResource, PluginHostState, TextComponentResource, WorldResource}, + state::{ + GuiResource, PlayerResource, PluginHostState, TextComponentResource, WorldResource, + }, wit::v0_1_0::{ events::{ from_wasm_game_mode, from_wasm_position, to_wasm_game_mode, to_wasm_position, @@ -333,6 +338,32 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(()) } + async fn open_gui( + &mut self, + player: Resource, + gui: Resource, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let gui_res = self + .resource_table + .get::(&Resource::new_own(gui.rep())) + .map_err(|_| wasmtime::Error::msg("invalid gui resource handle"))?; + let gui = gui_res.provider.lock().await; + + player.increment_screen_handler_sync_id(); + let sync_id = player.screen_handler_sync_id.load(Ordering::Relaxed); + let screen_handler = Arc::new(Mutex::new(PluginScreenHandler::new( + sync_id, + gui.window_type, + &gui.inventory, + ))); + + player + .open_handled_screen_direct(screen_handler, gui.title.clone()) + .await; + Ok(()) + } + async fn ban( &mut self, player: Resource, diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/scoreboard.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/scoreboard.rs new file mode 100644 index 000000000..478f371d9 --- /dev/null +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/scoreboard.rs @@ -0,0 +1,344 @@ +use wasmtime::component::Resource; + +use crate::plugin::loader::wasm::wasm_host::{ + state::{PluginHostState, ScoreboardResource}, + wit::v0_1_0::pumpkin::{ + self, + plugin::scoreboard::{ + self, CollisionRule, DisplaySlot, NametagVisibility, RenderType, TeamSettings, + }, + }, +}; +use crate::world::scoreboard::{ScoreboardObjective, ScoreboardScore, Team}; +use pumpkin_protocol::codec::var_int::VarInt; + +impl PluginHostState { + fn get_scoreboard_res( + &self, + res: &Resource, + ) -> wasmtime::Result<&ScoreboardResource> { + self.resource_table + .get::(&Resource::new_own(res.rep())) + .map_err(wasmtime::Error::from) + } +} + +impl scoreboard::Host for PluginHostState {} + +impl scoreboard::HostScoreboard for PluginHostState { + async fn add_objective( + &mut self, + res: Resource, + name: String, + display_name: Resource, + render_type: RenderType, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + let display_name = self.get_text_provider(&display_name)?; + + let rt = match render_type { + RenderType::Integer => pumpkin_protocol::java::client::play::RenderType::Integer, + RenderType::Hearts => pumpkin_protocol::java::client::play::RenderType::Hearts, + }; + + let objective = + ScoreboardObjective::new(Box::leak(name.into_boxed_str()), display_name, rt, None); + world + .scoreboard + .lock() + .await + .add_objective(&world, objective) + .await; + Ok(()) + } + + async fn remove_objective( + &mut self, + res: Resource, + name: String, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + world + .scoreboard + .lock() + .await + .remove_objective(&world, &name) + .await; + Ok(()) + } + + async fn set_display_slot( + &mut self, + res: Resource, + slot: DisplaySlot, + objective_name: String, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + let slot = match slot { + DisplaySlot::PlayerList => pumpkin_data::scoreboard::ScoreboardDisplaySlot::List, + DisplaySlot::Sidebar => pumpkin_data::scoreboard::ScoreboardDisplaySlot::Sidebar, + DisplaySlot::BelowName => pumpkin_data::scoreboard::ScoreboardDisplaySlot::BelowName, + DisplaySlot::SidebarTeamBlack => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamBlack + } + DisplaySlot::SidebarTeamDarkBlue => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkBlue + } + DisplaySlot::SidebarTeamDarkGreen => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkGreen + } + DisplaySlot::SidebarTeamDarkAqua => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkAqua + } + DisplaySlot::SidebarTeamDarkRed => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkRed + } + DisplaySlot::SidebarTeamDarkPurple => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkPurple + } + DisplaySlot::SidebarTeamGold => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGold + } + DisplaySlot::SidebarTeamGray => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGray + } + DisplaySlot::SidebarTeamDarkGray => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkGray + } + DisplaySlot::SidebarTeamBlue => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamBlue + } + DisplaySlot::SidebarTeamGreen => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGreen + } + DisplaySlot::SidebarTeamAqua => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamAqua + } + DisplaySlot::SidebarTeamRed => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamRed, + DisplaySlot::SidebarTeamLightPurple => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamLightPurple + } + DisplaySlot::SidebarTeamYellow => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamYellow + } + DisplaySlot::SidebarTeamWhite => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamWhite + } + }; + + world + .broadcast_packet_all( + &pumpkin_protocol::java::client::play::CDisplayObjective::new(slot, objective_name), + ) + .await; + Ok(()) + } + + async fn update_score( + &mut self, + res: Resource, + entity_name: String, + objective_name: String, + value: i32, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + let score = ScoreboardScore::new( + Box::leak(entity_name.into_boxed_str()), + Box::leak(objective_name.into_boxed_str()), + VarInt(value), + None, + None, + ); + world + .scoreboard + .lock() + .await + .update_score(&world, score) + .await; + Ok(()) + } + + async fn remove_score( + &mut self, + res: Resource, + entity_name: String, + objective_name: String, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + world + .scoreboard + .lock() + .await + .remove_score(&world, &entity_name, &objective_name) + .await; + Ok(()) + } + + async fn create_team( + &mut self, + res: Resource, + name: String, + settings: TeamSettings, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + let team = map_team_settings(name, &settings, self)?; + world.scoreboard.lock().await.add_team(&world, team).await; + Ok(()) + } + + async fn remove_team( + &mut self, + res: Resource, + name: String, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + world + .scoreboard + .lock() + .await + .remove_team(&world, &name) + .await; + Ok(()) + } + + async fn update_team( + &mut self, + res: Resource, + name: String, + settings: TeamSettings, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + let team = map_team_settings(name, &settings, self)?; + world + .scoreboard + .lock() + .await + .update_team(&world, team) + .await; + Ok(()) + } + + async fn add_player_to_team( + &mut self, + res: Resource, + team_name: String, + player_name: String, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + world + .scoreboard + .lock() + .await + .add_player_to_team(&world, &team_name, player_name) + .await; + Ok(()) + } + + async fn remove_player_from_team( + &mut self, + res: Resource, + team_name: String, + player_name: String, + ) -> wasmtime::Result<()> { + let world = self.get_scoreboard_res(&res)?.provider.clone(); + world + .scoreboard + .lock() + .await + .remove_player_from_team(&world, &team_name, &player_name) + .await; + Ok(()) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table + .delete::(Resource::new_own(rep.rep())) + .map_err(wasmtime::Error::from)?; + Ok(()) + } +} + +fn map_team_settings( + name: String, + settings: &TeamSettings, + state: &PluginHostState, +) -> wasmtime::Result { + let display_name = state.get_text_provider(&settings.display_name)?; + let player_prefix = state.get_text_provider(&settings.prefix)?; + let player_suffix = state.get_text_provider(&settings.suffix)?; + + let mut options = 0; + if settings.friendly_fire { + options |= 0x01; + } + if settings.see_friendly_invisibles { + options |= 0x02; + } + + Ok(Team { + name, + display_name, + options, + nametag_visibility: match settings.nametag_visibility { + NametagVisibility::Always => crate::world::scoreboard::NameTagVisibility::Always, + NametagVisibility::Never => crate::world::scoreboard::NameTagVisibility::Never, + NametagVisibility::HideForOtherTeams => { + crate::world::scoreboard::NameTagVisibility::HideForOtherTeams + } + NametagVisibility::HideForOwnTeam => { + crate::world::scoreboard::NameTagVisibility::HideForOwnTeam + } + }, + collision_rule: match settings.collision_rule { + CollisionRule::Always => crate::world::scoreboard::CollisionRule::Always, + CollisionRule::Never => crate::world::scoreboard::CollisionRule::Never, + CollisionRule::PushOtherTeams => { + crate::world::scoreboard::CollisionRule::PushOtherTeams + } + CollisionRule::PushOwnTeam => crate::world::scoreboard::CollisionRule::PushOwnTeam, + }, + color: map_named_color(settings.color), + player_prefix, + player_suffix, + players: Vec::new(), + }) +} + +const fn map_named_color( + color: pumpkin::plugin::common::NamedColor, +) -> pumpkin_util::text::color::NamedColor { + match color { + pumpkin::plugin::common::NamedColor::Black => pumpkin_util::text::color::NamedColor::Black, + pumpkin::plugin::common::NamedColor::DarkBlue => { + pumpkin_util::text::color::NamedColor::DarkBlue + } + pumpkin::plugin::common::NamedColor::DarkGreen => { + pumpkin_util::text::color::NamedColor::DarkGreen + } + pumpkin::plugin::common::NamedColor::DarkAqua => { + pumpkin_util::text::color::NamedColor::DarkAqua + } + pumpkin::plugin::common::NamedColor::DarkRed => { + pumpkin_util::text::color::NamedColor::DarkRed + } + pumpkin::plugin::common::NamedColor::DarkPurple => { + pumpkin_util::text::color::NamedColor::DarkPurple + } + pumpkin::plugin::common::NamedColor::Gold => pumpkin_util::text::color::NamedColor::Gold, + pumpkin::plugin::common::NamedColor::Gray => pumpkin_util::text::color::NamedColor::Gray, + pumpkin::plugin::common::NamedColor::DarkGray => { + pumpkin_util::text::color::NamedColor::DarkGray + } + pumpkin::plugin::common::NamedColor::Blue => pumpkin_util::text::color::NamedColor::Blue, + pumpkin::plugin::common::NamedColor::Green => pumpkin_util::text::color::NamedColor::Green, + pumpkin::plugin::common::NamedColor::Aqua => pumpkin_util::text::color::NamedColor::Aqua, + pumpkin::plugin::common::NamedColor::Red => pumpkin_util::text::color::NamedColor::Red, + pumpkin::plugin::common::NamedColor::LightPurple => { + pumpkin_util::text::color::NamedColor::LightPurple + } + pumpkin::plugin::common::NamedColor::Yellow => { + pumpkin_util::text::color::NamedColor::Yellow + } + pumpkin::plugin::common::NamedColor::White => pumpkin_util::text::color::NamedColor::White, + } +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs index bf3ac8c61..f61284c9c 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs @@ -20,7 +20,7 @@ impl PluginHostState { .map_err(wasmtime::Error::from) } - fn get_text_provider( + pub(crate) fn get_text_provider( &self, res: &Resource, ) -> wasmtime::Result { @@ -220,6 +220,14 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { Ok(()) } + async fn get_scoreboard( + &mut self, + world: Resource, + ) -> wasmtime::Result> { + let world_provider = self.get_world_res(&world)?.provider.clone(); + self.add_scoreboard(world_provider) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { self.resource_table .delete::(Resource::new_own(rep.rep())) diff --git a/pumpkin/src/world/scoreboard.rs b/pumpkin/src/world/scoreboard.rs index 7c852826a..c44506cfc 100644 --- a/pumpkin/src/world/scoreboard.rs +++ b/pumpkin/src/world/scoreboard.rs @@ -4,9 +4,12 @@ use pumpkin_data::scoreboard::ScoreboardDisplaySlot; use pumpkin_protocol::{ NumberFormat, codec::var_int::VarInt, - java::client::play::{CDisplayObjective, CUpdateObjectives, CUpdateScore, RenderType}, + java::client::play::{ + CDisplayObjective, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, RenderType, TeamMethod, + TeamParameters, + }, }; -use pumpkin_util::text::TextComponent; +use pumpkin_util::text::{TextComponent, color::NamedColor}; use tracing::warn; use super::World; @@ -14,13 +17,13 @@ use super::World; #[derive(Default)] pub struct Scoreboard { objectives: HashMap>, - // teams: HashMap, + teams: HashMap, + scores: HashMap>>, } impl Scoreboard { - pub async fn add_objective(&mut self, world: &World, objective: ScoreboardObjective<'_>) { + pub async fn add_objective(&mut self, world: &World, objective: ScoreboardObjective<'static>) { if self.objectives.contains_key(objective.name) { - // Maybe make this an error? warn!( "Tried to create an objective which already exists: {}", &objective.name @@ -31,9 +34,9 @@ impl Scoreboard { .broadcast_packet_all(&CUpdateObjectives::new( objective.name.to_string(), pumpkin_protocol::java::client::play::Mode::Add, - objective.display_name, + objective.display_name.clone(), objective.render_type, - objective.number_format, + objective.number_format.clone(), )) .await; world @@ -42,10 +45,33 @@ impl Scoreboard { objective.name.to_string(), )) .await; + self.objectives + .insert(objective.name.to_string(), objective); } - pub async fn update_score(&self, world: &World, score: ScoreboardScore<'_>) { - if self.objectives.contains_key(score.objective_name) { + pub async fn remove_objective(&mut self, world: &World, name: &str) { + if !self.objectives.contains_key(name) { + warn!( + "Tried to remove an objective which does not exist: {}", + name + ); + return; + } + world + .broadcast_packet_all(&CUpdateObjectives::new( + name.to_string(), + pumpkin_protocol::java::client::play::Mode::Remove, + TextComponent::empty(), + pumpkin_protocol::java::client::play::RenderType::Integer, + None, + )) + .await; + self.objectives.remove(name); + self.scores.remove(name); + } + + pub async fn update_score(&mut self, world: &World, score: ScoreboardScore<'static>) { + if !self.objectives.contains_key(score.objective_name) { warn!( "Tried to place a score into an objective which does not exist: {}", &score.objective_name @@ -57,25 +83,163 @@ impl Scoreboard { score.entity_name.to_string(), score.objective_name.to_string(), score.value, - score.display_name, - score.number_format, + score.display_name.clone(), + score.number_format.clone(), )) .await; + + self.scores + .entry(score.objective_name.to_string()) + .or_default() + .insert(score.entity_name.to_string(), score); } - // pub fn add_team(&mut self, name: String) { - // if self.teams.contains_key(&name) { - // // Maybe make this an error ? - // log::warn!("Tried to create Team which does already exist, {}", name); - // } - // } + pub async fn remove_score(&mut self, world: &World, entity_name: &str, objective_name: &str) { + world + .broadcast_packet_all(&CUpdateScore::new_remove( + entity_name.to_string(), + objective_name.to_string(), + )) + .await; + + if let Some(objective_scores) = self.scores.get_mut(objective_name) { + objective_scores.remove(entity_name); + } + } + + pub async fn add_team(&mut self, world: &World, team: Team) { + if self.teams.contains_key(&team.name) { + warn!( + "Tried to create Team which does already exist, {}", + team.name + ); + return; + } + + let parameters = TeamParameters { + display_name: &team.display_name, + options: team.options, + nametag_visibility: team.nametag_visibility.to_str(), + collision_rule: team.collision_rule.to_str(), + color: team.color as i32, + player_prefix: &team.player_prefix, + player_suffix: &team.player_suffix, + }; + + world + .broadcast_packet_all(&CSetPlayerTeam { + team_name: team.name.clone(), + method: TeamMethod::Create, + parameters: Some(parameters), + players: team.players.clone(), + }) + .await; + + self.teams.insert(team.name.clone(), team); + } + + pub async fn update_team(&mut self, world: &World, team: Team) { + if !self.teams.contains_key(&team.name) { + warn!("Tried to update Team which does not exist, {}", team.name); + return; + } + + let parameters = TeamParameters { + display_name: &team.display_name, + options: team.options, + nametag_visibility: team.nametag_visibility.to_str(), + collision_rule: team.collision_rule.to_str(), + color: team.color as i32, + player_prefix: &team.player_prefix, + player_suffix: &team.player_suffix, + }; + + world + .broadcast_packet_all(&CSetPlayerTeam { + team_name: team.name.clone(), + method: TeamMethod::Update, + parameters: Some(parameters), + players: Vec::new(), + }) + .await; + + self.teams.insert(team.name.clone(), team); + } + + pub async fn remove_team(&mut self, world: &World, name: &str) { + if !self.teams.contains_key(name) { + warn!("Tried to remove Team which does not exist, {}", name); + return; + } + + world + .broadcast_packet_all(&CSetPlayerTeam { + team_name: name.to_string(), + method: TeamMethod::Remove, + parameters: None, + players: Vec::new(), + }) + .await; + + self.teams.remove(name); + } + + pub async fn add_player_to_team(&mut self, world: &World, team_name: &str, player: String) { + let Some(team) = self.teams.get_mut(team_name) else { + warn!( + "Tried to add player to Team which does not exist, {}", + team_name + ); + return; + }; + + if team.players.contains(&player) { + return; + } + + world + .broadcast_packet_all(&CSetPlayerTeam { + team_name: team_name.to_string(), + method: TeamMethod::AddPlayers, + parameters: None, + players: vec![player.clone()], + }) + .await; + + team.players.push(player); + } + + pub async fn remove_player_from_team(&mut self, world: &World, team_name: &str, player: &str) { + let Some(team) = self.teams.get_mut(team_name) else { + warn!( + "Tried to remove player from Team which does not exist, {}", + team_name + ); + return; + }; + + if !team.players.contains(&player.to_string()) { + return; + } + + world + .broadcast_packet_all(&CSetPlayerTeam { + team_name: team_name.to_string(), + method: TeamMethod::RemovePlayers, + parameters: None, + players: vec![player.to_string()], + }) + .await; + + team.players.retain(|p| p != player); + } } pub struct ScoreboardObjective<'a> { - name: &'a str, - display_name: TextComponent, - render_type: RenderType, - number_format: Option, + pub name: &'a str, + pub display_name: TextComponent, + pub render_type: RenderType, + pub number_format: Option, } impl<'a> ScoreboardObjective<'a> { @@ -96,11 +260,11 @@ impl<'a> ScoreboardObjective<'a> { } pub struct ScoreboardScore<'a> { - entity_name: &'a str, - objective_name: &'a str, - value: VarInt, - display_name: Option, - number_format: Option, + pub entity_name: &'a str, + pub objective_name: &'a str, + pub value: VarInt, + pub display_name: Option, + pub number_format: Option, } impl<'a> ScoreboardScore<'a> { @@ -121,3 +285,53 @@ impl<'a> ScoreboardScore<'a> { } } } + +pub enum NameTagVisibility { + Always, + Never, + HideForOtherTeams, + HideForOwnTeam, +} + +impl NameTagVisibility { + #[must_use] + pub const fn to_str(&self) -> &'static str { + match self { + Self::Always => "always", + Self::Never => "never", + Self::HideForOtherTeams => "hideForOtherTeams", + Self::HideForOwnTeam => "hideForOwnTeam", + } + } +} + +pub enum CollisionRule { + Always, + Never, + PushOtherTeams, + PushOwnTeam, +} + +impl CollisionRule { + #[must_use] + pub const fn to_str(&self) -> &'static str { + match self { + Self::Always => "always", + Self::Never => "never", + Self::PushOtherTeams => "pushOtherTeams", + Self::PushOwnTeam => "pushOwnTeam", + } + } +} + +pub struct Team { + pub name: String, + pub display_name: TextComponent, + pub options: i8, + pub nametag_visibility: NameTagVisibility, + pub collision_rule: CollisionRule, + pub color: NamedColor, + pub player_prefix: TextComponent, + pub player_suffix: TextComponent, + pub players: Vec, +}