feat(plugin-api): add bossbars

This commit is contained in:
Alexander Medvedev 2026-04-19 19:40:19 +02:00
parent 71cddd72bc
commit 452edf8987
10 changed files with 588 additions and 1 deletions

View File

@ -0,0 +1,66 @@
interface boss-bar {
use text.{text-component};
use player.{player};
/// Represents the color of a boss bar.
enum boss-bar-color {
pink,
blue,
red,
green,
yellow,
purple,
white,
}
/// Represents the division (notches) of a boss bar.
enum boss-bar-division {
no-division,
notches-6,
notches-10,
notches-12,
notches-20,
}
/// Represents a boss bar that can be shown to players.
resource boss-bar {
/// Creates a new boss bar.
constructor(title: text-component, color: boss-bar-color, division: boss-bar-division);
/// Returns the title of this boss bar.
get-title: func() -> text-component;
/// Sets the title of this boss bar.
set-title: func(title: text-component);
/// Returns the health (0.0 to 1.0) of this boss bar.
get-health: func() -> f32;
/// Sets the health (0.0 to 1.0) of this boss bar.
set-health: func(health: f32);
/// Returns the color of this boss bar.
get-color: func() -> boss-bar-color;
/// Sets the color of this boss bar.
set-color: func(color: boss-bar-color);
/// Returns the division of this boss bar.
get-division: func() -> boss-bar-division;
/// Sets the division of this boss bar.
set-division: func(division: boss-bar-division);
/// Adds a player to this boss bar, making it visible to them.
add-player: func(player: player);
/// Removes a player from this boss bar, hiding it from them.
remove-player: func(player: player);
/// Returns a list of all players currently seeing this boss bar.
get-players: func() -> list<player>;
/// Removes this boss bar from all players and cleans it up.
remove-all: func();
}
}

View File

@ -34,5 +34,17 @@ interface gui {
constructor(%type: gui-type, title: text-component);
set-item: func(slot: u32, item: item-stack);
get-item: func(slot: u32) -> option<item-stack>;
/// Returns the type of this GUI.
get-type: func() -> gui-type;
/// Returns the title of this GUI.
get-title: func() -> text-component;
/// Returns the number of slots in this GUI.
get-size: func() -> u32;
/// Clears all items from the GUI.
clear-items: func();
}
}

View File

