feat: more plugin api features
This commit is contained in:
parent
bc7e59df39
commit
413f7ccb16
@ -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;
|
||||||
|
|||||||
38
pumpkin-plugin-wit/v0.1.0/gui.wit
Normal file
38
pumpkin-plugin-wit/v0.1.0/gui.wit
Normal 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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>);
|
||||||
|
|
||||||
@ -90,4 +93,4 @@ interface player {
|
|||||||
|
|
||||||
get-ip: func() -> string;
|
get-ip: func() -> string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
72
pumpkin-plugin-wit/v0.1.0/scoreboard.wit
Normal file
72
pumpkin-plugin-wit/v0.1.0/scoreboard.wit
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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::*;
|
||||||
|
|||||||
81
pumpkin-protocol/src/java/client/play/set_player_team.rs
Normal file
81
pumpkin-protocol/src/java/client/play/set_player_team.rs
Normal 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(¶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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -74,6 +74,7 @@ pub enum Mode {
|
|||||||
Update,
|
Update,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub enum RenderType {
|
pub enum RenderType {
|
||||||
Integer,
|
Integer,
|
||||||
Hearts,
|
Hearts,
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
136
pumpkin/src/plugin/api/gui.rs
Normal file
136
pumpkin/src/plugin/api/gui.rs
Normal 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() })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
124
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/gui.rs
Normal file
124
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/gui.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()))
|
||||||
|
|||||||
@ -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>,
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user