feat(plugin-api): allow to set tab header/footer

This commit is contained in:
Alexander Medvedev 2026-04-20 14:06:37 +02:00
parent 53aa958d90
commit cddb7e3f6a
20 changed files with 468 additions and 89 deletions

View File

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

View File

@ -13,7 +13,7 @@ interface boss-bar {
white,
}
/// Represents the division (notches) of a boss bar.
/// Represents the notches/divisions of a boss bar.
enum boss-bar-division {
no-division,
notches-6,
@ -22,6 +22,16 @@ interface boss-bar {
notches-20,
}
/// Metadata flags that can be toggled on a boss bar.
record boss-bar-metadata {
/// Darkens the sky for players who see the boss bar.
darken-sky: bool,
/// If true, the bar is considered a "dragon bar" (used for Ender Dragon).
dragon-bar: bool,
/// If true, fog is created around the players who see the boss bar.
create-fog: bool,
}
/// Represents a boss bar that can be shown to players.
resource boss-bar {
/// Creates a new boss bar.
@ -51,6 +61,12 @@ interface boss-bar {
/// Sets the division of this boss bar.
set-division: func(division: boss-bar-division);
/// Returns the currently active metadata on this boss bar.
get-metadata: func() -> boss-bar-metadata;
/// Updates the metadata on this boss bar.
set-metadata: func(metadata: boss-bar-metadata);
/// Adds a player to this boss bar, making it visible to them.
add-player: func(player: player);

View File

@ -63,12 +63,25 @@ interface player {
has-permission: func (node: string) -> bool;
/// Returns the player's display name, which may include formatting.
get-display-name: func () -> text-component;
/// Sets the player's display name.
set-display-name: func (display-name: text-component);
// Messaging & titles
/// Sends a system message to the player's chat.
send-system-message: func (text: text-component, overlay: bool);
/// Sets the header and footer of the player's Tab list.
set-tab-list-header-footer: func (header: text-component, footer: text-component);
/// Sets the sorting order of the player in the Tab list.
set-tab-list-order: func (order: s32);
/// Sets the latency (ping) shown for the player in the Tab list.
set-tab-list-latency: func (latency: s32);
/// Sets whether the player is visible in the Tab list.
set-tab-list-listed: func (listed: bool);
/// Shows a large title on the player's screen.
show-title: func (text: text-component);
/// Shows a smaller subtitle on the player's screen.
show-subtitle: func (text: text-component);

View File

@ -2,6 +2,7 @@ interface server {
use player.{player};
use common.{game-mode};
use %world.{%world};
use text.{text-component};
/// Represents the difficulty setting of a Minecraft server.
enum difficulty {
@ -49,6 +50,9 @@ interface server {
/// Broadcasts a system chat message to all players.
broadcast: func(message: string);
/// Broadcasts a tab list header and footer to all players.
broadcast-tab-list-header-footer: func(header: text-component, footer: text-component);
/// Returns the maximum number of players allowed on the server.
get-max-players: func() -> u32;

View File

@ -81,6 +81,7 @@ mod stop_sound;
mod store_cookie;
mod subtitle;
mod system_chat_message;
mod tab_list;
mod take_item;
mod teleport_entity;
mod ticking_state;
@ -179,6 +180,7 @@ pub use stop_sound::*;
pub use store_cookie::*;
pub use subtitle::*;
pub use system_chat_message::*;
pub use tab_list::*;
pub use take_item::*;
pub use teleport_entity::*;
pub use ticking_state::*;

View File

@ -1,3 +1,5 @@
use pumpkin_util::text::TextComponent;
use crate::{Property, VarInt};
pub enum PlayerAction<'a> {
@ -8,9 +10,9 @@ pub enum PlayerAction<'a> {
InitializeChat(Option<InitChat>),
UpdateGameMode(VarInt),
UpdateListed(bool),
UpdateLatency(u8),
UpdateDisplayName(u8),
UpdateListOrder,
UpdateLatency(VarInt),
UpdateDisplayName(Option<&'a TextComponent>),
UpdateListOrder(VarInt),
}
pub struct InitChat {

View File

@ -105,9 +105,11 @@ impl ClientPacket for CPlayerInfoUpdate<'_> {
}
PlayerAction::UpdateGameMode(gamemode) => p.write_var_int(gamemode)?,
PlayerAction::UpdateListed(listed) => p.write_bool(*listed)?,
PlayerAction::UpdateLatency(_) => todo!(),
PlayerAction::UpdateDisplayName(_) => todo!(),
PlayerAction::UpdateListOrder => todo!(),
PlayerAction::UpdateLatency(latency) => p.write_var_int(latency)?,
PlayerAction::UpdateDisplayName(display_name) => {
p.write_option(display_name, crate::ser::NetworkWriteExt::write_serialize)?;
}
PlayerAction::UpdateListOrder(order) => p.write_var_int(order)?,
}
}

View File

@ -0,0 +1,21 @@
use pumpkin_data::packet::clientbound::PLAY_TAB_LIST;
use pumpkin_macros::java_packet;
use pumpkin_util::text::TextComponent;
use serde::Serialize;
/// Updates the header and footer of the player list (Tab List).
#[derive(Serialize)]
#[java_packet(PLAY_TAB_LIST)]
pub struct CTabList<'a> {
/// The text to be displayed at the top of the player list.
pub header: &'a TextComponent,
/// The text to be displayed at the bottom of the player list.
pub footer: &'a TextComponent,
}
impl<'a> CTabList<'a> {
#[must_use]
pub const fn new(header: &'a TextComponent, footer: &'a TextComponent) -> Self {
Self { header, footer }
}
}

View File

@ -1,4 +1,5 @@
use core::str;
use serde::Serialize;
use std::io::{Read, Write};
use crate::{
@ -283,9 +284,7 @@ pub trait NetworkWriteExt {
list: &[G],
writer: impl Fn(&mut Self, &G) -> Result<(), WritingError>,
) -> Result<(), WritingError> {
self.write_var_int(&list.len().try_into().map_err(|_| {
WritingError::Message(format!("{} isn't representable as a VarInt", list.len()))
})?)?;
self.write_var_int(&(list.len() as i32).into())?;
for data in list {
writer(self, data)?;
@ -294,6 +293,10 @@ pub trait NetworkWriteExt {
Ok(())
}
fn write_serialize<G: Serialize>(&mut self, data: &G) -> Result<(), WritingError>
where
Self: Sized;
fn write_nbt(&mut self, data: NbtTag) -> Result<(), WritingError>;
}
@ -398,6 +401,7 @@ impl<W: Write> NetworkWriteExt for W {
writer: impl Fn(&mut Self, &G) -> Result<(), WritingError>,
) -> Result<(), WritingError> {
self.write_var_int(&(list.len() as i32).into())?;
for data in list {
writer(self, data)?;
}
@ -405,6 +409,11 @@ impl<W: Write> NetworkWriteExt for W {
Ok(())
}
fn write_serialize<G: Serialize>(&mut self, data: &G) -> Result<(), WritingError> {
let mut serializer = serializer::Serializer::new(self);
data.serialize(&mut serializer)
}
fn write_nbt(&mut self, data: NbtTag) -> Result<(), WritingError> {
let mut write_adaptor = WriteAdaptor::new(self);
data.serialize(&mut write_adaptor)

View File

@ -296,7 +296,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_color(server, namespace.to_string(), color.clone())
.update_color(server, namespace.to_string(), *color)
.await
{
Ok(()) => {}
@ -450,7 +450,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_division(server, namespace.to_string(), style.clone())
.update_division(server, namespace.to_string(), *style)
.await
{
Ok(()) => {}

View File

@ -59,9 +59,9 @@ use pumpkin_protocol::java::client::play::{
COpenScreen, CParticle, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition,
CPlayerSpawnPosition, CRespawn, CSetContainerContent, CSetContainerProperty, CSetContainerSlot,
CSetCursorItem, CSetEquipment, CSetExperience, CSetHealth, CSetPlayerInventory,
CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle, CSystemChatMessage, CTitleAnimation,
CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata, PlayerAction,
PlayerInfoFlags, PreviousMessage,
CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle, CSystemChatMessage, CTabList,
CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata,
PlayerAction, PlayerInfoFlags, PreviousMessage,
};
use pumpkin_protocol::java::server::play::SClickSlot;
use pumpkin_util::math::{
@ -480,9 +480,16 @@ pub struct Player {
pub screen_handler_sync_id: AtomicU8,
pub screen_handler_listener: Arc<dyn ScreenHandlerListener>,
pub screen_handler_sync_handler: Arc<SyncHandler>,
pub tab_list_header: Mutex<TextComponent>,
pub tab_list_footer: Mutex<TextComponent>,
pub display_name: Mutex<Option<TextComponent>>,
pub tab_list_order: AtomicI32,
pub tab_list_latency: AtomicI32,
pub tab_list_listed: AtomicBool,
}
impl Player {
#[expect(clippy::too_many_lines)]
pub async fn new(
client: ClientPlatform,
gameprofile: GameProfile,
@ -603,11 +610,82 @@ impl Player {
player_screen_handler: player_screen_handler.clone(),
current_screen_handler: Mutex::new(player_screen_handler),
screen_handler_sync_id: AtomicU8::new(0),
screen_handler_listener: Arc::new(ScreenListener {}),
screen_handler_listener: Arc::new(ScreenListener),
screen_handler_sync_handler: Arc::new(SyncHandler::new()),
tab_list_header: Mutex::new(TextComponent::text("")),
tab_list_footer: Mutex::new(TextComponent::text("")),
display_name: Mutex::new(None),
tab_list_order: AtomicI32::new(0),
tab_list_latency: AtomicI32::new(0),
tab_list_listed: AtomicBool::new(true),
}
}
pub async fn set_tab_list_header_footer(&self, header: TextComponent, footer: TextComponent) {
*self.tab_list_header.lock().await = header.clone();
*self.tab_list_footer.lock().await = footer.clone();
self.client
.enqueue_packet(&CTabList::new(&header, &footer))
.await;
}
pub async fn set_display_name(&self, display_name: Option<TextComponent>) {
*self.display_name.lock().await = display_name.clone();
// Update the tab list for everyone
let world = self.world();
world
.broadcast_packet_all(&CPlayerInfoUpdate::new(
PlayerInfoFlags::UPDATE_DISPLAY_NAME.bits(),
&[pumpkin_protocol::java::client::play::Player {
uuid: self.gameprofile.id,
actions: &[PlayerAction::UpdateDisplayName(display_name.as_ref())],
}],
))
.await;
}
pub async fn set_tab_list_order(&self, order: i32) {
self.tab_list_order.store(order, Ordering::Relaxed);
let world = self.world();
world
.broadcast_packet_all(&CPlayerInfoUpdate::new(
PlayerInfoFlags::UPDATE_LIST_PRIORITY.bits(),
&[pumpkin_protocol::java::client::play::Player {
uuid: self.gameprofile.id,
actions: &[PlayerAction::UpdateListOrder(VarInt(order))],
}],
))
.await;
}
pub async fn set_tab_list_latency(&self, latency: i32) {
self.tab_list_latency.store(latency, Ordering::Relaxed);
let world = self.world();
world
.broadcast_packet_all(&CPlayerInfoUpdate::new(
PlayerInfoFlags::UPDATE_LATENCY.bits(),
&[pumpkin_protocol::java::client::play::Player {
uuid: self.gameprofile.id,
actions: &[PlayerAction::UpdateLatency(VarInt(latency))],
}],
))
.await;
}
pub async fn set_tab_list_listed(&self, listed: bool) {
self.tab_list_listed.store(listed, Ordering::Relaxed);
let world = self.world();
world
.broadcast_packet_all(&CPlayerInfoUpdate::new(
PlayerInfoFlags::UPDATE_LISTED.bits(),
&[pumpkin_protocol::java::client::play::Player {
uuid: self.gameprofile.id,
actions: &[PlayerAction::UpdateListed(listed)],
}],
))
.await;
}
/// Spawns a task associated with this player-client. All tasks spawned with this method are awaited
/// when the client. This means tasks should complete in a reasonable amount of time or select
/// on `Self::await_close_interrupt` to cancel the task when the client is closed
@ -3533,17 +3611,22 @@ impl EntityBase for Player {
}
fn get_display_name(&self) -> EntityBaseFuture<'_, TextComponent> {
let name = self.get_name();
let name_clone = name.clone();
let mut name = name.click_event(ClickEvent::SuggestCommand {
command: format!("/tell {} ", self.gameprofile.name.clone()).into(),
});
name = name.hover_event(HoverEvent::show_entity(
self.living_entity.entity.entity_uuid.to_string(),
self.living_entity.entity.entity_type.resource_name.into(),
Some(name_clone),
));
Box::pin(async move { name.insertion(self.gameprofile.name.clone()) })
Box::pin(async move {
if let Some(display_name) = self.display_name.lock().await.as_ref() {
return display_name.clone();
}
let name = self.get_name();
let name_clone = name.clone();
let mut name = name.click_event(ClickEvent::SuggestCommand {
command: format!("/tell {} ", self.gameprofile.name.clone()).into(),
});
name = name.hover_event(HoverEvent::show_entity(
self.living_entity.entity.entity_uuid.to_string(),
self.living_entity.entity.entity_type.resource_name.into(),
Some(name_clone),
));
name.insertion(self.gameprofile.name.clone())
})
}
fn cast_any(&self) -> &dyn std::any::Any {

View File

@ -64,8 +64,8 @@ impl OwnedArg {
Arg::ResourceLocation(s) => Self::ResourceLocation(s.to_string()),
Arg::Block(s) => Self::Block(s.to_string()),
Arg::BlockPredicate(s) => Self::BlockPredicate(s.to_string()),
Arg::BossbarColor(c) => Self::BossbarColor(c.clone()),
Arg::BossbarStyle(s) => Self::BossbarStyle(s.clone()),
Arg::BossbarColor(c) => Self::BossbarColor(*c),
Arg::BossbarStyle(s) => Self::BossbarStyle(*s),
Arg::Particle(p) => Self::Particle(*p),
Arg::Msg(m) => Self::Msg(m.clone()),
Arg::TextComponent(t) => Self::TextComponent(t.clone()),

View File

@ -2,10 +2,11 @@ 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,
BossBarMetadata as WitMetadata,
},
};
use crate::server::Server;
use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions};
use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions, BossbarFlags};
use std::sync::{Arc, Weak};
use tokio::sync::Mutex;
use uuid::Uuid;
@ -41,6 +42,72 @@ fn player_from_resource(
.map(|resource| resource.provider.clone())
}
const fn to_wit_color(color: BossbarColor) -> WitColor {
match color {
BossbarColor::Pink => WitColor::Pink,
BossbarColor::Blue => WitColor::Blue,
BossbarColor::Red => WitColor::Red,
BossbarColor::Green => WitColor::Green,
BossbarColor::Yellow => WitColor::Yellow,
BossbarColor::Purple => WitColor::Purple,
BossbarColor::White => WitColor::White,
}
}
const fn from_wit_color(color: WitColor) -> BossbarColor {
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,
}
}
const fn to_wit_division(division: BossbarDivisions) -> WitDivision {
match division {
BossbarDivisions::NoDivision => WitDivision::NoDivision,
BossbarDivisions::Notches6 => WitDivision::Notches6,
BossbarDivisions::Notches10 => WitDivision::Notches10,
BossbarDivisions::Notches12 => WitDivision::Notches12,
BossbarDivisions::Notches20 => WitDivision::Notches20,
}
}
const fn from_wit_division(division: WitDivision) -> BossbarDivisions {
match division {
WitDivision::NoDivision => BossbarDivisions::NoDivision,
WitDivision::Notches6 => BossbarDivisions::Notches6,
WitDivision::Notches10 => BossbarDivisions::Notches10,
WitDivision::Notches12 => BossbarDivisions::Notches12,
WitDivision::Notches20 => BossbarDivisions::Notches20,
}
}
const fn to_wit_metadata(flags: BossbarFlags) -> WitMetadata {
WitMetadata {
darken_sky: flags.contains(BossbarFlags::DARKEN_SKY),
dragon_bar: flags.contains(BossbarFlags::DRAGON_BAR),
create_fog: flags.contains(BossbarFlags::CREATE_FOG),
}
}
fn from_wit_metadata(metadata: WitMetadata) -> BossbarFlags {
let mut f = BossbarFlags::empty();
if metadata.darken_sky {
f |= BossbarFlags::DARKEN_SKY;
}
if metadata.dragon_bar {
f |= BossbarFlags::DRAGON_BAR;
}
if metadata.create_fog {
f |= BossbarFlags::CREATE_FOG;
}
f
}
impl PluginHostState {
fn get_bossbar_res(&self, res: &Resource<BossBar>) -> wasmtime::Result<&BossBarResource> {
self.resource_table
@ -63,23 +130,8 @@ impl boss_bar::HostBossBar for PluginHostState {
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,
};
bossbar.color = from_wit_color(color);
bossbar.division = from_wit_division(division);
let server = self.server.as_ref().expect("server not available").clone();
let plugin_bossbar = Arc::new(Mutex::new(PluginBossBar::new(
@ -147,29 +199,85 @@ impl boss_bar::HostBossBar for PluginHostState {
Ok(())
}
async fn get_color(&mut self, _res: Resource<BossBar>) -> wasmtime::Result<WitColor> {
todo!()
async fn get_color(&mut self, res: Resource<BossBar>) -> wasmtime::Result<WitColor> {
let pbb = self.get_bossbar_res(&res)?.provider.lock().await;
Ok(to_wit_color(pbb.bossbar.color))
}
async fn set_color(
&mut self,
_res: Resource<BossBar>,
_color: WitColor,
) -> wasmtime::Result<()> {
todo!()
async fn set_color(&mut self, res: Resource<BossBar>, color: WitColor) -> wasmtime::Result<()> {
let mut pbb = self.get_bossbar_res(&res)?.provider.lock().await;
pbb.bossbar.color = from_wit_color(color);
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_style(
&pbb.bossbar.uuid,
pbb.bossbar.color,
pbb.bossbar.division,
)
.await;
}
}
}
Ok(())
}
async fn get_division(&mut self, _res: Resource<BossBar>) -> wasmtime::Result<WitDivision> {
todo!()
async fn get_division(&mut self, res: Resource<BossBar>) -> wasmtime::Result<WitDivision> {
let pbb = self.get_bossbar_res(&res)?.provider.lock().await;
Ok(to_wit_division(pbb.bossbar.division))
}
async fn set_division(
&mut self,
_res: Resource<BossBar>,
_division: WitDivision,
res: Resource<BossBar>,
division: WitDivision,
) -> wasmtime::Result<()> {
todo!()
let mut pbb = self.get_bossbar_res(&res)?.provider.lock().await;
pbb.bossbar.division = from_wit_division(division);
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_style(
&pbb.bossbar.uuid,
pbb.bossbar.color,
pbb.bossbar.division,
)
.await;
}
}
}
Ok(())
}
async fn get_metadata(&mut self, res: Resource<BossBar>) -> wasmtime::Result<WitMetadata> {
let pbb = self.get_bossbar_res(&res)?.provider.lock().await;
Ok(to_wit_metadata(pbb.bossbar.flags))
}
async fn set_metadata(
&mut self,
res: Resource<BossBar>,
metadata: WitMetadata,
) -> wasmtime::Result<()> {
let mut pbb = self.get_bossbar_res(&res)?.provider.lock().await;
pbb.bossbar.flags = from_wit_metadata(metadata);
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_flags(&pbb.bossbar.uuid, pbb.bossbar.flags)
.await;
}
}
}
Ok(())
}
async fn get_players(
&mut self,
_res: Resource<BossBar>,
res: Resource<BossBar>,
) -> wasmtime::Result<
Vec<
Resource<
@ -177,7 +285,23 @@ impl boss_bar::HostBossBar for PluginHostState {
>,
>,
> {
todo!()
let players = {
let pbb = self.get_bossbar_res(&res)?.provider.lock().await;
pbb.players.clone()
};
let server = self
.server
.clone()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
let mut wit_players = Vec::new();
for uuid in players {
if let Some(player) = server.get_player_by_uuid(uuid) {
wit_players.push(self.add_player(player)?);
}
}
Ok(wit_players)
}
async fn add_player(

View File

@ -34,7 +34,7 @@ fn player_from_resource(
.map(|resource| resource.provider.clone())
}
fn text_component_from_resource(
pub(crate) fn text_component_from_resource(
state: &PluginHostState,
text: &Resource<pumpkin::plugin::text::TextComponent>,
) -> pumpkin_util::text::TextComponent {
@ -248,6 +248,17 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
.map_err(|_| wasmtime::Error::msg("failed to add text-component resource"))
}
async fn set_display_name(
&mut self,
player: Resource<Player>,
display_name: wasmtime::component::Resource<pumpkin::plugin::text::TextComponent>,
) -> wasmtime::Result<()> {
let display_name = text_component_from_resource(self, &display_name);
let player = player_from_resource(self, &player)?;
player.set_display_name(Some(display_name)).await;
Ok(())
}
async fn send_system_message(
&mut self,
player: Resource<Player>,
@ -260,6 +271,49 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(())
}
async fn set_tab_list_header_footer(
&mut self,
player: Resource<Player>,
header: wasmtime::component::Resource<pumpkin::plugin::text::TextComponent>,
footer: wasmtime::component::Resource<pumpkin::plugin::text::TextComponent>,
) -> wasmtime::Result<()> {
let header = text_component_from_resource(self, &header);
let footer = text_component_from_resource(self, &footer);
let player = player_from_resource(self, &player)?;
player.set_tab_list_header_footer(header, footer).await;
Ok(())
}
async fn set_tab_list_order(
&mut self,
player: Resource<Player>,
order: i32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.set_tab_list_order(order).await;
Ok(())
}
async fn set_tab_list_latency(
&mut self,
player: Resource<Player>,
latency: i32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.set_tab_list_latency(latency).await;
Ok(())
}
async fn set_tab_list_listed(
&mut self,
player: Resource<Player>,
listed: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.set_tab_list_listed(listed).await;
Ok(())
}
async fn show_title(
&mut self,
player: Resource<Player>,

View File

@ -1,6 +1,8 @@
use pumpkin_util::text::TextComponent;
use uuid::Uuid;
use wasmtime::component::Resource;
use super::player::text_component_from_resource;
use crate::plugin::loader::wasm::wasm_host::{
state::{PluginHostState, ServerResource},
wit::v0_1::pumpkin::{
@ -162,8 +164,8 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
server
.broadcast_message(
&pumpkin_util::text::TextComponent::text(message),
&pumpkin_util::text::TextComponent::text("Server"),
&TextComponent::text(message),
&TextComponent::text("Server"),
0,
None,
)
@ -172,6 +174,24 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
Ok(())
}
async fn broadcast_tab_list_header_footer(
&mut self,
_rep: Resource<Server>,
header: wasmtime::component::Resource<pumpkin::plugin::text::TextComponent>,
footer: wasmtime::component::Resource<pumpkin::plugin::text::TextComponent>,
) -> wasmtime::Result<()> {
let header = text_component_from_resource(self, &header);
let footer = text_component_from_resource(self, &footer);
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
server
.broadcast_tab_list_header_footer(&header, &footer)
.await;
Ok(())
}
async fn get_max_players(&mut self, _rep: Resource<Server>) -> wasmtime::Result<u32> {
let server = self
.server

View File

@ -27,7 +27,7 @@ use tracing::{debug, error, info, warn};
use crate::command::CommandSender;
use pumpkin_macros::send_cancellable;
use pumpkin_protocol::java::client::login::CEncryptionRequest;
use pumpkin_protocol::java::client::play::CChangeDifficulty;
use pumpkin_protocol::java::client::play::{CChangeDifficulty, CTabList};
use pumpkin_protocol::{ClientPacket, java::client::config::CPluginMessage};
use pumpkin_util::Difficulty;
use pumpkin_util::text::TextComponent;
@ -506,6 +506,21 @@ impl Server {
}
}
pub async fn broadcast_tab_list_header_footer(
&self,
header: &TextComponent,
footer: &TextComponent,
) {
let packet = CTabList::new(header, footer);
for world in self.worlds.load().iter() {
for player in world.players.load().iter() {
*player.tab_list_header.lock().await = header.clone();
*player.tab_list_footer.lock().await = footer.clone();
player.client.enqueue_packet(&packet).await;
}
}
}
pub async fn broadcast_message(
&self,
message: &TextComponent,

View File

@ -1,9 +1,10 @@
use crate::entity::player::Player;
use bitflags::bitflags;
use pumpkin_protocol::java::client::play::{BosseventAction, CBossEvent};
use pumpkin_util::text::TextComponent;
use uuid::Uuid;
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum BossbarColor {
Pink,
Blue,
@ -14,7 +15,7 @@ pub enum BossbarColor {
White,
}
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum BossbarDivisions {
NoDivision,
Notches6,
@ -23,12 +24,13 @@ pub enum BossbarDivisions {
Notches20,
}
#[derive(Clone)]
pub enum BossbarFlags {
NoFlags,
DarkenSky = 0x01,
DragonBar = 0x02,
CreateFog = 0x04,
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BossbarFlags: u8 {
const DARKEN_SKY = 0x01;
const DRAGON_BAR = 0x02;
const CREATE_FOG = 0x04;
}
}
#[derive(Clone)]
@ -52,7 +54,7 @@ impl Bossbar {
health: 0.0,
color: BossbarColor::White,
division: BossbarDivisions::NoDivision,
flags: BossbarFlags::NoFlags,
flags: BossbarFlags::empty(),
}
}
}
@ -60,19 +62,18 @@ impl Bossbar {
/// Extra methods for [`Player`] to send and manage the bossbar.
impl Player {
pub async fn send_bossbar(&self, bossbar: &Bossbar) {
// Maybe this section could be implemented. Feel free to change it.
let bossbar = bossbar.clone();
let boss_action = BosseventAction::Add {
title: bossbar.title,
title: bossbar.title.clone(),
health: bossbar.health,
color: (bossbar.color as u8).into(),
division: (bossbar.division as u8).into(),
flags: bossbar.flags as u8,
flags: bossbar.flags.bits(),
};
let packet = CBossEvent::new(&bossbar.uuid, boss_action);
self.client.enqueue_packet(&packet).await;
}
pub async fn remove_bossbar(&self, uuid: Uuid) {
let boss_action = BosseventAction::Remove;
@ -110,7 +111,7 @@ impl Player {
}
pub async fn update_bossbar_flags(&self, uuid: &Uuid, flags: BossbarFlags) {
let boss_action = BosseventAction::UpdateFlags(flags as u8);
let boss_action = BosseventAction::UpdateFlags(flags.bits());
let packet = CBossEvent::new(uuid, boss_action);
self.client.enqueue_packet(&packet).await;

View File

@ -292,8 +292,8 @@ impl CustomBossbars {
player
.update_bossbar_style(
&bossbar.bossbar_data.uuid,
bossbar.bossbar_data.color.clone(),
bossbar.bossbar_data.division.clone(),
bossbar.bossbar_data.color,
bossbar.bossbar_data.division,
)
.await;
}
@ -331,8 +331,8 @@ impl CustomBossbars {
player
.update_bossbar_style(
&bossbar.bossbar_data.uuid,
bossbar.bossbar_data.color.clone(),
bossbar.bossbar_data.division.clone(),
bossbar.bossbar_data.color,
bossbar.bossbar_data.division,
)
.await;
}

View File

@ -603,7 +603,7 @@ impl DragonFight {
health: 1.0,
color: BossbarColor::Pink,
division: BossbarDivisions::NoDivision,
flags: BossbarFlags::DragonBar,
flags: BossbarFlags::DRAGON_BAR,
}
}

View File

@ -1837,7 +1837,9 @@ impl World {
self.broadcast_packet_all(&CPlayerInfoUpdate::new(
(PlayerInfoFlags::ADD_PLAYER
| PlayerInfoFlags::UPDATE_GAME_MODE
| PlayerInfoFlags::UPDATE_LISTED)
| PlayerInfoFlags::UPDATE_LISTED
| PlayerInfoFlags::UPDATE_LATENCY
| PlayerInfoFlags::UPDATE_LIST_PRIORITY)
.bits(),
&[pumpkin_protocol::java::client::play::Player {
uuid: gameprofile.id,
@ -1848,6 +1850,8 @@ impl World {
},
PlayerAction::UpdateGameMode(VarInt(gamemode as i32)),
PlayerAction::UpdateListed(true),
PlayerAction::UpdateLatency(VarInt(0)),
PlayerAction::UpdateListOrder(VarInt(0)),
],
}],
))
@ -1867,7 +1871,13 @@ impl World {
name: &player.gameprofile.name,
properties: &player.gameprofile.properties,
},
PlayerAction::UpdateListed(true),
PlayerAction::UpdateListed(player.tab_list_listed.load(Ordering::Relaxed)),
PlayerAction::UpdateLatency(VarInt(
player.tab_list_latency.load(Ordering::Relaxed),
)),
PlayerAction::UpdateListOrder(VarInt(
player.tab_list_order.load(Ordering::Relaxed),
)),
];
if base_config.allow_chat_reports {
@ -1883,7 +1893,10 @@ impl World {
current_player_data.push((&player.gameprofile.id, player_actions));
}
let mut action_flags = PlayerInfoFlags::ADD_PLAYER | PlayerInfoFlags::UPDATE_LISTED;
let mut action_flags = PlayerInfoFlags::ADD_PLAYER
| PlayerInfoFlags::UPDATE_LISTED
| PlayerInfoFlags::UPDATE_LATENCY
| PlayerInfoFlags::UPDATE_LIST_PRIORITY;
if base_config.allow_chat_reports {
action_flags |= PlayerInfoFlags::INITIALIZE_CHAT;
}