@ -33,6 +33,8 @@ world plugin {
import %world;
/// Provides entity types and manipulation.
import entity;
/// Provides boss bar creation and management.
import boss-bar;
// --- Plugin-provided exports ---

View File

@ -1,5 +1,7 @@
interface server {
use player.{player};
use common.{game-mode};
use %world.{%world};
/// Represents the difficulty setting of a Minecraft server.
enum difficulty {
@ -37,5 +39,44 @@ interface server {
/// Searches for a player by their UUID string.
get-player-by-uuid: func(id: string) -> option<player>;
/// Returns a list of all worlds on this server.
get-all-worlds: func() -> list<%world>;
/// Gets a world by its dimension name (e.g., "minecraft:overworld").
get-world-by-name: func(name: string) -> option<%world>;
/// Broadcasts a system chat message to all players.
broadcast: func(message: string);
/// Returns the maximum number of players allowed on the server.
get-max-players: func() -> u32;
/// Returns whether the server is in hardcore mode.
is-hardcore: func() -> bool;
/// Returns whether the server is in online mode (authenticates with Mojang).
is-online-mode: func() -> bool;
/// Returns the server's Message of the Day (MOTD).
get-motd: func() -> string;
/// Returns whether the server has a whitelist enabled.
has-whitelist: func() -> bool;
/// Returns whether the server allows the Nether dimension.
get-allow-nether: func() -> bool;
/// Returns whether the server allows the End dimension.
get-allow-end: func() -> bool;
/// Returns the server's maximum view distance.
get-view-distance: func() -> u8;
/// Returns the server's maximum simulation distance.
get-simulation-distance: func() -> u8;
/// Returns the server's default gamemode.
get-default-gamemode: func() -> game-mode;
}
}

View File

@ -35,7 +35,6 @@ impl PlayerInteractEvent {
///
/// - `player`: A reference-counted pointer to the player who triggered the event.
/// - `action`: The type of interaction performed.
/// - `item`: A reference-counted, mutex-protected item stack used during the interaction.
/// - `block`: The block that was interacted with.
/// - `clicked_pos`: The optional position of the block that was clicked.
///

View File

@ -36,6 +36,9 @@ pub type EntityResource = WasmResource<Arc<dyn EntityBase>>;
pub type WorldResource = WasmResource<Arc<World>>;
pub type ScoreboardResource = WasmResource<Arc<World>>;
pub type GuiResource = WasmResource<Arc<Mutex<PluginGui>>>;
pub type BossBarResource = WasmResource<
Arc<Mutex<crate::plugin::loader::wasm::wasm_host::wit::v0_1::boss_bar::PluginBossBar>>,
>;
pub type TextComponentResource = WasmResource<TextComponent>;
pub type CommandResource = WasmResource<CommandTree>;
pub type CommandSenderResource = WasmResource<CommandSender>;
@ -125,6 +128,16 @@ impl PluginHostState {
Ok(wasmtime::component::Resource::new_own(resource.rep()))
}
pub fn add_boss_bar<T>(
&mut self,
provider: Arc<
Mutex<crate::plugin::loader::wasm::wasm_host::wit::v0_1::boss_bar::PluginBossBar>,
>,
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
let resource = self.resource_table.push(BossBarResource { provider })?;
Ok(wasmtime::component::Resource::new_own(resource.rep()))
}
pub fn add_text_component<T>(
&mut self,
provider: TextComponent,

View File

@ -0,0 +1,239 @@
use crate::plugin::loader::wasm::wasm_host::{
state::{BossBarResource, PlayerResource, PluginHostState},
wit::v0_1::pumpkin::plugin::boss_bar::{
self, BossBar, BossBarColor as WitColor, BossBarDivision as WitDivision,
},
};
use crate::server::Server;
use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions};
use std::sync::{Arc, Weak};
use tokio::sync::Mutex;
use uuid::Uuid;
use wasmtime::component::Resource;
pub struct PluginBossBar {
pub bossbar: Bossbar,
pub players: Vec<Uuid>,
pub server: Weak<Server>,
}
impl PluginBossBar {
#[must_use]
pub const fn new(bossbar: Bossbar, server: Weak<Server>) -> Self {
Self {
bossbar,
players: Vec::new(),
server,
}
}
}
fn player_from_resource(
state: &PluginHostState,
player: &Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::Player,
>,
) -> wasmtime::Result<std::sync::Arc<crate::entity::player::Player>> {
state
.resource_table
.get::<PlayerResource>(&Resource::new_own(player.rep()))
.map_err(wasmtime::Error::from)
.map(|resource| resource.provider.clone())
}
impl PluginHostState {
fn get_bossbar_res(&self, res: &Resource<BossBar>) -> wasmtime::Result<&BossBarResource> {
self.resource_table
.get::<BossBarResource>(&Resource::new_own(res.rep()))
.map_err(wasmtime::Error::from)
}
}
impl boss_bar::Host for PluginHostState {}
impl boss_bar::HostBossBar for PluginHostState {
async fn new(
&mut self,
title: Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::text::TextComponent,
>,
color: WitColor,
division: WitDivision,
) -> wasmtime::Result<Resource<BossBar>> {
let title = self.get_text_provider(&title)?;
let mut bossbar = Bossbar::new(title);
bossbar.color = match color {
WitColor::Pink => BossbarColor::Pink,
WitColor::Blue => BossbarColor::Blue,
WitColor::Red => BossbarColor::Red,
WitColor::Green => BossbarColor::Green,
WitColor::Yellow => BossbarColor::Yellow,
WitColor::Purple => BossbarColor::Purple,
WitColor::White => BossbarColor::White,
};
bossbar.division = match division {
WitDivision::NoDivision => BossbarDivisions::NoDivision,
WitDivision::Notches6 => BossbarDivisions::Notches6,
WitDivision::Notches10 => BossbarDivisions::Notches10,
WitDivision::Notches12 => BossbarDivisions::Notches12,
WitDivision::Notches20 => BossbarDivisions::Notches20,
};
let server = self.server.as_ref().expect("server not available").clone();
let plugin_bossbar = Arc::new(Mutex::new(PluginBossBar::new(
bossbar,
Arc::downgrade(&server),
)));
self.add_boss_bar(plugin_bossbar)
}
async fn get_title(
&mut self,
res: Resource<BossBar>,
) -> wasmtime::Result<
Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::text::TextComponent,
>,
> {
let title = {
let bossbar = self.get_bossbar_res(&res)?.provider.lock().await;
bossbar.bossbar.title.clone()
};
self.add_text_component(title)
.map_err(|_| wasmtime::Error::msg("Failed to add text component"))
}
async fn set_title(
&mut self,
res: Resource<BossBar>,
title: Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::text::TextComponent,
>,
) -> wasmtime::Result<()> {
let title = self.get_text_provider(&title)?;
let mut pbb = self.get_bossbar_res(&res)?.provider.lock().await;
pbb.bossbar.title = title.clone();
if let Some(server) = pbb.server.upgrade() {
for uuid in &pbb.players {
if let Some(player) = server.get_player_by_uuid(*uuid) {
player
.update_bossbar_title(&pbb.bossbar.uuid, title.clone())
.await;
}
}
}
Ok(())
}
async fn get_health(&mut self, res: Resource<BossBar>) -> wasmtime::Result<f32> {
let pbb = self.get_bossbar_res(&res)?.provider.lock().await;
Ok(pbb.bossbar.health)
}
async fn set_health(&mut self, res: Resource<BossBar>, health: f32) -> wasmtime::Result<()> {
let mut pbb = self.get_bossbar_res(&res)?.provider.lock().await;
pbb.bossbar.health = health;
if let Some(server) = pbb.server.upgrade() {
for uuid in &pbb.players {
if let Some(player) = server.get_player_by_uuid(*uuid) {
player
.update_bossbar_health(&pbb.bossbar.uuid, health)
.await;
}
}
}
Ok(())
}
async fn get_color(&mut self, _res: Resource<BossBar>) -> wasmtime::Result<WitColor> {
todo!()
}
async fn set_color(
&mut self,
_res: Resource<BossBar>,
_color: WitColor,
) -> wasmtime::Result<()> {
todo!()
}
async fn get_division(&mut self, _res: Resource<BossBar>) -> wasmtime::Result<WitDivision> {
todo!()
}
async fn set_division(
&mut self,
_res: Resource<BossBar>,
_division: WitDivision,
) -> wasmtime::Result<()> {
todo!()
}
async fn get_players(
&mut self,
_res: Resource<BossBar>,
) -> wasmtime::Result<
Vec<
Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::Player,
>,
>,
> {
todo!()
}
async fn add_player(
&mut self,
res: Resource<BossBar>,
player: Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::Player,
>,
) -> wasmtime::Result<()> {
let mut pbb = self.get_bossbar_res(&res)?.provider.lock().await;
let player = player_from_resource(self, &player)?;
let uuid = player.gameprofile.id;
if !pbb.players.contains(&uuid) {
pbb.players.push(uuid);
player.send_bossbar(&pbb.bossbar).await;
}
Ok(())
}
async fn remove_player(
&mut self,
res: Resource<BossBar>,
player: Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::Player,
>,
) -> wasmtime::Result<()> {
let mut pbb = self.get_bossbar_res(&res)?.provider.lock().await;
let player = player_from_resource(self, &player)?;
let uuid = player.gameprofile.id;
if let Some(idx) = pbb.players.iter().position(|&x| x == uuid) {
pbb.players.remove(idx);
player.remove_bossbar(pbb.bossbar.uuid).await;
}
Ok(())
}
async fn remove_all(&mut self, res: Resource<BossBar>) -> wasmtime::Result<()> {
let pbb = self.get_bossbar_res(&res)?.provider.lock().await;
if let Some(server) = pbb.server.upgrade() {
for uuid in &pbb.players {
if let Some(player) = server.get_player_by_uuid(*uuid) {
player.remove_bossbar(pbb.bossbar.uuid).await;
}
}
}
Ok(())
}
async fn drop(&mut self, res: Resource<BossBar>) -> wasmtime::Result<()> {
let rep = res.rep();
self.remove_all(res).await?;
self.resource_table
.delete::<BossBarResource>(Resource::new_own(rep))
.map_err(wasmtime::Error::from)?;
Ok(())
}
}

View File

@ -117,6 +117,66 @@ impl gui::HostGui for PluginHostState {
}
}
async fn get_type(&mut self, res: Resource<Gui>) -> wasmtime::Result<GuiType> {
let gui = self.get_gui_res(&res)?.provider.lock().await;
Ok(match gui.window_type {
pumpkin_data::screen::WindowType::Generic9x1 => GuiType::Generic9x1,
pumpkin_data::screen::WindowType::Generic9x2 => GuiType::Generic9x2,
pumpkin_data::screen::WindowType::Generic9x3 => GuiType::Generic9x3,
pumpkin_data::screen::WindowType::Generic9x4 => GuiType::Generic9x4,
pumpkin_data::screen::WindowType::Generic9x5 => GuiType::Generic9x5,
pumpkin_data::screen::WindowType::Generic9x6 => GuiType::Generic9x6,
pumpkin_data::screen::WindowType::Generic3x3 => GuiType::Generic3x3,
pumpkin_data::screen::WindowType::Crafter3x3 => GuiType::Crafter3x3,
pumpkin_data::screen::WindowType::Anvil => GuiType::Anvil,
pumpkin_data::screen::WindowType::Beacon => GuiType::Beacon,
pumpkin_data::screen::WindowType::BlastFurnace => GuiType::BlastFurnace,
pumpkin_data::screen::WindowType::BrewingStand => GuiType::BrewingStand,
pumpkin_data::screen::WindowType::Crafting => GuiType::Crafting,
pumpkin_data::screen::WindowType::Enchantment => GuiType::Enchantment,
pumpkin_data::screen::WindowType::Furnace => GuiType::Furnace,
pumpkin_data::screen::WindowType::Grindstone => GuiType::Grindstone,
pumpkin_data::screen::WindowType::Hopper => GuiType::Hopper,
pumpkin_data::screen::WindowType::Lectern => GuiType::Lectern,
pumpkin_data::screen::WindowType::Loom => GuiType::Loom,
pumpkin_data::screen::WindowType::Merchant => GuiType::Merchant,
pumpkin_data::screen::WindowType::ShulkerBox => GuiType::ShulkerBox,
pumpkin_data::screen::WindowType::Smithing => GuiType::Smithing,
pumpkin_data::screen::WindowType::Smoker => GuiType::Smoker,
pumpkin_data::screen::WindowType::CartographyTable => GuiType::CartographyTable,
pumpkin_data::screen::WindowType::Stonecutter => GuiType::Stonecutter,
})
}
async fn get_title(
&mut self,
res: Resource<Gui>,
) -> wasmtime::Result<
Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::text::TextComponent,
>,
> {
let title = {
let gui = self.get_gui_res(&res)?.provider.lock().await;
gui.title.clone()
};
self.add_text_component(title)
.map_err(|_| wasmtime::Error::msg("Failed to add text component resource"))
}
async fn get_size(&mut self, res: Resource<Gui>) -> wasmtime::Result<u32> {
use pumpkin_world::inventory::Inventory;
let gui = self.get_gui_res(&res)?.provider.lock().await;
Ok(gui.inventory.size() as u32)
}
async fn clear_items(&mut self, res: Resource<Gui>) -> wasmtime::Result<()> {
use pumpkin_world::inventory::Clearable;
let gui = self.get_gui_res(&res)?.provider.lock().await;
gui.inventory.clear().await;
Ok(())
}
async fn drop(&mut self, rep: Resource<Gui>) -> wasmtime::Result<()> {
self.resource_table
.delete::<GuiResource>(Resource::new_own(rep.rep()))

View File

@ -7,6 +7,7 @@ use wasmtime::component::{HasSelf, InstancePre, Linker, bindgen};
use wasmtime::{Engine, Store};
pub mod block_entity;
pub mod boss_bar;
pub mod commands;
pub mod common;
pub mod context;

View File

@ -113,6 +113,160 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
.transpose()
}
async fn get_all_worlds(
&mut self,
_rep: Resource<Server>,
) -> wasmtime::Result<Vec<Resource<pumpkin::plugin::world::World>>> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server
.worlds
.load()
.iter()
.map(|world| {
self.add_world(world.clone())
.expect("failed to add world resource")
})
.collect())
}
async fn get_world_by_name(
&mut self,
_rep: Resource<Server>,
name: String,
) -> wasmtime::Result<Option<Resource<pumpkin::plugin::world::World>>> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server
.worlds
.load()
.iter()
.find(|world| world.dimension.minecraft_name == name)
.map(|world| {
self.add_world(world.clone())
.expect("failed to add world resource")
}))
}
async fn broadcast(&mut self, _rep: Resource<Server>, message: String) -> wasmtime::Result<()> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
server
.broadcast_message(
&pumpkin_util::text::TextComponent::text(message),
&pumpkin_util::text::TextComponent::text("Server"),
0,
None,
)
.await;
Ok(())
}
async fn get_max_players(&mut self, _rep: Resource<Server>) -> wasmtime::Result<u32> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.max_players)
}
async fn is_hardcore(&mut self, _rep: Resource<Server>) -> wasmtime::Result<bool> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.hardcore)
}
async fn is_online_mode(&mut self, _rep: Resource<Server>) -> wasmtime::Result<bool> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.advanced_config.networking.authentication.enabled)
}
async fn get_motd(&mut self, _rep: Resource<Server>) -> wasmtime::Result<String> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.motd.clone())
}
async fn has_whitelist(&mut self, _rep: Resource<Server>) -> wasmtime::Result<bool> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.white_list)
}
async fn get_allow_nether(&mut self, _rep: Resource<Server>) -> wasmtime::Result<bool> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.allow_nether)
}
async fn get_allow_end(&mut self, _rep: Resource<Server>) -> wasmtime::Result<bool> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.allow_end)
}
async fn get_view_distance(&mut self, _rep: Resource<Server>) -> wasmtime::Result<u8> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.view_distance.get())
}
async fn get_simulation_distance(&mut self, _rep: Resource<Server>) -> wasmtime::Result<u8> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.simulation_distance.get())
}
async fn get_default_gamemode(
&mut self,
_rep: Resource<Server>,
) -> wasmtime::Result<pumpkin::plugin::common::GameMode> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(super::events::to_wasm_game_mode(
server.basic_config.default_gamemode,
))
}
async fn drop(&mut self, rep: Resource<Server>) -> wasmtime::Result<()> {
self.resource_table
.delete::<ServerResource>(Resource::new_own(rep.rep()))