From 18db9551ea0a8a4fba557b7452c24e0de2e1181f Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 3 Apr 2026 20:28:03 +0200 Subject: [PATCH] chore: some more plugin api features --- pumpkin-plugin-wit/v0.1.0/player.wit | 16 +++ pumpkin-plugin-wit/v0.1.0/world.wit | 29 +++- pumpkin/src/entity/hunger.rs | 17 +++ pumpkin/src/entity/player.rs | 109 +++++++++++++++ .../wasm/wasm_host/wit/v0_1_0/player.rs | 127 ++++++++++++++++++ .../loader/wasm/wasm_host/wit/v0_1_0/world.rs | 83 +++++++++++- pumpkin/src/world/mod.rs | 41 +++++- 7 files changed, 418 insertions(+), 4 deletions(-) diff --git a/pumpkin-plugin-wit/v0.1.0/player.wit b/pumpkin-plugin-wit/v0.1.0/player.wit index f0c35b3fd..b8c478641 100644 --- a/pumpkin-plugin-wit/v0.1.0/player.wit +++ b/pumpkin-plugin-wit/v0.1.0/player.wit @@ -44,6 +44,10 @@ interface player { teleport: func (position: position, yaw: f32, pitch: f32) -> result<_, string>; teleport-world: func (world-ref: %world, position: position, yaw: option, pitch: option) -> result<_, string>; kick: func (message: text-component) -> result<_, string>; + respawn: func () -> result<_, string>; + + ban: func (reason: option) -> result<_, string>; + ban-ip: func (reason: option) -> result<_, string>; // Inventory get-selected-slot: func () -> result; @@ -52,9 +56,17 @@ interface player { // Status get-health: func () -> result; + set-health: func (health: f32) -> result<_, string>; get-max-health: func () -> result; + set-max-health: func (max-health: f32) -> result<_, string>; get-food-level: func () -> result; + set-food-level: func (food-level: u8) -> result<_, string>; get-saturation: func () -> result; + set-saturation: func (saturation: f32) -> result<_, string>; + get-exhaustion: func () -> result; + set-exhaustion: func (exhaustion: f32) -> result<_, string>; + get-absorption: func () -> result; + set-absorption: func (absorption: f32) -> result<_, string>; get-experience-level: func() -> result; get-experience-progress: func() -> result; @@ -66,12 +78,16 @@ interface player { add-experience-points: func(points: s32) -> result<_, string>; is-sneaking: func() -> result; + set-sneaking: func(sneaking: bool) -> result<_, string>; is-sprinting: func() -> result; + set-sprinting: func(sprinting: bool) -> result<_, string>; is-on-ground: func() -> result; is-flying: func() -> result; set-flying: func(flying: bool) -> result<_, string>; get-abilities: func() -> result; set-abilities: func(abilities: player-abilities) -> result<_, string>; + + get-ip: func() -> result; } } diff --git a/pumpkin-plugin-wit/v0.1.0/world.wit b/pumpkin-plugin-wit/v0.1.0/world.wit index aacd33632..a111622f3 100644 --- a/pumpkin-plugin-wit/v0.1.0/world.wit +++ b/pumpkin-plugin-wit/v0.1.0/world.wit @@ -1,4 +1,7 @@ interface %world { + use common.{position}; + use text.{text-component}; + enum piston-behavior { normal, destroy, @@ -39,11 +42,33 @@ interface %world { resource %world { get-id: func() -> string; - + get-block-state-id: func(pos: block-pos) -> u16; get-block-state: func(pos: block-pos) -> block-state; set-block-state: func(pos: block-pos, state: u16, update-flags: block-flags); + + get-time-of-day: func() -> u64; + + set-time-of-day: func(time: u64); + + get-world-age: func() -> u64; + + get-dimension: func() -> string; + + get-top-block-y: func(x: s32, z: s32) -> s32; + + get-motion-blocking-height: func(x: s32, z: s32) -> s32; + + is-raining: func() -> bool; + + set-raining: func(raining: bool); + + is-thundering: func() -> bool; + + set-thundering: func(thundering: bool); + + broadcast-system-message: func(message: text-component, overlay: bool); } -} \ No newline at end of file +} diff --git a/pumpkin/src/entity/hunger.rs b/pumpkin/src/entity/hunger.rs index 323630196..12d15d829 100644 --- a/pumpkin/src/entity/hunger.rs +++ b/pumpkin/src/entity/hunger.rs @@ -150,6 +150,23 @@ impl HungerManager { .store((current + saturation).min(f32::from(self.level.load()))); } + pub fn set_level(&self, level: u8) { + self.level.store(level.min(MAX_FOOD)); + } + + pub fn set_saturation(&self, saturation: f32) { + self.saturation + .store(saturation.min(f32::from(self.level.load()))); + } + + pub fn get_exhaustion(&self) -> f32 { + self.exhaustion.load() + } + + pub fn set_exhaustion(&self, exhaustion: f32) { + self.exhaustion.store(exhaustion.min(MAX_EXHAUSTION)); + } + pub fn restart(&self) { self.level.store(MAX_FOOD); self.saturation.store(5.0); diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 7f6f05fd5..4e93e06ea 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -83,6 +83,7 @@ use crate::block::blocks::bed::BedBlock; use crate::command::context::command_source::CommandSource; use crate::command::node::dispatcher::CommandDispatcher; use crate::command::{CommandSender, client_suggestions}; +use crate::data::SaveJSONConfiguration; use crate::entity::{EntityBaseFuture, NbtFuture, TeleportFuture}; use crate::net::{ClientPlatform, GameProfile}; use crate::net::{DisconnectReason, PlayerConfig}; @@ -2119,6 +2120,114 @@ impl Player { self.send_health().await; } + pub async fn set_max_health(&self, max_health: f32) { + self.living_entity.set_max_health(max_health).await; + self.send_health().await; + } + + pub async fn set_food_level(&self, food_level: u8) { + self.hunger_manager.set_level(food_level); + self.send_health().await; + } + + pub async fn set_saturation(&self, saturation: f32) { + self.hunger_manager.set_saturation(saturation); + self.send_health().await; + } + + pub fn get_exhaustion(&self) -> f32 { + self.hunger_manager.get_exhaustion() + } + + pub async fn set_exhaustion(&self, exhaustion: f32) { + self.hunger_manager.set_exhaustion(exhaustion); + self.send_health().await; + } + + pub fn get_absorption(&self) -> f32 { + self.living_entity.get_absorption() + } + + pub async fn set_absorption(&self, absorption: f32) { + self.living_entity.set_absorption(absorption).await; + } + + pub async fn get_ip(&self) -> String { + self.client.address().await.to_string() + } + + pub async fn respawn(self: &Arc) { + self.world().respawn_player(self, false).await; + } + + pub async fn ban(&self, server: &Server, reason: Option) { + let mut banned_players = server.data.banned_player_list.write().await; + let string_reason = reason + .clone() + .map_or_else(|| "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text); + + if banned_players + .banned_players + .iter() + .any(|entry| entry.uuid == self.gameprofile.id) + { + return; + } + + banned_players.banned_players.push( + crate::data::banlist_serializer::BannedPlayerEntry::new( + &self.gameprofile, + "Plugin".to_string(), + None, + string_reason, + ), + ); + + banned_players.save(); + drop(banned_players); + + let kick_reason = reason.unwrap_or_else(|| { + TextComponent::translate(translation::MULTIPLAYER_DISCONNECT_BANNED, []) + }); + + self.kick(DisconnectReason::Kicked, kick_reason).await; + } + + pub async fn ban_ip(&self, server: &Server, reason: Option) { + let mut banned_ips = server.data.banned_ip_list.write().await; + let string_reason = reason + .clone() + .map_or_else(|| "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text); + let target_ip = self.client.address().await.ip(); + + if banned_ips.get_entry(&target_ip).is_some() { + return; + } + + banned_ips + .banned_ips + .push(crate::data::banlist_serializer::BannedIpEntry::new( + target_ip, + "Plugin".to_string(), + None, + string_reason, + )); + + banned_ips.save(); + drop(banned_ips); + + let kick_reason = reason.unwrap_or_else(|| { + TextComponent::translate(translation::MULTIPLAYER_DISCONNECT_IP_BANNED, []) + }); + + let affected = server.get_players_by_ip(target_ip).await; + for target in affected { + target + .kick(DisconnectReason::Kicked, kick_reason.clone()) + .await; + } + } + pub fn tick_client_load_timeout(&self) { if !self.client_loaded.load(Ordering::Relaxed) { let timeout = self.client_loaded_timeout.load(Ordering::Relaxed); diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs index 8a5ca8e8d..4f2fe6a76 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/player.rs @@ -331,6 +331,46 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(()) } + async fn respawn(&mut self, player: Resource) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.respawn().await; + Ok(()) + } + + async fn ban( + &mut self, + player: Resource, + reason: Option>, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + let Some(server) = self.server.as_ref() else { + return Err("server not available".to_string()); + }; + let reason = match reason { + Some(r) => Some(text_component_from_resource(self, &r)?), + None => None, + }; + player.ban(server, reason).await; + Ok(()) + } + + async fn ban_ip( + &mut self, + player: Resource, + reason: Option>, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + let Some(server) = self.server.as_ref() else { + return Err("server not available".to_string()); + }; + let reason = match reason { + Some(r) => Some(text_component_from_resource(self, &r)?), + None => None, + }; + player.ban_ip(server, reason).await; + Ok(()) + } + async fn get_selected_slot(&mut self, player: Resource) -> Result { let player = player_from_resource(self, &player)?; Ok(player.inventory.get_selected_slot()) @@ -371,21 +411,83 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(player.living_entity.health.load()) } + async fn set_health(&mut self, player: Resource, health: f32) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.set_health(health).await; + Ok(()) + } + async fn get_max_health(&mut self, player: Resource) -> Result { let player = player_from_resource(self, &player)?; Ok(player.living_entity.get_max_health()) } + async fn set_max_health( + &mut self, + player: Resource, + max_health: f32, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.set_max_health(max_health).await; + Ok(()) + } + async fn get_food_level(&mut self, player: Resource) -> Result { let player = player_from_resource(self, &player)?; Ok(player.hunger_manager.level.load()) } + async fn set_food_level(&mut self, player: Resource, level: u8) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.set_food_level(level).await; + Ok(()) + } + async fn get_saturation(&mut self, player: Resource) -> Result { let player = player_from_resource(self, &player)?; Ok(player.hunger_manager.saturation.load()) } + async fn set_saturation( + &mut self, + player: Resource, + saturation: f32, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.set_saturation(saturation).await; + Ok(()) + } + + async fn get_exhaustion(&mut self, player: Resource) -> Result { + let player = player_from_resource(self, &player)?; + Ok(player.get_exhaustion()) + } + + async fn set_exhaustion( + &mut self, + player: Resource, + exhaustion: f32, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.set_exhaustion(exhaustion).await; + Ok(()) + } + + async fn get_absorption(&mut self, player: Resource) -> Result { + let player = player_from_resource(self, &player)?; + Ok(player.get_absorption()) + } + + async fn set_absorption( + &mut self, + player: Resource, + absorption: f32, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.set_absorption(absorption).await; + Ok(()) + } + async fn get_experience_level(&mut self, player: Resource) -> Result { let player = player_from_resource(self, &player)?; Ok(player.experience_level.load(Ordering::Relaxed)) @@ -468,11 +570,31 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(player.get_entity().sneaking.load(Ordering::Relaxed)) } + async fn set_sneaking( + &mut self, + player: Resource, + sneaking: bool, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.get_entity().set_sneaking(sneaking).await; + Ok(()) + } + async fn is_sprinting(&mut self, player: Resource) -> Result { let player = player_from_resource(self, &player)?; Ok(player.get_entity().sprinting.load(Ordering::Relaxed)) } + async fn set_sprinting( + &mut self, + player: Resource, + sprinting: bool, + ) -> Result<(), String> { + let player = player_from_resource(self, &player)?; + player.get_entity().set_sprinting(sprinting).await; + Ok(()) + } + async fn is_on_ground(&mut self, player: Resource) -> Result { let player = player_from_resource(self, &player)?; Ok(player.get_entity().on_ground.load(Ordering::Relaxed)) @@ -530,6 +652,11 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(()) } + async fn get_ip(&mut self, player: Resource) -> Result { + let player = player_from_resource(self, &player)?; + Ok(player.get_ip().await) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { let _ = self .resource_table diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs index 03d7556e2..076d5aede 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs @@ -3,16 +3,29 @@ use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockFlags; use wasmtime::component::Resource; +use pumpkin_world::world::SimpleWorld; + use crate::plugin::loader::wasm::wasm_host::wit::v0_1_0::pumpkin::plugin::world::{ BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState, PistonBehavior as WitPistonBehavior, }; use crate::plugin::loader::wasm::wasm_host::{ DowncastResourceExt, - state::{PluginHostState, WorldResource}, + state::{PluginHostState, TextComponentResource, WorldResource}, wit::v0_1_0::pumpkin::{self, plugin::world::World}, }; +fn text_component_from_resource( + state: &PluginHostState, + text: &Resource, +) -> Result { + state + .resource_table + .get::(&Resource::new_own(text.rep())) + .map_err(|_| "invalid text-component resource handle".to_string()) + .map(|resource| resource.provider.clone()) +} + impl DowncastResourceExt for Resource { fn downcast_ref<'a>(&'a self, state: &'a mut PluginHostState) -> &'a WorldResource { state @@ -126,10 +139,78 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { // Update the world world_ref .provider + .clone() .set_block_state(&internal_pos, state, internal_flags) .await; } + async fn get_time_of_day(&mut self, world: Resource) -> u64 { + let world_ref = world.downcast_ref(self); + world_ref.provider.get_time_of_day().await as u64 + } + + async fn set_time_of_day(&mut self, world: Resource, time: u64) { + let world_ref = world.downcast_ref(self); + world_ref.provider.set_time_of_day(time as i64).await; + } + + async fn get_world_age(&mut self, world: Resource) -> u64 { + let world_ref = world.downcast_ref(self); + world_ref.provider.get_world_age().await as u64 + } + + async fn get_dimension(&mut self, world: Resource) -> String { + let world_ref = world.downcast_ref(self); + world_ref.provider.dimension.minecraft_name.to_string() + } + + async fn get_top_block_y(&mut self, world: Resource, x: i32, z: i32) -> i32 { + let world_ref = world.downcast_ref(self); + world_ref + .provider + .get_top_block(pumpkin_util::math::vector2::Vector2::new(x, z)) + .await + } + + async fn get_motion_blocking_height(&mut self, world: Resource, x: i32, z: i32) -> i32 { + let world_ref = world.downcast_ref(self); + world_ref.provider.get_motion_blocking_height(x, z).await + } + + async fn is_raining(&mut self, world: Resource) -> bool { + let world_ref = world.downcast_ref(self); + world_ref.provider.is_raining().await + } + + async fn set_raining(&mut self, world: Resource, raining: bool) { + let world_ref = world.downcast_ref(self); + world_ref.provider.set_raining(raining).await; + } + + async fn is_thundering(&mut self, world: Resource) -> bool { + let world_ref = world.downcast_ref(self); + world_ref.provider.is_thundering().await + } + + async fn set_thundering(&mut self, world: Resource, thundering: bool) { + let world_ref = world.downcast_ref(self); + world_ref.provider.set_thundering(thundering).await; + } + + async fn broadcast_system_message( + &mut self, + world: Resource, + message: Resource, + overlay: bool, + ) { + let message = text_component_from_resource(self, &message).unwrap(); + let world_ref = world.downcast_ref(self); + world_ref + .provider + .broadcast_system_message(&message, overlay) + .await; + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { let _ = self .resource_table diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 79f66fbd2..812b84b3c 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -67,7 +67,7 @@ use pumpkin_protocol::bedrock::client::set_actor_data::{ }; use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData}; use pumpkin_protocol::bedrock::frame_set::FrameSet; -use pumpkin_protocol::java::client::play::CPlayerSpawnPosition; +use pumpkin_protocol::java::client::play::{CPlayerSpawnPosition, CSystemChatMessage}; use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata}; use pumpkin_protocol::{ BClientPacket, ClientPacket, IdOr, SoundEvent, @@ -486,6 +486,11 @@ impl World { Self::broadcast_java_grouped(packet, recipients_by_version).await; } + pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) { + let packet = CSystemChatMessage::new(message, overlay); + self.broadcast_packet_all(&packet).await; + } + pub async fn broadcast_message( &self, message: &TextComponent, @@ -1396,6 +1401,40 @@ impl World { spawn_for_chunk(self, chunk_pos, chunk, spawn_state, spawn_list).await; } + pub async fn set_time_of_day(&self, time: i64) { + let mut level_time = self.level_time.lock().await; + level_time.set_time(time); + level_time.send_time(self).await; + } + + pub async fn is_raining(&self) -> bool { + self.weather.lock().await.raining + } + + pub async fn set_raining(&self, raining: bool) { + let mut weather = self.weather.lock().await; + if weather.raining != raining { + let thunder = weather.thundering; + weather + .set_weather_parameters(self, 0, 0, raining, thunder) + .await; + } + } + + pub async fn is_thundering(&self) -> bool { + self.weather.lock().await.thundering + } + + pub async fn set_thundering(&self, thundering: bool) { + let mut weather = self.weather.lock().await; + if weather.thundering != thundering { + let raining = weather.raining; + weather + .set_weather_parameters(self, 0, 0, raining, thundering) + .await; + } + } + /// Gets the y position of the first non air block from the top down pub async fn get_top_block(&self, position: Vector2) -> i32 { for y in (self.dimension.min_y..self.dimension.height).rev() {