feat: more plugin api features

This commit is contained in:
Alexander Medvedev 2026-04-07 16:12:46 +02:00
parent bc7e59df39
commit 413f7ccb16
21 changed files with 1162 additions and 31 deletions

View File

@ -42,7 +42,7 @@ pub mod command {
pub use wit::pumpkin::plugin::{ pub use wit::pumpkin::plugin::{
command as command_wit, common, command as command_wit, common,
context::{Context, Server}, context::{Context, Server},
permission, server, text, world, gui, permission, scoreboard, server, text, world,
}; };
pub mod logging; pub mod logging;

View File

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

View File

@ -3,6 +3,7 @@ interface player {
use %world.{%world}; use %world.{%world};
use permission.{permission-level}; use permission.{permission-level};
use text.{text-component}; use text.{text-component};
use gui.{gui};
record player-abilities { record player-abilities {
invulnerable: bool, invulnerable: bool,
@ -46,6 +47,8 @@ interface player {
kick: func (message: text-component); kick: func (message: text-component);
respawn: func (); respawn: func ();
open-gui: func (gui-ref: gui);
ban: func (reason: option<text-component>); ban: func (reason: option<text-component>);
ban-ip: func (reason: option<text-component>); ban-ip: func (reason: option<text-component>);

View File

@ -8,6 +8,8 @@ world plugin {
// This is what the host should provide to the plugin // This is what the host should provide to the plugin
import logging; import logging;
import gui;
import scoreboard;
import server; import server;
import text; import text;
import command; import command;

View File

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

View File

@ -1,6 +1,7 @@
interface %world { interface %world {
use common.{position}; use common.{position};
use text.{text-component}; use text.{text-component};
use scoreboard.{scoreboard};
enum piston-behavior { enum piston-behavior {
normal, normal,
@ -34,6 +35,10 @@ interface %world {
has-random-ticks: bool, has-random-ticks: bool,
} }
record block-state-id {
id: u16,
}
record block-pos { record block-pos {
x: s32, x: s32,
y: s32, y: s32,
@ -70,5 +75,7 @@ interface %world {
set-thundering: func(thundering: bool); set-thundering: func(thundering: bool);
broadcast-system-message: func(message: text-component, overlay: bool); broadcast-system-message: func(message: text-component, overlay: bool);
get-scoreboard: func() -> scoreboard;
} }
} }

View File

@ -69,6 +69,7 @@ mod set_health;
mod set_held_item; mod set_held_item;
mod set_passengers; mod set_passengers;
mod set_player_inventory; mod set_player_inventory;
mod set_player_team;
mod set_time; mod set_time;
mod set_title; mod set_title;
mod set_title_animation; mod set_title_animation;
@ -164,6 +165,7 @@ pub use set_health::*;
pub use set_held_item::*; pub use set_held_item::*;
pub use set_passengers::*; pub use set_passengers::*;
pub use set_player_inventory::*; pub use set_player_inventory::*;
pub use set_player_team::*;
pub use set_time::*; pub use set_time::*;
pub use set_title::*; pub use set_title::*;
pub use set_title_animation::*; pub use set_title_animation::*;

View File

@ -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<TeamParameters<'a>>,
pub players: Vec<String>,
}
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(&params.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(&params.player_prefix.encode())?;
write.write_slice(&params.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(())
}
}

View File

@ -74,6 +74,7 @@ pub enum Mode {
Update, Update,
} }
#[derive(Clone, Copy)]
pub enum RenderType { pub enum RenderType {
Integer, Integer,
Hearts, Hearts,

View File

@ -45,4 +45,15 @@ impl CUpdateScore {
number_format, 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,
}
}
} }

View File

@ -423,7 +423,7 @@ pub struct KnownPack<'a> {
pub version: &'a str, pub version: &'a str,
} }
#[derive(Serialize)] #[derive(Serialize, Clone)]
pub enum NumberFormat { pub enum NumberFormat {
/// Show nothing. /// Show nothing.
Blank, Blank,

View File

@ -2902,6 +2902,40 @@ impl Player {
} }
} }
pub async fn open_handled_screen_direct(
&self,
screen_handler: Arc<Mutex<dyn ScreenHandler>>,
title: TextComponent,
) {
if !self
.current_screen_handler
.lock()
.await
.lock()
.await
.as_any()
.is::<PlayerScreenHandler>()
{
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) { pub async fn on_slot_click(&self, packet: SClickSlot) {
self.update_last_action_time(); self.update_last_action_time();
let screen_handler = self.current_screen_handler.lock().await; let screen_handler = self.current_screen_handler.lock().await;

View File

@ -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<PluginInventory>,
}
pub struct PluginInventory {
pub slots: Vec<Arc<Mutex<ItemStack>>>,
}
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<Box<dyn std::future::Future<Output = ()> + 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<Mutex<ItemStack>>> {
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<PluginInventory>,
behaviour: ScreenHandlerBehaviour,
}
impl PluginScreenHandler {
#[must_use]
pub fn new(sync_id: u8, window_type: WindowType, inventory: &Arc<PluginInventory>) -> 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() })
}
}

View File

@ -1,5 +1,6 @@
pub mod context; pub mod context;
pub mod events; pub mod events;
pub mod gui;
use std::{pin::Pin, sync::Arc}; use std::{pin::Pin, sync::Arc};

View File

@ -4,6 +4,7 @@ use std::{
}; };
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use tokio::sync::Mutex;
use wasmtime::component::ResourceTable; use wasmtime::component::ResourceTable;
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
@ -16,6 +17,7 @@ use crate::{
entity::player::Player, entity::player::Player,
plugin::{ plugin::{
Context, Context,
api::gui::PluginGui,
loader::wasm::wasm_host::{WasmPlugin, args::OwnedArg}, loader::wasm::wasm_host::{WasmPlugin, args::OwnedArg},
}, },
server::Server, server::Server,
@ -30,6 +32,8 @@ pub type ServerResource = WasmResource<Arc<Server>>;
pub type ContextResource = WasmResource<Arc<Context>>; pub type ContextResource = WasmResource<Arc<Context>>;
pub type PlayerResource = WasmResource<Arc<Player>>; pub type PlayerResource = WasmResource<Arc<Player>>;
pub type WorldResource = WasmResource<Arc<World>>; pub type WorldResource = WasmResource<Arc<World>>;
pub type ScoreboardResource = WasmResource<Arc<World>>;
pub type GuiResource = WasmResource<Arc<Mutex<PluginGui>>>;
pub type TextComponentResource = WasmResource<TextComponent>; pub type TextComponentResource = WasmResource<TextComponent>;
pub type CommandResource = WasmResource<CommandTree>; pub type CommandResource = WasmResource<CommandTree>;
pub type CommandSenderResource = WasmResource<CommandSender>; pub type CommandSenderResource = WasmResource<CommandSender>;
@ -95,6 +99,22 @@ impl PluginHostState {
Ok(wasmtime::component::Resource::new_own(resource.rep())) Ok(wasmtime::component::Resource::new_own(resource.rep()))
} }
pub fn add_scoreboard<T>(
&mut self,
provider: Arc<World>,
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
let resource = self.resource_table.push(ScoreboardResource { provider })?;
Ok(wasmtime::component::Resource::new_own(resource.rep()))
}
pub fn add_gui<T>(
&mut self,
provider: Arc<Mutex<PluginGui>>,
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
let resource = self.resource_table.push(GuiResource { provider })?;
Ok(wasmtime::component::Resource::new_own(resource.rep()))
}
pub fn add_text_component<T>( pub fn add_text_component<T>(
&mut self, &mut self,
provider: TextComponent, provider: TextComponent,

View File

@ -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<Gui>) -> wasmtime::Result<&GuiResource> {
self.resource_table
.get::<GuiResource>(&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<crate::plugin::loader::wasm::wasm_host::wit::v0_1_0::pumpkin::plugin::text::TextComponent>,
) -> wasmtime::Result<Resource<Gui>> {
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<Gui>,
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<Gui>,
slot: u32,
) -> wasmtime::Result<Option<WitItemStack>> {
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<Gui>) -> wasmtime::Result<()> {
self.resource_table
.delete::<GuiResource>(Resource::new_own(rep.rep()))
.map_err(wasmtime::Error::from)?;
Ok(())
}
}

View File

@ -11,10 +11,12 @@ pub mod common;
pub mod context; pub mod context;
pub mod entity; pub mod entity;
pub mod events; pub mod events;
pub mod gui;
pub mod i18n; pub mod i18n;
pub mod logging; pub mod logging;
pub mod permission; pub mod permission;
pub mod player; pub mod player;
pub mod scoreboard;
pub mod server; pub mod server;
pub mod text; pub mod text;
pub mod world; pub mod world;

View File

@ -1,12 +1,17 @@
use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use tokio::sync::Mutex;
use wasmtime::component::Resource; use wasmtime::component::Resource;
use crate::plugin::api::gui::PluginScreenHandler;
use crate::{ use crate::{
entity::{EntityBase, player::TitleMode}, entity::{EntityBase, player::TitleMode},
net::DisconnectReason, net::DisconnectReason,
plugin::loader::wasm::wasm_host::{ plugin::loader::wasm::wasm_host::{
DowncastResourceExt, DowncastResourceExt,
state::{PlayerResource, PluginHostState, TextComponentResource, WorldResource}, state::{
GuiResource, PlayerResource, PluginHostState, TextComponentResource, WorldResource,
},
wit::v0_1_0::{ wit::v0_1_0::{
events::{ events::{
from_wasm_game_mode, from_wasm_position, to_wasm_game_mode, to_wasm_position, 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(()) Ok(())
} }
async fn open_gui(
&mut self,
player: Resource<Player>,
gui: Resource<pumpkin::plugin::gui::Gui>,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let gui_res = self
.resource_table
.get::<GuiResource>(&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( async fn ban(
&mut self, &mut self,
player: Resource<Player>, player: Resource<Player>,

View File

@ -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<scoreboard::Scoreboard>,
) -> wasmtime::Result<&ScoreboardResource> {
self.resource_table
.get::<ScoreboardResource>(&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<scoreboard::Scoreboard>,
name: String,
display_name: Resource<pumpkin::plugin::text::TextComponent>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>,
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<scoreboard::Scoreboard>) -> wasmtime::Result<()> {
self.resource_table
.delete::<ScoreboardResource>(Resource::new_own(rep.rep()))
.map_err(wasmtime::Error::from)?;
Ok(())
}
}
fn map_team_settings(
name: String,
settings: &TeamSettings,
state: &PluginHostState,
) -> wasmtime::Result<Team> {
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,
}
}

View File

@ -20,7 +20,7 @@ impl PluginHostState {
.map_err(wasmtime::Error::from) .map_err(wasmtime::Error::from)
} }
fn get_text_provider( pub(crate) fn get_text_provider(
&self, &self,
res: &Resource<pumpkin::plugin::text::TextComponent>, res: &Resource<pumpkin::plugin::text::TextComponent>,
) -> wasmtime::Result<pumpkin_util::text::TextComponent> { ) -> wasmtime::Result<pumpkin_util::text::TextComponent> {
@ -220,6 +220,14 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
Ok(()) Ok(())
} }
async fn get_scoreboard(
&mut self,
world: Resource<World>,
) -> wasmtime::Result<Resource<pumpkin::plugin::scoreboard::Scoreboard>> {
let world_provider = self.get_world_res(&world)?.provider.clone();
self.add_scoreboard(world_provider)
}
async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> { async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> {
self.resource_table self.resource_table
.delete::<WorldResource>(Resource::new_own(rep.rep())) .delete::<WorldResource>(Resource::new_own(rep.rep()))

View File

@ -4,9 +4,12 @@ use pumpkin_data::scoreboard::ScoreboardDisplaySlot;
use pumpkin_protocol::{ use pumpkin_protocol::{
NumberFormat, NumberFormat,
codec::var_int::VarInt, 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 tracing::warn;
use super::World; use super::World;
@ -14,13 +17,13 @@ use super::World;
#[derive(Default)] #[derive(Default)]
pub struct Scoreboard { pub struct Scoreboard {
objectives: HashMap<String, ScoreboardObjective<'static>>, objectives: HashMap<String, ScoreboardObjective<'static>>,
// teams: HashMap<String, Team>, teams: HashMap<String, Team>,
scores: HashMap<String, HashMap<String, ScoreboardScore<'static>>>,
} }
impl Scoreboard { 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) { if self.objectives.contains_key(objective.name) {
// Maybe make this an error?
warn!( warn!(
"Tried to create an objective which already exists: {}", "Tried to create an objective which already exists: {}",
&objective.name &objective.name
@ -31,9 +34,9 @@ impl Scoreboard {
.broadcast_packet_all(&CUpdateObjectives::new( .broadcast_packet_all(&CUpdateObjectives::new(
objective.name.to_string(), objective.name.to_string(),
pumpkin_protocol::java::client::play::Mode::Add, pumpkin_protocol::java::client::play::Mode::Add,
objective.display_name, objective.display_name.clone(),
objective.render_type, objective.render_type,
objective.number_format, objective.number_format.clone(),
)) ))
.await; .await;
world world
@ -42,10 +45,33 @@ impl Scoreboard {
objective.name.to_string(), objective.name.to_string(),
)) ))
.await; .await;
self.objectives
.insert(objective.name.to_string(), objective);
} }
pub async fn update_score(&self, world: &World, score: ScoreboardScore<'_>) { pub async fn remove_objective(&mut self, world: &World, name: &str) {
if self.objectives.contains_key(score.objective_name) { 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!( warn!(
"Tried to place a score into an objective which does not exist: {}", "Tried to place a score into an objective which does not exist: {}",
&score.objective_name &score.objective_name
@ -57,25 +83,163 @@ impl Scoreboard {
score.entity_name.to_string(), score.entity_name.to_string(),
score.objective_name.to_string(), score.objective_name.to_string(),
score.value, score.value,
score.display_name, score.display_name.clone(),
score.number_format, score.number_format.clone(),
)) ))
.await; .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) { pub async fn remove_score(&mut self, world: &World, entity_name: &str, objective_name: &str) {
// if self.teams.contains_key(&name) { world
// // Maybe make this an error ? .broadcast_packet_all(&CUpdateScore::new_remove(
// log::warn!("Tried to create Team which does already exist, {}", name); 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> { pub struct ScoreboardObjective<'a> {
name: &'a str, pub name: &'a str,
display_name: TextComponent, pub display_name: TextComponent,
render_type: RenderType, pub render_type: RenderType,
number_format: Option<NumberFormat>, pub number_format: Option<NumberFormat>,
} }
impl<'a> ScoreboardObjective<'a> { impl<'a> ScoreboardObjective<'a> {
@ -96,11 +260,11 @@ impl<'a> ScoreboardObjective<'a> {
} }
pub struct ScoreboardScore<'a> { pub struct ScoreboardScore<'a> {
entity_name: &'a str, pub entity_name: &'a str,
objective_name: &'a str, pub objective_name: &'a str,
value: VarInt, pub value: VarInt,
display_name: Option<TextComponent>, pub display_name: Option<TextComponent>,
number_format: Option<NumberFormat>, pub number_format: Option<NumberFormat>,
} }
impl<'a> ScoreboardScore<'a> { 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<String>,
}