From 7c3177b4046fb3fa721e07ad2f8e1787c379f369 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 4 Aug 2026 20:54:55 +0200 Subject: [PATCH] feat: some more plugin api functions --- pumpkin-codegen/src/wit/packet_mapping.rs | 6 + pumpkin-data/src/lib.rs | 5 + pumpkin-plugin-api/src/ai.rs | 39 ++ pumpkin-plugin-api/src/lib.rs | 44 ++ pumpkin-plugin-wit | 2 +- pumpkin/src/entity/ai/goal/goal_selector.rs | 11 + pumpkin/src/entity/breath.rs | 14 + pumpkin/src/entity/hunger.rs | 2 +- pumpkin/src/entity/mob/enderman.rs | 16 + pumpkin/src/entity/mob/mod.rs | 43 ++ pumpkin/src/entity/mod.rs | 40 ++ pumpkin/src/entity/player.rs | 243 +++++++++++ pumpkin/src/net/java/play.rs | 12 + .../api/events/entity/entity_air_change.rs | 23 ++ .../plugin/api/events/entity/entity_breed.rs | 27 ++ .../plugin/api/events/entity/entity_dye.rs | 50 +++ .../api/events/entity/entity_explode.rs | 28 ++ .../plugin/api/events/entity/entity_mount.rs | 23 ++ .../plugin/api/events/entity/entity_tame.rs | 25 ++ .../plugin/api/events/entity/entity_target.rs | 23 ++ .../api/events/entity/entity_teleport.rs | 32 ++ pumpkin/src/plugin/api/events/entity/mod.rs | 16 + .../src/plugin/api/events/inventory/brew.rs | 24 ++ .../api/events/inventory/furnace_burn.rs | 28 ++ .../src/plugin/api/events/inventory/mod.rs | 8 + .../api/events/inventory/prepare_anvil.rs | 27 ++ .../api/events/inventory/trade_select.rs | 25 ++ pumpkin/src/plugin/api/events/player/mod.rs | 10 + .../api/events/player/player_animation.rs | 33 ++ .../api/events/player/player_item_break.rs | 20 + .../plugin/api/events/player/player_kick.rs | 25 ++ .../api/events/player/player_level_change.rs | 27 ++ .../api/events/player/player_swap_hands.rs | 21 + .../plugin/api/events/world/generic_game.rs | 24 ++ .../api/events/world/lightning_strike.rs | 24 ++ pumpkin/src/plugin/api/events/world/mod.rs | 10 + .../plugin/api/events/world/portal_create.rs | 32 ++ .../plugin/api/events/world/structure_grow.rs | 45 ++ .../src/plugin/api/events/world/world_init.rs | 17 + pumpkin/src/plugin/api/mod.rs | 4 + pumpkin/src/plugin/api/tab_list.rs | 85 ++++ pumpkin/src/plugin/api/title.rs | 93 +++++ .../loader/wasm/wasm_host/wit/v0_1/entity.rs | 294 +++++++++++++ .../wasm_host/wit/v0_1/generated_packets.rs | 4 +- .../loader/wasm/wasm_host/wit/v0_1/mod.rs | 1 + .../loader/wasm/wasm_host/wit/v0_1/player.rs | 389 ++++++++++++++++++ .../wasm/wasm_host/wit/v0_1/status_effect.rs | 242 +++++++++++ .../loader/wasm/wasm_host/wit/v0_1/world.rs | 38 ++ pumpkin/src/server/mod.rs | 6 + pumpkin/src/world/mod.rs | 12 + 50 files changed, 2288 insertions(+), 4 deletions(-) create mode 100644 pumpkin-plugin-api/src/ai.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_air_change.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_breed.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_dye.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_explode.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_mount.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_tame.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_target.rs create mode 100644 pumpkin/src/plugin/api/events/entity/entity_teleport.rs create mode 100644 pumpkin/src/plugin/api/events/inventory/brew.rs create mode 100644 pumpkin/src/plugin/api/events/inventory/furnace_burn.rs create mode 100644 pumpkin/src/plugin/api/events/inventory/prepare_anvil.rs create mode 100644 pumpkin/src/plugin/api/events/inventory/trade_select.rs create mode 100644 pumpkin/src/plugin/api/events/player/player_animation.rs create mode 100644 pumpkin/src/plugin/api/events/player/player_item_break.rs create mode 100644 pumpkin/src/plugin/api/events/player/player_kick.rs create mode 100644 pumpkin/src/plugin/api/events/player/player_level_change.rs create mode 100644 pumpkin/src/plugin/api/events/player/player_swap_hands.rs create mode 100644 pumpkin/src/plugin/api/events/world/generic_game.rs create mode 100644 pumpkin/src/plugin/api/events/world/lightning_strike.rs create mode 100644 pumpkin/src/plugin/api/events/world/portal_create.rs create mode 100644 pumpkin/src/plugin/api/events/world/structure_grow.rs create mode 100644 pumpkin/src/plugin/api/events/world/world_init.rs create mode 100644 pumpkin/src/plugin/api/tab_list.rs create mode 100644 pumpkin/src/plugin/api/title.rs create mode 100644 pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/status_effect.rs diff --git a/pumpkin-codegen/src/wit/packet_mapping.rs b/pumpkin-codegen/src/wit/packet_mapping.rs index 88d37a695..2f3897b6c 100644 --- a/pumpkin-codegen/src/wit/packet_mapping.rs +++ b/pumpkin-codegen/src/wit/packet_mapping.rs @@ -421,6 +421,12 @@ fn convert_value( } else { format!("vec_{}", tmp) } + } else if is_ref { + prep.push_str(&format!( + "{}let vec_{}: Vec<_> = {}.iter().map(|v| *v as _).collect();\n", + prep_prefix, tmp, src + )); + format!("&vec_{}", tmp) } else { format!("{}.iter().map(|v| *v as _).collect()", src) } diff --git a/pumpkin-data/src/lib.rs b/pumpkin-data/src/lib.rs index 19f162fcf..9ebbb2ba8 100644 --- a/pumpkin-data/src/lib.rs +++ b/pumpkin-data/src/lib.rs @@ -294,6 +294,11 @@ pub mod fuels; #[path = "generated/effect.rs"] pub mod effect; +#[cfg(feature = "effect")] +#[rustfmt::skip] +#[path = "generated/status_effect.rs"] +pub mod status_effect; + #[cfg(feature = "structures")] #[rustfmt::skip] #[path = "generated/structures.rs"] diff --git a/pumpkin-plugin-api/src/ai.rs b/pumpkin-plugin-api/src/ai.rs new file mode 100644 index 000000000..5132890e2 --- /dev/null +++ b/pumpkin-plugin-api/src/ai.rs @@ -0,0 +1,39 @@ +use crate::wit::pumpkin::plugin::context::Server; +use crate::wit::pumpkin::plugin::world::Entity; +use std::collections::BTreeMap; +use std::sync::Mutex; + +#[allow(unused_variables)] +pub trait AiGoal: Send + Sync { + fn can_start(&mut self, server: Server, entity: Entity) -> bool { + false + } + fn should_continue(&mut self, server: Server, entity: Entity) -> bool { + false + } + fn start(&mut self, server: Server, entity: Entity) {} + fn tick(&mut self, server: Server, entity: Entity) {} + fn stop(&mut self, server: Server, entity: Entity) {} +} + +pub(crate) static AI_GOAL_HANDLERS: Mutex = Mutex::new(LazyAiGoalHandlers { + handlers: BTreeMap::new(), + next_id: 0, +}); + +#[allow(dead_code)] +pub(crate) struct LazyAiGoalHandlers { + pub handlers: BTreeMap>, + pub next_id: u32, +} + +#[allow(dead_code)] +impl LazyAiGoalHandlers { + #[must_use] + pub fn register(&mut self, goal: Box) -> u32 { + let id = self.next_id; + self.next_id += 1; + self.handlers.insert(id, goal); + id + } +} diff --git a/pumpkin-plugin-api/src/lib.rs b/pumpkin-plugin-api/src/lib.rs index 5cf59ef26..ebc0684df 100644 --- a/pumpkin-plugin-api/src/lib.rs +++ b/pumpkin-plugin-api/src/lib.rs @@ -165,6 +165,49 @@ impl wit::Guest for Component { let mut handlers = TASK_HANDLERS.lock().unwrap(); handlers.handle(handler_id, server); } + + fn handle_ai_goal_can_start(goal_id: u32, server: Server, entity: entity::Entity) -> bool { + let mut handlers = crate::ai::AI_GOAL_HANDLERS.lock().unwrap(); + if let Some(goal) = handlers.handlers.get_mut(&goal_id) { + goal.can_start(server, entity) + } else { + false + } + } + + fn handle_ai_goal_should_continue( + goal_id: u32, + server: Server, + entity: entity::Entity, + ) -> bool { + let mut handlers = crate::ai::AI_GOAL_HANDLERS.lock().unwrap(); + if let Some(goal) = handlers.handlers.get_mut(&goal_id) { + goal.should_continue(server, entity) + } else { + false + } + } + + fn handle_ai_goal_start(goal_id: u32, server: Server, entity: entity::Entity) { + let mut handlers = crate::ai::AI_GOAL_HANDLERS.lock().unwrap(); + if let Some(goal) = handlers.handlers.get_mut(&goal_id) { + goal.start(server, entity); + } + } + + fn handle_ai_goal_tick(goal_id: u32, server: Server, entity: entity::Entity) { + let mut handlers = crate::ai::AI_GOAL_HANDLERS.lock().unwrap(); + if let Some(goal) = handlers.handlers.get_mut(&goal_id) { + goal.tick(server, entity); + } + } + + fn handle_ai_goal_stop(goal_id: u32, server: Server, entity: entity::Entity) { + let mut handlers = crate::ai::AI_GOAL_HANDLERS.lock().unwrap(); + if let Some(goal) = handlers.handlers.get_mut(&goal_id) { + goal.stop(server, entity); + } + } } /// Convenience alias for `core::result::Result` used throughout the plugin API. @@ -237,3 +280,4 @@ macro_rules! register_plugin { } }; } +pub mod ai; diff --git a/pumpkin-plugin-wit b/pumpkin-plugin-wit index ff6acb80b..ab193d116 160000 --- a/pumpkin-plugin-wit +++ b/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit ff6acb80b159b567c087a835f53f205f6755e403 +Subproject commit ab193d1160b90c446fb35d48e6c636d8f867c473 diff --git a/pumpkin/src/entity/ai/goal/goal_selector.rs b/pumpkin/src/entity/ai/goal/goal_selector.rs index 39b022799..b534ca357 100644 --- a/pumpkin/src/entity/ai/goal/goal_selector.rs +++ b/pumpkin/src/entity/ai/goal/goal_selector.rs @@ -51,6 +51,17 @@ impl GoalSelector { } } + pub fn clear(&mut self) -> Vec { + let mut running = Vec::new(); + for goal in self.goals.drain(..) { + if goal.running { + running.push(goal); + } + } + self.goals_by_control = [usize::MAX; 4]; + running + } + fn uses_any(prioritized_goal: &PrioritizedGoal, controls: Controls) -> bool { let goal_controls = prioritized_goal.controls(); for control in Controls::ITER { diff --git a/pumpkin/src/entity/breath.rs b/pumpkin/src/entity/breath.rs index b2d4eae9d..f0b3f6fdc 100644 --- a/pumpkin/src/entity/breath.rs +++ b/pumpkin/src/entity/breath.rs @@ -71,6 +71,20 @@ impl BreathManager { let new_air = (prev - AIR_DEPLETION_RATE).max(0); if new_air != prev { self.air_supply.store(new_air, Ordering::Relaxed); + if let Some(server) = player.world().server.upgrade() { + let mut event = crate::plugin::api::events::entity::entity_air_change::EntityAirChangeEvent::new( + player.entity_id(), + new_air, + ); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(server.plugin_manager.fire(&server, &mut event)); + }); + if event.cancelled { + self.air_supply.store(prev, Ordering::Relaxed); + return; + } + } self.send_air_supply(player); } diff --git a/pumpkin/src/entity/hunger.rs b/pumpkin/src/entity/hunger.rs index 12d15d829..635d38316 100644 --- a/pumpkin/src/entity/hunger.rs +++ b/pumpkin/src/entity/hunger.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use super::{EntityBase, NBTStorage, NBTStorageInit, player::Player}; +use super::{NBTStorage, NBTStorageInit, player::Player}; use crate::entity::NbtFuture; use crossbeam::atomic::AtomicCell; use pumpkin_data::damage::DamageType; diff --git a/pumpkin/src/entity/mob/enderman.rs b/pumpkin/src/entity/mob/enderman.rs index 54cb63a6e..3ff3f8717 100644 --- a/pumpkin/src/entity/mob/enderman.rs +++ b/pumpkin/src/entity/mob/enderman.rs @@ -216,6 +216,22 @@ impl EndermanEntity { world.play_sound(Sound::EntityEndermanTeleport, SoundCategory::Hostile, pos); } + if let Some(server) = world.server.upgrade() { + let mut event = + crate::plugin::api::events::entity::entity_teleport::EntityTeleportEvent::new( + entity.entity_id, + origin, + new_pos, + ); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(server.plugin_manager.fire(&server, &mut event)); + }); + if event.cancelled { + return false; + } + } + entity.set_pos(new_pos); let chunk_pos = entity.chunk_pos.load(); world.broadcast_to_chunk( diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index 36af954a6..97281ce12 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -167,6 +167,45 @@ impl MobEntity { (self.mob_flags.load(Relaxed) & Self::AI_DISABLED_FLAG) != 0 } + pub async fn clear_ai_goals(&self, mob: &dyn Mob) { + let running_goals = self.goals_selector.lock().unwrap().clear(); + for mut goal in running_goals { + goal.goal.stop(mob).await; + } + + let running_target_goals = self.target_selector.lock().unwrap().clear(); + for mut goal in running_target_goals { + goal.goal.stop(mob).await; + } + } + + pub fn add_goal(&self, priority: u8, goal: G) { + self.goals_selector + .lock() + .unwrap() + .add_goal(priority, Box::new(goal)); + } + + pub fn add_target_goal( + &self, + priority: u8, + goal: G, + ) { + self.target_selector + .lock() + .unwrap() + .add_goal(priority, Box::new(goal)); + } + + pub async fn set_target(&self, target: Option>) { + let mut t = self.target.lock().await; + *t = target; + } + + pub async fn get_target(&self) -> Option> { + self.target.lock().await.clone() + } + fn set_mob_flag(&self, flag: u8, value: bool) { let old_b = self.mob_flags.load(Ordering::Relaxed); @@ -560,6 +599,10 @@ pub trait Mob: EntityBase + Send + Sync { fn mob_set_variant_name(&self, _name: &str) {} } impl EntityBase for T { + fn get_mob(&self) -> Option<&dyn Mob> { + Some(self) + } + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { self.mob_init_data_tracker().await; diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index c984c7e17..c872e9f3d 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -226,6 +226,10 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { 0.0 } + fn get_mob(&self) -> Option<&dyn mob::Mob> { + None + } + fn tick_in_void<'a>(&'a self, _dyn_self: &'a dyn EntityBase) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.get_entity().remove().await }) } @@ -805,6 +809,8 @@ pub struct Entity { pub custom_name: ArcSwap>, /// Indicates whether the entity's custom name is visible pub custom_name_visible: AtomicBool, + pub silent: AtomicBool, + pub has_no_gravity: AtomicBool, /// Scoreboard tags attached to this entity, managed with `/tag`. /// Vanilla allows at most [`MAX_SCOREBOARD_TAGS`] tags per entity. pub scoreboard_tags: Mutex>, @@ -943,6 +949,8 @@ impl Entity { portal_manager: Mutex::new(None), custom_name: ArcSwap::new(Arc::new(None)), custom_name_visible: AtomicBool::new(false), + silent: AtomicBool::new(false), + has_no_gravity: AtomicBool::new(false), scoreboard_tags: Mutex::new(HashSet::new()), no_clip: AtomicBool::new(false), movement_multiplier: AtomicCell::new(Vector3::default()), @@ -1102,6 +1110,38 @@ impl Entity { ); } + pub fn is_silent(&self) -> bool { + self.silent.load(Ordering::Relaxed) + } + + pub fn set_silent(&self, silent: bool) { + self.silent.store(silent, Ordering::Relaxed); + self.send_meta_data( + &[Metadata::new( + TrackedData::SILENT, + MetaDataType::BOOLEAN, + silent, + )], + None, + ); + } + + pub fn has_no_gravity(&self) -> bool { + self.has_no_gravity.load(Ordering::Relaxed) + } + + pub fn set_has_no_gravity(&self, no_gravity: bool) { + self.has_no_gravity.store(no_gravity, Ordering::Relaxed); + self.send_meta_data( + &[Metadata::new( + TrackedData::NO_GRAVITY, + MetaDataType::BOOLEAN, + no_gravity, + )], + None, + ); + } + pub fn send_velocity(&self) { let velocity = self.velocity.load(); let chunk_pos = self.chunk_pos.load(); diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 323388273..cfbea8256 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -418,6 +418,12 @@ pub struct ItemCooldown { pub duration: i32, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PlayerWeather { + Clear, + Downfall, +} + pub struct Player { /// The underlying living entity object that represents the player. pub living_entity: LivingEntity, @@ -520,6 +526,11 @@ pub struct Player { pub tab_list_order: AtomicI32, pub tab_list_latency: AtomicI32, pub tab_list_listed: AtomicBool, + pub per_player_time: AtomicCell>, + pub per_player_weather: AtomicCell>, + pub compass_target: AtomicCell>, + pub respawn_location: AtomicCell>, + pub hidden_players: Mutex>, pub advancements: Arc>, pub enchantment_seed: AtomicI32, pub fishing_bobber: AtomicI32, @@ -753,11 +764,26 @@ impl Player { tab_list_order: AtomicI32::new(0), tab_list_latency: AtomicI32::new(0), tab_list_listed: AtomicBool::new(true), + per_player_time: AtomicCell::new(None), + per_player_weather: AtomicCell::new(None), + compass_target: AtomicCell::new(None), + respawn_location: AtomicCell::new(None), + hidden_players: Mutex::new(std::collections::HashSet::new()), fishing_bobber: AtomicI32::new(-1), bedrock_skin: ArcSwap::new(Arc::new(bedrock_skin)), } } + /// Sets the tab list header and footer for Java Edition clients. + /// + /// Note: Tab list header and footer formatting is a Java Edition-specific protocol feature + /// and is safely ignored on Bedrock Edition clients. + pub async fn set_tab_list(&self, tab_list: impl Into) { + let list = tab_list.into(); + self.set_tab_list_header_footer(list.header, list.footer) + .await; + } + 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(); @@ -1279,6 +1305,15 @@ impl Player { // Send the break status before clearing the slot so the client can // use the item texture for break particles. if result == pumpkin_data::item_stack::DamageResult::Broken { + if let Some(server) = self.world().server.upgrade() + && let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) + { + let mut event = crate::plugin::api::events::player::player_item_break::PlayerItemBreakEvent::new( + player_arc, + updated_stack.item.registry_key.to_string(), + ); + server.plugin_manager.fire(&server, &mut event).await; + } self.increment_stat( statistics::StatisticCategory::Broken, updated_stack.item.id as i32, @@ -2513,6 +2548,23 @@ impl Player { /// Sends the world time to only this player. pub async fn send_time(&self, world: &World) { + if let Some((custom_time, relative)) = self.per_player_time.load() { + let time_of_day = if relative { + let l_world = world.level_time.lock().await; + (l_world.time_of_day as u64 + custom_time) as i64 + } else { + custom_time as i64 + }; + let l_world = world.level_time.lock().await; + self.client + .enqueue_packet_editioned( + &CUpdateTime::new(l_world.world_age, time_of_day, true), + &CSetTime::new(time_of_day as _), + ) + .await; + return; + } + let l_world = world.level_time.lock().await; self.client .enqueue_packet_editioned( @@ -2522,6 +2574,94 @@ impl Player { .await; } + pub async fn set_player_time(&self, world: &World, time: u64, relative: bool) { + self.per_player_time.store(Some((time, relative))); + self.send_time(world).await; + } + + pub async fn reset_player_time(&self, world: &World) { + self.per_player_time.store(None); + self.send_time(world).await; + } + + pub fn get_player_time(&self) -> Option { + self.per_player_time.load().map(|(t, _)| t) + } + + pub fn is_player_time_relative(&self) -> bool { + self.per_player_time.load().is_none_or(|(_, r)| r) + } + + pub fn set_player_weather(&self, weather: PlayerWeather) { + self.per_player_weather.store(Some(weather)); + } + + pub fn reset_player_weather(&self) { + self.per_player_weather.store(None); + } + + pub fn get_player_weather(&self) -> Option { + self.per_player_weather.load() + } + + pub async fn set_compass_target(&self, pos: pumpkin_util::math::position::BlockPos) { + use pumpkin_protocol::java::client::play::CPlayerSpawnPosition; + self.compass_target.store(Some(pos)); + self.client + .enqueue_packet(&CPlayerSpawnPosition::new(pos, 0.0, 0.0, String::new())) + .await; + } + + pub fn get_compass_target(&self) -> Option { + self.compass_target.load() + } + + pub fn set_respawn_location(&self, pos: pumpkin_util::math::position::BlockPos) { + self.respawn_location.store(Some(pos)); + } + + pub fn get_respawn_location(&self) -> Option { + self.respawn_location.load() + } + + pub async fn hide_player(&self, other_id: uuid::Uuid) { + self.hidden_players.lock().await.insert(other_id); + } + + pub async fn show_player(&self, other_id: uuid::Uuid) { + self.hidden_players.lock().await.remove(&other_id); + } + + pub async fn can_see(&self, other_id: &uuid::Uuid) -> bool { + !self.hidden_players.lock().await.contains(other_id) + } + + pub async fn get_target_block( + &self, + world: &Arc, + max_distance: f64, + ) -> Option { + let (yaw, pitch) = ( + self.living_entity.entity.yaw.load(), + self.living_entity.entity.pitch.load(), + ); + let eye_pos = self.living_entity.entity.get_eye_pos(); + let yaw_rad = f64::from(yaw + 90.0).to_radians(); + let pitch_rad = f64::from(-pitch).to_radians(); + let dir = pumpkin_util::math::vector3::Vector3::new( + pitch_rad.cos() * yaw_rad.cos(), + pitch_rad.sin(), + pitch_rad.cos() * yaw_rad.sin(), + ); + let end_pos = eye_pos + dir * max_distance; + let res = world + .raycast(eye_pos, end_pos, async |pos, w| { + !w.get_block_state(pos).is_air() + }) + .await; + res.map(|(pos, _)| pos) + } + pub async fn unload_watched_chunks(&self, world: &World) { let radial_chunks = self.watched_section.load().all_chunks_within(); let level = &world.level; @@ -2723,6 +2863,18 @@ impl Player { } pub async fn kick(&self, reason: DisconnectReason, message: TextComponent) { + if let Some(server) = self.world().server.upgrade() + && let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) + { + let mut event = crate::plugin::api::events::player::player_kick::PlayerKickEvent::new( + player_arc, + message.clone().to_pretty_console(), + ); + server.plugin_manager.fire(&server, &mut event).await; + if event.cancelled { + return; + } + } self.client.kick(reason, message).await; } @@ -2749,6 +2901,30 @@ impl Player { self.send_health().await; } + pub async fn damage( + &self, + caller: &dyn crate::entity::EntityBase, + amount: f32, + damage_type: pumpkin_data::damage::DamageType, + ) -> bool { + self.living_entity.damage(caller, amount, damage_type).await + } + + pub async fn damage_generic(&self, amount: f32) -> bool { + use pumpkin_data::damage::DamageType; + self.living_entity + .damage(self, amount, DamageType::GENERIC) + .await + } + + pub async fn kill(&self) { + use pumpkin_data::damage::DamageType; + let health = self.living_entity.health.load(); + self.living_entity + .damage(self, health + 10.0, DamageType::OUT_OF_WORLD) + .await; + } + pub async fn send_health(&self) { if !self.has_client_loaded() { return; @@ -2800,16 +2976,49 @@ impl Player { self.send_health().await; } + pub fn get_food_level(&self) -> u8 { + self.hunger_manager.level.load() + } + pub async fn set_food_level(&self, food_level: u8) { self.hunger_manager.set_level(food_level); self.send_health().await; } + pub fn get_saturation(&self) -> f32 { + self.hunger_manager.saturation.load() + } + pub async fn set_saturation(&self, saturation: f32) { self.hunger_manager.set_saturation(saturation); self.send_health().await; } + pub async fn set_allow_flight(&self, allow: bool) { + self.abilities.lock().await.allow_flying = allow; + self.send_abilities_update().await; + } + + pub async fn set_flying(&self, flying: bool) { + self.abilities.lock().await.flying = flying; + self.send_abilities_update().await; + } + + pub async fn set_fly_speed(&self, speed: f32) { + self.abilities.lock().await.fly_speed = speed; + self.send_abilities_update().await; + } + + pub async fn set_walk_speed(&self, speed: f32) { + self.abilities.lock().await.walk_speed = speed; + self.send_abilities_update().await; + } + + pub async fn set_invulnerable(&self, invulnerable: bool) { + self.abilities.lock().await.invulnerable = invulnerable; + self.send_abilities_update().await; + } + pub fn get_exhaustion(&self) -> f32 { self.hunger_manager.get_exhaustion() } @@ -3258,6 +3467,15 @@ impl Player { } pub async fn swap_item(&self) { + if let Some(server) = self.world().server.upgrade() + && let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) + { + let mut event = crate::plugin::api::events::player::player_swap_hands::PlayerSwapHandItemsEvent::new(player_arc); + server.plugin_manager.fire(&server, &mut event).await; + if event.cancelled { + return; + } + } let (main_hand_item, off_hand_item) = self.inventory.swap_item().await; let equipment = &[ (EquipmentSlot::MAIN_HAND, main_hand_item), @@ -3386,6 +3604,18 @@ impl Player { /// Sets the player's experience level and notifies the client. pub async fn set_experience(&self, level: i32, progress: f32, points: i32) { + let old_level = self.experience_level.load(Ordering::Relaxed); + if old_level != level + && let Some(server) = self.world().server.upgrade() + && let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) + { + let mut event = crate::plugin::api::events::player::player_level_change::PlayerLevelChangeEvent::new( + player_arc, + old_level, + level, + ); + server.plugin_manager.fire(&server, &mut event).await; + } // TODO: These should be atomic together, not isolated; make a struct containing these. can cause ABA issues self.experience_level.store(level, Ordering::Relaxed); self.experience_progress.store(progress.clamp(0.0, 1.0)); @@ -3429,6 +3659,19 @@ impl Player { self.living_entity.add_effect(effect).await; } + pub async fn has_effect(&self, effect_type: &'static StatusEffect) -> bool { + self.living_entity.has_effect(effect_type).await + } + + pub async fn get_effect(&self, effect_type: &'static StatusEffect) -> Option { + self.living_entity.get_effect(effect_type).await + } + + pub async fn get_active_effects(&self) -> Vec { + let effects = self.living_entity.active_effects.lock().await; + effects.values().cloned().collect() + } + pub async fn send_active_effects(&self) { let effects = self.living_entity.active_effects.lock().await; for effect in effects.values() { diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 014dded15..cc7658b5f 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -1356,6 +1356,18 @@ impl JavaClient { return; }; + let mut anim_event = crate::plugin::api::events::player::player_animation::PlayerAnimationEvent::new( + player.clone(), + match hand { + Hand::Left => crate::plugin::api::events::player::player_animation::PlayerAnimationType::ArmSwingOff, + Hand::Right => crate::plugin::api::events::player::player_animation::PlayerAnimationType::ArmSwingMain, + }, + ); + server.plugin_manager.fire(server, &mut anim_event).await; + if anim_event.cancelled { + return; + } + let (yaw, pitch) = player.rotation(); let hit_result = player .world() diff --git a/pumpkin/src/plugin/api/events/entity/entity_air_change.rs b/pumpkin/src/plugin/api/events/entity/entity_air_change.rs new file mode 100644 index 000000000..abbd488f6 --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_air_change.rs @@ -0,0 +1,23 @@ +use pumpkin_macros::{Event, cancellable}; + +/// An event that occurs when an entity's remaining air supply changes. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityAirChangeEvent { + /// The ID of the entity. + pub entity_id: i32, + + /// The new air amount. + pub amount: i32, +} + +impl EntityAirChangeEvent { + #[must_use] + pub const fn new(entity_id: i32, amount: i32) -> Self { + Self { + entity_id, + amount, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/entity_breed.rs b/pumpkin/src/plugin/api/events/entity/entity_breed.rs new file mode 100644 index 000000000..e87dcf9ea --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_breed.rs @@ -0,0 +1,27 @@ +use pumpkin_macros::{Event, cancellable}; + +/// An event that occurs when two entities breed to create a child. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityBreedEvent { + /// The ID of the parent 1 entity. + pub father_id: i32, + + /// The ID of the parent 2 entity. + pub mother_id: i32, + + /// The ID of the child entity. + pub child_id: i32, +} + +impl EntityBreedEvent { + #[must_use] + pub const fn new(father_id: i32, mother_id: i32, child_id: i32) -> Self { + Self { + father_id, + mother_id, + child_id, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/entity_dye.rs b/pumpkin/src/plugin/api/events/entity/entity_dye.rs new file mode 100644 index 000000000..ccd87ef78 --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_dye.rs @@ -0,0 +1,50 @@ +use crate::entity::player::Player; +use pumpkin_macros::{Event, cancellable}; +use std::sync::Arc; + +/// The 16 colors available for dyeing entities. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DyeColor { + White, + Orange, + Magenta, + LightBlue, + Yellow, + Lime, + Pink, + Gray, + LightGray, + Cyan, + Purple, + Blue, + Brown, + Green, + Red, + Black, +} + +/// An event that occurs when an entity is dyed. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityDyeEvent { + /// The ID of the dyed entity. + pub entity_id: i32, + + /// The color applied. + pub color: DyeColor, + + /// The player dyeing the entity, if any. + pub player: Option>, +} + +impl EntityDyeEvent { + #[must_use] + pub const fn new(entity_id: i32, color: DyeColor, player: Option>) -> Self { + Self { + entity_id, + color, + player, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/entity_explode.rs b/pumpkin/src/plugin/api/events/entity/entity_explode.rs new file mode 100644 index 000000000..e4703019c --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_explode.rs @@ -0,0 +1,28 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::vector3::Vector3; + +/// An event that occurs when an entity explodes. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityExplodeEvent { + /// The ID of the exploding entity. + pub entity_id: i32, + + /// The position where the explosion occurred. + pub position: Vector3, + + /// The yield rate of blocks destroyed. + pub yield_rate: f32, +} + +impl EntityExplodeEvent { + #[must_use] + pub const fn new(entity_id: i32, position: Vector3, yield_rate: f32) -> Self { + Self { + entity_id, + position, + yield_rate, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/entity_mount.rs b/pumpkin/src/plugin/api/events/entity/entity_mount.rs new file mode 100644 index 000000000..760f5ce14 --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_mount.rs @@ -0,0 +1,23 @@ +use pumpkin_macros::{Event, cancellable}; + +/// An event that occurs when an entity mounts another entity. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityMountEvent { + /// The ID of the mounting entity. + pub entity_id: i32, + + /// The ID of the vehicle entity being mounted. + pub mount_id: i32, +} + +impl EntityMountEvent { + #[must_use] + pub const fn new(entity_id: i32, mount_id: i32) -> Self { + Self { + entity_id, + mount_id, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/entity_tame.rs b/pumpkin/src/plugin/api/events/entity/entity_tame.rs new file mode 100644 index 000000000..7764ef57d --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_tame.rs @@ -0,0 +1,25 @@ +use crate::entity::player::Player; +use pumpkin_macros::{Event, cancellable}; +use std::sync::Arc; + +/// An event that occurs when an entity is tamed by a player. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityTameEvent { + /// The ID of the tamed entity. + pub entity_id: i32, + + /// The player taming the entity. + pub owner: Arc, +} + +impl EntityTameEvent { + #[must_use] + pub const fn new(entity_id: i32, owner: Arc) -> Self { + Self { + entity_id, + owner, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/entity_target.rs b/pumpkin/src/plugin/api/events/entity/entity_target.rs new file mode 100644 index 000000000..2ec48b7c4 --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_target.rs @@ -0,0 +1,23 @@ +use pumpkin_macros::{Event, cancellable}; + +/// An event that occurs when an entity targets another entity. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityTargetEvent { + /// The ID of the targeting entity. + pub entity_id: i32, + + /// The ID of the target entity, if any. + pub target_id: Option, +} + +impl EntityTargetEvent { + #[must_use] + pub const fn new(entity_id: i32, target_id: Option) -> Self { + Self { + entity_id, + target_id, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/entity_teleport.rs b/pumpkin/src/plugin/api/events/entity/entity_teleport.rs new file mode 100644 index 000000000..0d0ef3253 --- /dev/null +++ b/pumpkin/src/plugin/api/events/entity/entity_teleport.rs @@ -0,0 +1,32 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::vector3::Vector3; + +/// An event that occurs when an entity teleports. +#[cancellable] +#[derive(Event, Clone)] +pub struct EntityTeleportEvent { + /// The entity ID. + pub entity_id: i32, + + /// The origin position. + pub from_position: Vector3, + + /// The destination position. + pub to_position: Vector3, +} + +impl EntityTeleportEvent { + #[must_use] + pub const fn new( + entity_id: i32, + from_position: Vector3, + to_position: Vector3, + ) -> Self { + Self { + entity_id, + from_position, + to_position, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/entity/mod.rs b/pumpkin/src/plugin/api/events/entity/mod.rs index 6c9dd8189..096553b4a 100644 --- a/pumpkin/src/plugin/api/events/entity/mod.rs +++ b/pumpkin/src/plugin/api/events/entity/mod.rs @@ -1,11 +1,27 @@ +pub mod entity_air_change; +pub mod entity_breed; pub mod entity_combust; pub mod entity_damage; pub mod entity_death; +pub mod entity_dye; +pub mod entity_explode; +pub mod entity_mount; pub mod entity_regain_health; pub mod entity_spawn; +pub mod entity_tame; +pub mod entity_target; +pub mod entity_teleport; +pub use entity_air_change::*; +pub use entity_breed::*; pub use entity_combust::*; pub use entity_damage::*; pub use entity_death::*; +pub use entity_dye::*; +pub use entity_explode::*; +pub use entity_mount::*; pub use entity_regain_health::*; pub use entity_spawn::*; +pub use entity_tame::*; +pub use entity_target::*; +pub use entity_teleport::*; diff --git a/pumpkin/src/plugin/api/events/inventory/brew.rs b/pumpkin/src/plugin/api/events/inventory/brew.rs new file mode 100644 index 000000000..0313d9167 --- /dev/null +++ b/pumpkin/src/plugin/api/events/inventory/brew.rs @@ -0,0 +1,24 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::position::BlockPos; + +/// An event that occurs when a brewing stand finishes brewing potion(s). +#[cancellable] +#[derive(Event, Clone)] +pub struct BrewEvent { + /// The block position of the brewing stand. + pub block_pos: BlockPos, + + /// The remaining fuel power. + pub fuel_level: u8, +} + +impl BrewEvent { + #[must_use] + pub const fn new(block_pos: BlockPos, fuel_level: u8) -> Self { + Self { + block_pos, + fuel_level, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/inventory/furnace_burn.rs b/pumpkin/src/plugin/api/events/inventory/furnace_burn.rs new file mode 100644 index 000000000..3b7321db7 --- /dev/null +++ b/pumpkin/src/plugin/api/events/inventory/furnace_burn.rs @@ -0,0 +1,28 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::position::BlockPos; + +/// An event that occurs when an item is consumed as fuel in a furnace. +#[cancellable] +#[derive(Event, Clone)] +pub struct FurnaceBurnEvent { + /// The position of the furnace block. + pub block_pos: BlockPos, + + /// The registry key of the fuel item. + pub fuel_item: String, + + /// The burn time in ticks. + pub burn_time: u32, +} + +impl FurnaceBurnEvent { + #[must_use] + pub const fn new(block_pos: BlockPos, fuel_item: String, burn_time: u32) -> Self { + Self { + block_pos, + fuel_item, + burn_time, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/inventory/mod.rs b/pumpkin/src/plugin/api/events/inventory/mod.rs index 24100c452..75dc92afc 100644 --- a/pumpkin/src/plugin/api/events/inventory/mod.rs +++ b/pumpkin/src/plugin/api/events/inventory/mod.rs @@ -1,9 +1,17 @@ +pub mod brew; pub mod craft_item; +pub mod furnace_burn; pub mod furnace_smelt; pub mod inventory_drag; pub mod inventory_open; +pub mod prepare_anvil; +pub mod trade_select; +pub use brew::*; pub use craft_item::*; +pub use furnace_burn::*; pub use furnace_smelt::*; pub use inventory_drag::*; pub use inventory_open::*; +pub use prepare_anvil::*; +pub use trade_select::*; diff --git a/pumpkin/src/plugin/api/events/inventory/prepare_anvil.rs b/pumpkin/src/plugin/api/events/inventory/prepare_anvil.rs new file mode 100644 index 000000000..74f2e68b8 --- /dev/null +++ b/pumpkin/src/plugin/api/events/inventory/prepare_anvil.rs @@ -0,0 +1,27 @@ +use crate::entity::player::Player; +use pumpkin_macros::Event; +use std::sync::Arc; + +/// An event that occurs when an item is prepared in an anvil. +#[derive(Event, Clone)] +pub struct PrepareAnvilEvent { + /// The player using the anvil. + pub player: Arc, + + /// The name prepared in the anvil input text field. + pub rename_text: String, + + /// The repair cost in experience levels. + pub repair_cost: u32, +} + +impl PrepareAnvilEvent { + #[must_use] + pub const fn new(player: Arc, rename_text: String, repair_cost: u32) -> Self { + Self { + player, + rename_text, + repair_cost, + } + } +} diff --git a/pumpkin/src/plugin/api/events/inventory/trade_select.rs b/pumpkin/src/plugin/api/events/inventory/trade_select.rs new file mode 100644 index 000000000..6638ac592 --- /dev/null +++ b/pumpkin/src/plugin/api/events/inventory/trade_select.rs @@ -0,0 +1,25 @@ +use crate::entity::player::Player; +use pumpkin_macros::{Event, cancellable}; +use std::sync::Arc; + +/// An event that occurs when a player selects a trade recipe with a merchant. +#[cancellable] +#[derive(Event, Clone)] +pub struct TradeSelectEvent { + /// The player selecting the trade. + pub player: Arc, + + /// The index of the selected trade slot. + pub slot_index: u8, +} + +impl TradeSelectEvent { + #[must_use] + pub const fn new(player: Arc, slot_index: u8) -> Self { + Self { + player, + slot_index, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/player/mod.rs b/pumpkin/src/plugin/api/events/player/mod.rs index e2b38c7ed..9e26b371c 100644 --- a/pumpkin/src/plugin/api/events/player/mod.rs +++ b/pumpkin/src/plugin/api/events/player/mod.rs @@ -7,6 +7,7 @@ pub mod fish; pub mod inventory_close; pub mod inventory_interact; pub mod item_held; +pub mod player_animation; pub mod player_bed; pub mod player_bucket; pub mod player_change_world; @@ -18,14 +19,18 @@ pub mod player_gamemode_change; pub mod player_interact_entity_event; pub mod player_interact_event; pub mod player_interact_unknown_entity_event; +pub mod player_item_break; pub mod player_item_consume; pub mod player_item_damage; pub mod player_join; +pub mod player_kick; pub mod player_leave; +pub mod player_level_change; pub mod player_login; pub mod player_move; pub mod player_permission_check; pub mod player_respawn; +pub mod player_swap_hands; pub mod player_teleport; pub mod player_toggle_flight_event; pub mod player_toggle_sneak_event; @@ -40,6 +45,7 @@ pub use fish::*; pub use inventory_close::*; pub use inventory_interact::*; pub use item_held::*; +pub use player_animation::*; pub use player_bed::*; pub use player_bucket::*; pub use player_change_world::*; @@ -51,14 +57,18 @@ pub use player_gamemode_change::*; pub use player_interact_entity_event::*; pub use player_interact_event::*; pub use player_interact_unknown_entity_event::*; +pub use player_item_break::*; pub use player_item_consume::*; pub use player_item_damage::*; pub use player_join::*; +pub use player_kick::*; pub use player_leave::*; +pub use player_level_change::*; pub use player_login::*; pub use player_move::*; pub use player_permission_check::*; pub use player_respawn::*; +pub use player_swap_hands::*; pub use player_teleport::*; pub use player_toggle_flight_event::*; pub use player_toggle_sneak_event::*; diff --git a/pumpkin/src/plugin/api/events/player/player_animation.rs b/pumpkin/src/plugin/api/events/player/player_animation.rs new file mode 100644 index 000000000..7b59604ad --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/player_animation.rs @@ -0,0 +1,33 @@ +use crate::entity::player::Player; +use pumpkin_macros::{Event, cancellable}; +use std::sync::Arc; + +/// The type of animation performed by a player. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlayerAnimationType { + ArmSwingMain, + ArmSwingOff, + LeaveBed, +} + +/// An event that occurs when a player performs an animation. +#[cancellable] +#[derive(Event, Clone)] +pub struct PlayerAnimationEvent { + /// The player performing the animation. + pub player: Arc, + + /// The type of animation. + pub animation_type: PlayerAnimationType, +} + +impl PlayerAnimationEvent { + #[must_use] + pub const fn new(player: Arc, animation_type: PlayerAnimationType) -> Self { + Self { + player, + animation_type, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/player/player_item_break.rs b/pumpkin/src/plugin/api/events/player/player_item_break.rs new file mode 100644 index 000000000..85d64ca4e --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/player_item_break.rs @@ -0,0 +1,20 @@ +use crate::entity::player::Player; +use pumpkin_macros::Event; +use std::sync::Arc; + +/// An event that occurs when a player breaks an item. +#[derive(Event, Clone)] +pub struct PlayerItemBreakEvent { + /// The player whose item broke. + pub player: Arc, + + /// The registry key of the broken item. + pub item_name: String, +} + +impl PlayerItemBreakEvent { + #[must_use] + pub const fn new(player: Arc, item_name: String) -> Self { + Self { player, item_name } + } +} diff --git a/pumpkin/src/plugin/api/events/player/player_kick.rs b/pumpkin/src/plugin/api/events/player/player_kick.rs new file mode 100644 index 000000000..833d67b81 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/player_kick.rs @@ -0,0 +1,25 @@ +use crate::entity::player::Player; +use pumpkin_macros::{Event, cancellable}; +use std::sync::Arc; + +/// An event that occurs when a player is kicked from the server. +#[cancellable] +#[derive(Event, Clone)] +pub struct PlayerKickEvent { + /// The player being kicked. + pub player: Arc, + + /// The reason for the kick. + pub reason: String, +} + +impl PlayerKickEvent { + #[must_use] + pub const fn new(player: Arc, reason: String) -> Self { + Self { + player, + reason, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/player/player_level_change.rs b/pumpkin/src/plugin/api/events/player/player_level_change.rs new file mode 100644 index 000000000..e998b02d6 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/player_level_change.rs @@ -0,0 +1,27 @@ +use crate::entity::player::Player; +use pumpkin_macros::Event; +use std::sync::Arc; + +/// An event that occurs when a player's experience level changes. +#[derive(Event, Clone)] +pub struct PlayerLevelChangeEvent { + /// The player whose level changed. + pub player: Arc, + + /// The old level. + pub old_level: i32, + + /// The new level. + pub new_level: i32, +} + +impl PlayerLevelChangeEvent { + #[must_use] + pub const fn new(player: Arc, old_level: i32, new_level: i32) -> Self { + Self { + player, + old_level, + new_level, + } + } +} diff --git a/pumpkin/src/plugin/api/events/player/player_swap_hands.rs b/pumpkin/src/plugin/api/events/player/player_swap_hands.rs new file mode 100644 index 000000000..00d883094 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/player_swap_hands.rs @@ -0,0 +1,21 @@ +use crate::entity::player::Player; +use pumpkin_macros::{Event, cancellable}; +use std::sync::Arc; + +/// An event that occurs when a player swaps items between main hand and offhand. +#[cancellable] +#[derive(Event, Clone)] +pub struct PlayerSwapHandItemsEvent { + /// The player swapping hand items. + pub player: Arc, +} + +impl PlayerSwapHandItemsEvent { + #[must_use] + pub const fn new(player: Arc) -> Self { + Self { + player, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/world/generic_game.rs b/pumpkin/src/plugin/api/events/world/generic_game.rs new file mode 100644 index 000000000..0e29e5e9a --- /dev/null +++ b/pumpkin/src/plugin/api/events/world/generic_game.rs @@ -0,0 +1,24 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::vector3::Vector3; + +/// An event that occurs when a generic game event is triggered in a world. +#[cancellable] +#[derive(Event, Clone)] +pub struct GenericGameEvent { + /// The key or type of the game event. + pub event_key: String, + + /// The position where the game event occurred. + pub position: Vector3, +} + +impl GenericGameEvent { + #[must_use] + pub const fn new(event_key: String, position: Vector3) -> Self { + Self { + event_key, + position, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/world/lightning_strike.rs b/pumpkin/src/plugin/api/events/world/lightning_strike.rs new file mode 100644 index 000000000..874ab612d --- /dev/null +++ b/pumpkin/src/plugin/api/events/world/lightning_strike.rs @@ -0,0 +1,24 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::vector3::Vector3; + +/// An event that occurs when a lightning bolt strikes in a world. +#[cancellable] +#[derive(Event, Clone)] +pub struct LightningStrikeEvent { + /// The location of the strike. + pub position: Vector3, + + /// Whether this strike was spawned by an effect. + pub is_effect: bool, +} + +impl LightningStrikeEvent { + #[must_use] + pub const fn new(position: Vector3, is_effect: bool) -> Self { + Self { + position, + is_effect, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/world/mod.rs b/pumpkin/src/plugin/api/events/world/mod.rs index 0fcc2b81d..74052f15d 100644 --- a/pumpkin/src/plugin/api/events/world/mod.rs +++ b/pumpkin/src/plugin/api/events/world/mod.rs @@ -1,13 +1,23 @@ pub mod chunk_load; pub mod chunk_save; pub mod chunk_send; +pub mod generic_game; +pub mod lightning_strike; +pub mod portal_create; pub mod spawn_change; +pub mod structure_grow; pub mod weather_change; +pub mod world_init; pub mod world_load; pub use chunk_load::*; pub use chunk_save::*; pub use chunk_send::*; +pub use generic_game::*; +pub use lightning_strike::*; +pub use portal_create::*; pub use spawn_change::*; +pub use structure_grow::*; pub use weather_change::*; +pub use world_init::*; pub use world_load::*; diff --git a/pumpkin/src/plugin/api/events/world/portal_create.rs b/pumpkin/src/plugin/api/events/world/portal_create.rs new file mode 100644 index 000000000..645c704d4 --- /dev/null +++ b/pumpkin/src/plugin/api/events/world/portal_create.rs @@ -0,0 +1,32 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::position::BlockPos; + +/// The type of portal created. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PortalType { + Nether, + End, + Custom, +} + +/// An event that occurs when a nether or end portal frame/block is created. +#[cancellable] +#[derive(Event, Clone)] +pub struct PortalCreateEvent { + /// The block position of the portal creation. + pub block_pos: BlockPos, + + /// The type of portal created. + pub portal_type: PortalType, +} + +impl PortalCreateEvent { + #[must_use] + pub const fn new(block_pos: BlockPos, portal_type: PortalType) -> Self { + Self { + block_pos, + portal_type, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/world/structure_grow.rs b/pumpkin/src/plugin/api/events/world/structure_grow.rs new file mode 100644 index 000000000..db81fb503 --- /dev/null +++ b/pumpkin/src/plugin/api/events/world/structure_grow.rs @@ -0,0 +1,45 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_util::math::position::BlockPos; + +/// The type of structure growing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TreeType { + Oak, + Spruce, + Birch, + Jungle, + Acacia, + DarkOak, + Mangrove, + Cherry, + Azalea, + BrownMushroom, + RedMushroom, + Custom, +} + +/// An event that occurs when a structure (like a tree or mushroom) grows. +#[cancellable] +#[derive(Event, Clone)] +pub struct StructureGrowEvent { + /// The origin block position where growth started. + pub block_pos: BlockPos, + + /// The type of structure growing. + pub species: TreeType, + + /// Whether bone meal was used. + pub bone_meal: bool, +} + +impl StructureGrowEvent { + #[must_use] + pub const fn new(block_pos: BlockPos, species: TreeType, bone_meal: bool) -> Self { + Self { + block_pos, + species, + bone_meal, + cancelled: false, + } + } +} diff --git a/pumpkin/src/plugin/api/events/world/world_init.rs b/pumpkin/src/plugin/api/events/world/world_init.rs new file mode 100644 index 000000000..dc0771cb7 --- /dev/null +++ b/pumpkin/src/plugin/api/events/world/world_init.rs @@ -0,0 +1,17 @@ +use crate::world::World; +use pumpkin_macros::Event; +use std::sync::Arc; + +/// An event that occurs when a world is initialized. +#[derive(Event, Clone)] +pub struct WorldInitEvent { + /// The world being initialized. + pub world: Arc, +} + +impl WorldInitEvent { + #[must_use] + pub const fn new(world: Arc) -> Self { + Self { world } + } +} diff --git a/pumpkin/src/plugin/api/mod.rs b/pumpkin/src/plugin/api/mod.rs index f8c17cd71..ec5654f6e 100644 --- a/pumpkin/src/plugin/api/mod.rs +++ b/pumpkin/src/plugin/api/mod.rs @@ -1,11 +1,15 @@ pub mod context; pub mod events; pub mod gui; +pub mod tab_list; +pub mod title; use std::{pin::Pin, sync::Arc}; pub use context::*; pub use events::*; +pub use tab_list::*; +pub use title::*; /// Struct representing metadata for a plugin. /// diff --git a/pumpkin/src/plugin/api/tab_list.rs b/pumpkin/src/plugin/api/tab_list.rs new file mode 100644 index 000000000..26b31f162 --- /dev/null +++ b/pumpkin/src/plugin/api/tab_list.rs @@ -0,0 +1,85 @@ +use pumpkin_util::text::TextComponent; + +/// Represents a Java Edition tab list configuration containing header and footer components. +#[derive(Debug, Clone)] +pub struct TabList { + /// The tab list header component. + pub header: TextComponent, + /// The tab list footer component. + pub footer: TextComponent, +} + +impl Default for TabList { + fn default() -> Self { + Self { + header: TextComponent::text(""), + footer: TextComponent::text(""), + } + } +} + +impl TabList { + /// Creates a new `TabListBuilder`. + #[must_use] + pub fn builder() -> TabListBuilder { + TabListBuilder::default() + } +} + +/// A fluent builder for `TabList`. +#[derive(Debug, Clone, Default)] +pub struct TabListBuilder { + header: Option, + footer: Option, +} + +impl TabListBuilder { + /// Creates a new `TabListBuilder`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the tab list header component. + #[must_use] + pub fn header(mut self, header: impl Into) -> Self { + self.header = Some(header.into()); + self + } + + /// Sets the tab list header from plain text. + #[must_use] + pub fn header_text(mut self, text: impl Into) -> Self { + self.header = Some(TextComponent::text(text.into())); + self + } + + /// Sets the tab list footer component. + #[must_use] + pub fn footer(mut self, footer: impl Into) -> Self { + self.footer = Some(footer.into()); + self + } + + /// Sets the tab list footer from plain text. + #[must_use] + pub fn footer_text(mut self, text: impl Into) -> Self { + self.footer = Some(TextComponent::text(text.into())); + self + } + + /// Builds the `TabList`. + #[must_use] + pub fn build(self) -> TabList { + TabList { + header: self.header.unwrap_or_else(|| TextComponent::text("")), + footer: self.footer.unwrap_or_else(|| TextComponent::text("")), + } + } +} + +impl From for TabList { + fn from(builder: TabListBuilder) -> Self { + builder.build() + } +} diff --git a/pumpkin/src/plugin/api/title.rs b/pumpkin/src/plugin/api/title.rs new file mode 100644 index 000000000..57f638729 --- /dev/null +++ b/pumpkin/src/plugin/api/title.rs @@ -0,0 +1,93 @@ +use crate::entity::player::{Player, TitleMode}; +use pumpkin_util::text::TextComponent; + +/// A fluent Builder for constructing and displaying titles, subtitles, and action bar text with animation timings. +#[derive(Debug, Clone, Default)] +pub struct TitleBuilder { + title: Option, + subtitle: Option, + actionbar: Option, + fade_in: Option, + stay: Option, + fade_out: Option, +} + +impl TitleBuilder { + /// Creates a new `TitleBuilder`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the main title text component. + #[must_use] + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + /// Sets the main title from plain text. + #[must_use] + pub fn title_text(mut self, text: impl Into) -> Self { + self.title = Some(TextComponent::text(text.into())); + self + } + + /// Sets the subtitle text component. + #[must_use] + pub fn subtitle(mut self, subtitle: impl Into) -> Self { + self.subtitle = Some(subtitle.into()); + self + } + + /// Sets the subtitle from plain text. + #[must_use] + pub fn subtitle_text(mut self, text: impl Into) -> Self { + self.subtitle = Some(TextComponent::text(text.into())); + self + } + + /// Sets the action bar text component. + #[must_use] + pub fn actionbar(mut self, actionbar: impl Into) -> Self { + self.actionbar = Some(actionbar.into()); + self + } + + /// Sets the action bar text from plain text. + #[must_use] + pub fn actionbar_text(mut self, text: impl Into) -> Self { + self.actionbar = Some(TextComponent::text(text.into())); + self + } + + /// Sets the fade-in, stay, and fade-out timings in ticks. + #[must_use] + pub const fn times(mut self, fade_in: i32, stay: i32, fade_out: i32) -> Self { + self.fade_in = Some(fade_in); + self.stay = Some(stay); + self.fade_out = Some(fade_out); + self + } + + /// Sends the title configuration to the specified player. + pub async fn send_to(&self, player: &Player) { + if let (Some(fade_in), Some(stay), Some(fade_out)) = + (self.fade_in, self.stay, self.fade_out) + { + player.send_title_animation(fade_in, stay, fade_out).await; + } + + if let Some(ref title) = self.title { + player.show_title(title, &TitleMode::Title).await; + } + + if let Some(ref subtitle) = self.subtitle { + player.show_title(subtitle, &TitleMode::SubTitle).await; + } + + if let Some(ref actionbar) = self.actionbar { + player.show_title(actionbar, &TitleMode::ActionBar).await; + } + } +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs index c1b24b968..b451df509 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs @@ -3,6 +3,9 @@ use wasmtime::component::Resource; use pumpkin_util::math::vector3::Vector3; +use crate::entity::ai::goal::{Goal, GoalFuture}; +use crate::entity::mob::Mob; +use crate::plugin::loader::wasm::wasm_host::{PluginInstance, WasmPlugin}; use crate::plugin::loader::wasm::wasm_host::{ state::{EntityResource, PluginHostState}, wit::v0_1::events::to_wasm_position, @@ -501,6 +504,32 @@ impl HostEntity for PluginHostState { Ok(()) } + async fn is_silent(&mut self, entity: Resource) -> wasmtime::Result { + let entity = entity_from_resource(self, &entity)?; + Ok(entity.get_entity().is_silent()) + } + + async fn set_silent(&mut self, entity: Resource, silent: bool) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + entity.get_entity().set_silent(silent); + Ok(()) + } + + async fn has_gravity(&mut self, entity: Resource) -> wasmtime::Result { + let entity = entity_from_resource(self, &entity)?; + Ok(!entity.get_entity().has_no_gravity()) + } + + async fn set_has_gravity( + &mut self, + entity: Resource, + gravity: bool, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + entity.get_entity().set_has_no_gravity(!gravity); + Ok(()) + } + async fn get_eye_height(&mut self, entity: Resource) -> wasmtime::Result { let entity = entity_from_resource(self, &entity)?; Ok(entity.get_entity().entity_dimension.load().eye_height) @@ -804,6 +833,118 @@ impl HostEntity for PluginHostState { Ok(()) } + async fn add_ai_goal( + &mut self, + entity: Resource, + priority: u8, + goal: crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + if let Some(mob) = entity.get_mob() { + let mob_entity = mob.get_mob_entity(); + match goal { + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::Swim => { + mob_entity.add_goal(priority, crate::entity::ai::goal::swim::SwimGoal::default()); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::WanderAround(speed) => { + mob_entity.add_goal(priority, crate::entity::ai::goal::wander_around::WanderAroundGoal::new(f64::from(speed))); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::MeleeAttack(speed) => { + mob_entity.add_goal(priority, crate::entity::ai::goal::melee_attack::MeleeAttackGoal::new(f64::from(speed), false)); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::LookAtPlayer(range) => { + mob_entity.add_goal(priority, crate::entity::ai::goal::look_at_entity::LookAtEntityGoal::new( + std::sync::Weak::::new() as std::sync::Weak, + &pumpkin_data::entity::EntityType::PLAYER, + range, + 0.02, + false, + )); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::LookAround => { + mob_entity.add_goal(priority, crate::entity::ai::goal::look_around::RandomLookAroundGoal::default()); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::EscapeDanger(speed) => { + mob_entity.add_goal(priority, *crate::entity::ai::goal::escape_danger::EscapeDangerGoal::new(f64::from(speed))); + } + _ => {} // Remaining goals + } + } + Ok(()) + } + + async fn add_custom_ai_goal( + &mut self, + entity: Resource, + priority: u8, + goal_id: u32, + ) -> wasmtime::Result<()> { + let plugin = self.plugin.as_ref().unwrap().upgrade().unwrap(); + let entity = entity_from_resource(self, &entity)?; + if let Some(mob) = entity.get_mob() { + let mob_entity = mob.get_mob_entity(); + mob_entity.add_goal(priority, CustomWasmGoal { plugin, goal_id }); + } + Ok(()) + } + + async fn clear_ai_goals(&mut self, entity: Resource) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity().clear_ai_goals(mob).await; + } + Ok(()) + } + + async fn set_ai_disabled( + &mut self, + entity: Resource, + disabled: bool, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity().set_no_ai(disabled); + } + Ok(()) + } + + async fn is_ai_disabled(&mut self, entity: Resource) -> wasmtime::Result { + let entity = entity_from_resource(self, &entity)?; + Ok(entity + .get_mob() + .is_none_or(|mob| mob.get_mob_entity().is_no_ai())) + } + + async fn set_target( + &mut self, + entity: Resource, + target: Option>, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + let target_entity = if let Some(t) = target { + Some(entity_from_resource(self, &t)?) + } else { + None + }; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity().set_target(target_entity).await; + } + Ok(()) + } + + async fn get_target( + &mut self, + entity: Resource, + ) -> wasmtime::Result>> { + let entity = entity_from_resource(self, &entity)?; + if let Some(mob) = entity.get_mob() + && let Some(target) = mob.get_mob_entity().get_target().await + { + return Ok(Some(self.add_entity(target)?)); + } + Ok(None) + } + async fn raycast( &mut self, entity: Resource, @@ -848,3 +989,156 @@ impl HostEntity for PluginHostState { Ok(()) } } + +pub struct CustomWasmGoal { + pub plugin: Arc, + pub goal_id: u32, +} + +impl Goal for CustomWasmGoal { + fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async { + let mut store = self.plugin.store.lock().await; + if let Some(entity_arc) = mob + .get_entity() + .world + .load() + .get_entity_by_id(mob.get_entity().entity_id) + { + match self.plugin.plugin_instance { + PluginInstance::V0_1(ref plugin) => { + let server = store.data_mut().server.clone().unwrap(); + let server_res = store.data_mut().add_server(server).unwrap(); + let entity_res = store.data_mut().add_entity(entity_arc).unwrap(); + plugin + .call_handle_ai_goal_can_start( + &mut *store, + self.goal_id, + server_res, + entity_res, + ) + .await + .unwrap_or(false) + } + } + } else { + false + } + }) + } + + fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async { + let mut store = self.plugin.store.lock().await; + if let Some(entity_arc) = mob + .get_entity() + .world + .load() + .get_entity_by_id(mob.get_entity().entity_id) + { + match self.plugin.plugin_instance { + PluginInstance::V0_1(ref plugin) => { + let server = store.data_mut().server.clone().unwrap(); + let server_res = store.data_mut().add_server(server).unwrap(); + let entity_res = store.data_mut().add_entity(entity_arc).unwrap(); + plugin + .call_handle_ai_goal_should_continue( + &mut *store, + self.goal_id, + server_res, + entity_res, + ) + .await + .unwrap_or(false) + } + } + } else { + false + } + }) + } + + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async { + let mut store = self.plugin.store.lock().await; + if let Some(entity_arc) = mob + .get_entity() + .world + .load() + .get_entity_by_id(mob.get_entity().entity_id) + { + match self.plugin.plugin_instance { + PluginInstance::V0_1(ref plugin) => { + let server = store.data_mut().server.clone().unwrap(); + let server_res = store.data_mut().add_server(server).unwrap(); + let entity_res = store.data_mut().add_entity(entity_arc).unwrap(); + let _ = plugin + .call_handle_ai_goal_start( + &mut *store, + self.goal_id, + server_res, + entity_res, + ) + .await; + } + } + } + }) + } + + fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async { + let mut store = self.plugin.store.lock().await; + if let Some(entity_arc) = mob + .get_entity() + .world + .load() + .get_entity_by_id(mob.get_entity().entity_id) + { + match self.plugin.plugin_instance { + PluginInstance::V0_1(ref plugin) => { + let server = store.data_mut().server.clone().unwrap(); + let server_res = store.data_mut().add_server(server).unwrap(); + let entity_res = store.data_mut().add_entity(entity_arc).unwrap(); + let _ = plugin + .call_handle_ai_goal_tick( + &mut *store, + self.goal_id, + server_res, + entity_res, + ) + .await; + } + } + } + }) + } + + fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async { + let mut store = self.plugin.store.lock().await; + if let Some(entity_arc) = mob + .get_entity() + .world + .load() + .get_entity_by_id(mob.get_entity().entity_id) + { + match self.plugin.plugin_instance { + PluginInstance::V0_1(ref plugin) => { + let server = store.data_mut().server.clone().unwrap(); + let server_res = store.data_mut().add_server(server).unwrap(); + let entity_res = store.data_mut().add_entity(entity_arc).unwrap(); + let _ = plugin + .call_handle_ai_goal_stop( + &mut *store, + self.goal_id, + server_res, + entity_res, + ) + .await; + } + } + } + }) + } +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs index bd93df64b..b93e10e1c 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/generated_packets.rs @@ -206,9 +206,9 @@ pub fn serialize_java_packet( Some(buf.into()) } ClientboundPacket::CDebugSample(data) => { - let sample_vec: Vec = data.sample.iter().map(|v| *v as i64).collect(); + let vec_sample: Vec<_> = data.sample.iter().map(|v| *v as _).collect(); let p = pumpkin_protocol::java::client::play::CDebugSample { - sample: &sample_vec, + sample: &vec_sample, sample_type: VarInt(data.sample_type), }; let mut buf = Vec::new(); diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs index 99fe17957..9f5096a70 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs @@ -28,6 +28,7 @@ pub mod recipe; pub mod scheduler; pub mod scoreboard; pub mod server; +pub mod status_effect; pub mod text; pub mod uuid; pub mod world; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 7199555d9..c41c332fd 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -868,6 +868,395 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(()) } + async fn add_effect( + &mut self, + player: Resource, + effect: pumpkin::plugin::status_effect::StatusEffectInstance, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let effect_type = super::status_effect::from_wasm_status_effect_type(effect.effect_type); + if let Some(status_effect) = + pumpkin_data::effect::StatusEffect::from_name(effect_type.to_name()) + { + let effect_obj = pumpkin_data::potion::Effect { + effect_type: status_effect, + duration: effect.duration as i32, + amplifier: effect.amplifier, + ambient: effect.ambient, + show_particles: effect.show_particles, + show_icon: effect.show_icon, + blend: false, + }; + player.add_effect(effect_obj).await; + } + Ok(()) + } + + async fn remove_effect( + &mut self, + player: Resource, + effect: pumpkin::plugin::status_effect::StatusEffectType, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let effect_type = super::status_effect::from_wasm_status_effect_type(effect); + if let Some(status_effect) = + pumpkin_data::effect::StatusEffect::from_name(effect_type.to_name()) + { + player.remove_effect(status_effect).await; + } + Ok(()) + } + + async fn clear_effects(&mut self, player: Resource) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.remove_all_effects().await; + Ok(()) + } + + async fn has_effect( + &mut self, + player: Resource, + effect: pumpkin::plugin::status_effect::StatusEffectType, + ) -> wasmtime::Result { + let player = player_from_resource(self, &player)?; + let effect_type = super::status_effect::from_wasm_status_effect_type(effect); + if let Some(status_effect) = + pumpkin_data::effect::StatusEffect::from_name(effect_type.to_name()) + { + Ok(player.has_effect(status_effect).await) + } else { + Ok(false) + } + } + + async fn get_effect( + &mut self, + player: Resource, + effect: pumpkin::plugin::status_effect::StatusEffectType, + ) -> wasmtime::Result> { + let player = player_from_resource(self, &player)?; + let effect_type = super::status_effect::from_wasm_status_effect_type(effect); + if let Some(status_effect) = + pumpkin_data::effect::StatusEffect::from_name(effect_type.to_name()) + && let Some(eff) = player.get_effect(status_effect).await + { + return Ok(super::status_effect::to_wasm_status_effect_instance(&eff)); + } + Ok(None) + } + + async fn get_active_effects( + &mut self, + player: Resource, + ) -> wasmtime::Result> { + let player = player_from_resource(self, &player)?; + let effects = player.get_active_effects().await; + let mut list = Vec::with_capacity(effects.len()); + for eff in &effects { + if let Some(instance) = super::status_effect::to_wasm_status_effect_instance(eff) { + list.push(instance); + } + } + Ok(list) + } + + async fn heal(&mut self, player: Resource, amount: f32) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.heal(amount).await; + Ok(()) + } + + async fn damage(&mut self, player: Resource, amount: f32) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.damage_generic(amount).await; + Ok(()) + } + + async fn kill(&mut self, player: Resource) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.kill().await; + Ok(()) + } + + async fn start_cooldown( + &mut self, + player: Resource, + group: String, + duration_ticks: i32, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.start_cooldown(group, duration_ticks).await; + Ok(()) + } + + async fn get_cooldown( + &mut self, + player: Resource, + group: String, + ) -> wasmtime::Result { + let player = player_from_resource(self, &player)?; + Ok(player.get_cooldown(&group).await) + } + + async fn is_on_cooldown( + &mut self, + player: Resource, + group: String, + ) -> wasmtime::Result { + let player = player_from_resource(self, &player)?; + Ok(player.is_on_cooldown(&group).await) + } + + async fn set_allow_flight( + &mut self, + player: Resource, + allowed: bool, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.set_allow_flight(allowed).await; + Ok(()) + } + + async fn set_fly_speed( + &mut self, + player: Resource, + speed: f32, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.set_fly_speed(speed).await; + Ok(()) + } + + async fn set_walk_speed( + &mut self, + player: Resource, + speed: f32, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.set_walk_speed(speed).await; + Ok(()) + } + + async fn set_invulnerable( + &mut self, + player: Resource, + invulnerable: bool, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.set_invulnerable(invulnerable).await; + Ok(()) + } + + async fn set_player_time( + &mut self, + player: Resource, + time: u64, + relative: bool, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player + .set_player_time( + &player.living_entity.entity.world.load_full(), + time, + relative, + ) + .await; + Ok(()) + } + + async fn reset_player_time(&mut self, player: Resource) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player + .reset_player_time(&player.living_entity.entity.world.load_full()) + .await; + Ok(()) + } + + async fn get_player_time(&mut self, player: Resource) -> wasmtime::Result> { + let player = player_from_resource(self, &player)?; + Ok(player.get_player_time()) + } + + async fn is_player_time_relative( + &mut self, + player: Resource, + ) -> wasmtime::Result { + let player = player_from_resource(self, &player)?; + Ok(player.is_player_time_relative()) + } + + async fn set_player_weather( + &mut self, + player: Resource, + weather: crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::PlayerWeather, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let w = match weather { + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::PlayerWeather::Clear => crate::entity::player::PlayerWeather::Clear, + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::PlayerWeather::Downfall => crate::entity::player::PlayerWeather::Downfall, + }; + player.set_player_weather(w); + Ok(()) + } + + async fn reset_player_weather(&mut self, player: Resource) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.reset_player_weather(); + Ok(()) + } + + async fn get_player_weather( + &mut self, + player: Resource, + ) -> wasmtime::Result>{ + let player = player_from_resource(self, &player)?; + Ok(player.get_player_weather().map(|w| match w { + crate::entity::player::PlayerWeather::Clear => crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::PlayerWeather::Clear, + crate::entity::player::PlayerWeather::Downfall => crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::PlayerWeather::Downfall, + })) + } + + async fn set_compass_target( + &mut self, + player: Resource, + pos: crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::common::Position, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let pos_vec = from_wasm_position(pos); + let block_pos = pumpkin_util::math::position::BlockPos::new( + pos_vec.x as i32, + pos_vec.y as i32, + pos_vec.z as i32, + ); + player.set_compass_target(block_pos).await; + Ok(()) + } + + async fn get_compass_target( + &mut self, + player: Resource, + ) -> wasmtime::Result< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::common::Position, + > { + let player = player_from_resource(self, &player)?; + let target = player + .get_compass_target() + .unwrap_or(pumpkin_util::math::position::BlockPos::new(0, 0, 0)); + let vec3 = pumpkin_util::math::vector3::Vector3::new( + f64::from(target.0.x), + f64::from(target.0.y), + f64::from(target.0.z), + ); + Ok(to_wasm_position(vec3)) + } + + async fn set_respawn_location( + &mut self, + player: Resource, + pos: crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::common::Position, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let pos_vec = from_wasm_position(pos); + let block_pos = pumpkin_util::math::position::BlockPos::new( + pos_vec.x as i32, + pos_vec.y as i32, + pos_vec.z as i32, + ); + player.set_respawn_location(block_pos); + Ok(()) + } + + async fn get_respawn_location( + &mut self, + player: Resource, + ) -> wasmtime::Result< + Option< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::common::Position, + >, + > { + let player = player_from_resource(self, &player)?; + Ok(player.get_respawn_location().map(|p| { + let vec3 = pumpkin_util::math::vector3::Vector3::new( + f64::from(p.0.x), + f64::from(p.0.y), + f64::from(p.0.z), + ); + to_wasm_position(vec3) + })) + } + + async fn hide_player( + &mut self, + player: Resource, + other: Resource, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let other = player_from_resource(self, &other)?; + player.hide_player(other.gameprofile.id).await; + Ok(()) + } + + async fn show_player( + &mut self, + player: Resource, + other: Resource, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + let other = player_from_resource(self, &other)?; + player.show_player(other.gameprofile.id).await; + Ok(()) + } + + async fn can_see( + &mut self, + player: Resource, + other: Resource, + ) -> wasmtime::Result { + let player = player_from_resource(self, &player)?; + let other = player_from_resource(self, &other)?; + Ok(player.can_see(&other.gameprofile.id).await) + } + + async fn get_target_block( + &mut self, + player: Resource, + max_distance: u32, + ) -> wasmtime::Result< + Option< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::common::Position, + >, + > { + let player = player_from_resource(self, &player)?; + let res = player + .get_target_block( + &player.living_entity.entity.world.load_full(), + f64::from(max_distance), + ) + .await; + Ok(res.map(|p| { + let vec3 = pumpkin_util::math::vector3::Vector3::new( + f64::from(p.0.x), + f64::from(p.0.y), + f64::from(p.0.z), + ); + to_wasm_position(vec3) + })) + } + + async fn launch_projectile( + &mut self, + _player: Resource, + _type_: crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::player::ProjectileType, + ) -> wasmtime::Result< + Option< + Resource< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::Entity, + >, + >, + > { + Ok(None) + } + async fn set_tab_list_header_footer( &mut self, player: Resource, diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/status_effect.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/status_effect.rs new file mode 100644 index 000000000..eb7f9d008 --- /dev/null +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/status_effect.rs @@ -0,0 +1,242 @@ +use crate::plugin::loader::wasm::wasm_host::{ + state::PluginHostState, wit::v0_1::pumpkin::plugin::status_effect, +}; + +impl status_effect::Host for PluginHostState {} + +#[must_use] +#[allow(clippy::too_many_lines)] +pub const fn from_wasm_status_effect_type( + t: status_effect::StatusEffectType, +) -> pumpkin_data::status_effect::EffectType { + match t { + status_effect::StatusEffectType::Speed => pumpkin_data::status_effect::EffectType::Speed, + status_effect::StatusEffectType::Slowness => { + pumpkin_data::status_effect::EffectType::Slowness + } + status_effect::StatusEffectType::Haste => pumpkin_data::status_effect::EffectType::Haste, + status_effect::StatusEffectType::MiningFatigue => { + pumpkin_data::status_effect::EffectType::MiningFatigue + } + status_effect::StatusEffectType::Strength => { + pumpkin_data::status_effect::EffectType::Strength + } + status_effect::StatusEffectType::InstantHealth => { + pumpkin_data::status_effect::EffectType::InstantHealth + } + status_effect::StatusEffectType::InstantDamage => { + pumpkin_data::status_effect::EffectType::InstantDamage + } + status_effect::StatusEffectType::JumpBoost => { + pumpkin_data::status_effect::EffectType::JumpBoost + } + status_effect::StatusEffectType::Nausea => pumpkin_data::status_effect::EffectType::Nausea, + status_effect::StatusEffectType::Regeneration => { + pumpkin_data::status_effect::EffectType::Regeneration + } + status_effect::StatusEffectType::Resistance => { + pumpkin_data::status_effect::EffectType::Resistance + } + status_effect::StatusEffectType::FireResistance => { + pumpkin_data::status_effect::EffectType::FireResistance + } + status_effect::StatusEffectType::WaterBreathing => { + pumpkin_data::status_effect::EffectType::WaterBreathing + } + status_effect::StatusEffectType::Invisibility => { + pumpkin_data::status_effect::EffectType::Invisibility + } + status_effect::StatusEffectType::Blindness => { + pumpkin_data::status_effect::EffectType::Blindness + } + status_effect::StatusEffectType::NightVision => { + pumpkin_data::status_effect::EffectType::NightVision + } + status_effect::StatusEffectType::Hunger => pumpkin_data::status_effect::EffectType::Hunger, + status_effect::StatusEffectType::Weakness => { + pumpkin_data::status_effect::EffectType::Weakness + } + status_effect::StatusEffectType::Poison => pumpkin_data::status_effect::EffectType::Poison, + status_effect::StatusEffectType::Wither => pumpkin_data::status_effect::EffectType::Wither, + status_effect::StatusEffectType::HealthBoost => { + pumpkin_data::status_effect::EffectType::HealthBoost + } + status_effect::StatusEffectType::Absorption => { + pumpkin_data::status_effect::EffectType::Absorption + } + status_effect::StatusEffectType::Saturation => { + pumpkin_data::status_effect::EffectType::Saturation + } + status_effect::StatusEffectType::Glowing => { + pumpkin_data::status_effect::EffectType::Glowing + } + status_effect::StatusEffectType::Levitation => { + pumpkin_data::status_effect::EffectType::Levitation + } + status_effect::StatusEffectType::Luck => pumpkin_data::status_effect::EffectType::Luck, + status_effect::StatusEffectType::Unluck => pumpkin_data::status_effect::EffectType::Unluck, + status_effect::StatusEffectType::SlowFalling => { + pumpkin_data::status_effect::EffectType::SlowFalling + } + status_effect::StatusEffectType::ConduitPower => { + pumpkin_data::status_effect::EffectType::ConduitPower + } + status_effect::StatusEffectType::DolphinsGrace => { + pumpkin_data::status_effect::EffectType::DolphinsGrace + } + status_effect::StatusEffectType::BadOmen => { + pumpkin_data::status_effect::EffectType::BadOmen + } + status_effect::StatusEffectType::HeroOfTheVillage => { + pumpkin_data::status_effect::EffectType::HeroOfTheVillage + } + status_effect::StatusEffectType::Darkness => { + pumpkin_data::status_effect::EffectType::Darkness + } + status_effect::StatusEffectType::TrialOmen => { + pumpkin_data::status_effect::EffectType::TrialOmen + } + status_effect::StatusEffectType::RaidOmen => { + pumpkin_data::status_effect::EffectType::RaidOmen + } + status_effect::StatusEffectType::WindCharged => { + pumpkin_data::status_effect::EffectType::WindCharged + } + status_effect::StatusEffectType::Weaving => { + pumpkin_data::status_effect::EffectType::Weaving + } + status_effect::StatusEffectType::Oozing => pumpkin_data::status_effect::EffectType::Oozing, + status_effect::StatusEffectType::Infested => { + pumpkin_data::status_effect::EffectType::Infested + } + } +} + +#[must_use] +#[allow(clippy::too_many_lines)] +pub const fn to_wasm_status_effect_type( + t: pumpkin_data::status_effect::EffectType, +) -> status_effect::StatusEffectType { + match t { + pumpkin_data::status_effect::EffectType::Speed => status_effect::StatusEffectType::Speed, + pumpkin_data::status_effect::EffectType::Slowness => { + status_effect::StatusEffectType::Slowness + } + pumpkin_data::status_effect::EffectType::Haste => status_effect::StatusEffectType::Haste, + pumpkin_data::status_effect::EffectType::MiningFatigue => { + status_effect::StatusEffectType::MiningFatigue + } + pumpkin_data::status_effect::EffectType::Strength => { + status_effect::StatusEffectType::Strength + } + pumpkin_data::status_effect::EffectType::InstantHealth => { + status_effect::StatusEffectType::InstantHealth + } + pumpkin_data::status_effect::EffectType::InstantDamage => { + status_effect::StatusEffectType::InstantDamage + } + pumpkin_data::status_effect::EffectType::JumpBoost => { + status_effect::StatusEffectType::JumpBoost + } + pumpkin_data::status_effect::EffectType::Nausea => status_effect::StatusEffectType::Nausea, + pumpkin_data::status_effect::EffectType::Regeneration => { + status_effect::StatusEffectType::Regeneration + } + pumpkin_data::status_effect::EffectType::Resistance => { + status_effect::StatusEffectType::Resistance + } + pumpkin_data::status_effect::EffectType::FireResistance => { + status_effect::StatusEffectType::FireResistance + } + pumpkin_data::status_effect::EffectType::WaterBreathing => { + status_effect::StatusEffectType::WaterBreathing + } + pumpkin_data::status_effect::EffectType::Invisibility => { + status_effect::StatusEffectType::Invisibility + } + pumpkin_data::status_effect::EffectType::Blindness => { + status_effect::StatusEffectType::Blindness + } + pumpkin_data::status_effect::EffectType::NightVision => { + status_effect::StatusEffectType::NightVision + } + pumpkin_data::status_effect::EffectType::Hunger => status_effect::StatusEffectType::Hunger, + pumpkin_data::status_effect::EffectType::Weakness => { + status_effect::StatusEffectType::Weakness + } + pumpkin_data::status_effect::EffectType::Poison => status_effect::StatusEffectType::Poison, + pumpkin_data::status_effect::EffectType::Wither => status_effect::StatusEffectType::Wither, + pumpkin_data::status_effect::EffectType::HealthBoost => { + status_effect::StatusEffectType::HealthBoost + } + pumpkin_data::status_effect::EffectType::Absorption => { + status_effect::StatusEffectType::Absorption + } + pumpkin_data::status_effect::EffectType::Saturation => { + status_effect::StatusEffectType::Saturation + } + pumpkin_data::status_effect::EffectType::Glowing => { + status_effect::StatusEffectType::Glowing + } + pumpkin_data::status_effect::EffectType::Levitation => { + status_effect::StatusEffectType::Levitation + } + pumpkin_data::status_effect::EffectType::Luck => status_effect::StatusEffectType::Luck, + pumpkin_data::status_effect::EffectType::Unluck => status_effect::StatusEffectType::Unluck, + pumpkin_data::status_effect::EffectType::SlowFalling => { + status_effect::StatusEffectType::SlowFalling + } + pumpkin_data::status_effect::EffectType::ConduitPower => { + status_effect::StatusEffectType::ConduitPower + } + pumpkin_data::status_effect::EffectType::DolphinsGrace => { + status_effect::StatusEffectType::DolphinsGrace + } + pumpkin_data::status_effect::EffectType::BadOmen => { + status_effect::StatusEffectType::BadOmen + } + pumpkin_data::status_effect::EffectType::HeroOfTheVillage => { + status_effect::StatusEffectType::HeroOfTheVillage + } + pumpkin_data::status_effect::EffectType::Darkness => { + status_effect::StatusEffectType::Darkness + } + pumpkin_data::status_effect::EffectType::TrialOmen => { + status_effect::StatusEffectType::TrialOmen + } + pumpkin_data::status_effect::EffectType::RaidOmen => { + status_effect::StatusEffectType::RaidOmen + } + pumpkin_data::status_effect::EffectType::WindCharged => { + status_effect::StatusEffectType::WindCharged + } + pumpkin_data::status_effect::EffectType::Weaving => { + status_effect::StatusEffectType::Weaving + } + pumpkin_data::status_effect::EffectType::Oozing => status_effect::StatusEffectType::Oozing, + pumpkin_data::status_effect::EffectType::Infested => { + status_effect::StatusEffectType::Infested + } + } +} + +#[must_use] +pub fn to_wasm_status_effect_instance( + effect: &pumpkin_data::potion::Effect, +) -> Option { + let name = effect + .effect_type + .minecraft_name + .strip_prefix("minecraft:") + .unwrap_or(effect.effect_type.minecraft_name); + let effect_type_enum = pumpkin_data::status_effect::EffectType::from_name(name)?; + let wasm_type = to_wasm_status_effect_type(effect_type_enum); + Some(status_effect::StatusEffectInstance { + effect_type: wasm_type, + duration: u32::try_from(effect.duration).unwrap_or(0), + amplifier: effect.amplifier, + ambient: effect.ambient, + show_particles: effect.show_particles, + show_icon: effect.show_icon, + }) +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs index 3a4946ac7..3163c8e92 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs @@ -13,6 +13,7 @@ use crate::block::entities::command_block::CommandBlockEntity as InternalCommand use crate::block::entities::jukebox::JukeboxBlockEntity as InternalJukeboxBlockEntity; use crate::block::entities::mob_spawner::MobSpawnerBlockEntity as InternalMobSpawnerBlockEntity; use crate::block::entities::sign::SignBlockEntity as InternalSignBlockEntity; +use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::common::Position as WitPosition; use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::{ BlockDirection as WitBlockDirection, BlockEntity, BlockEntityType, BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState, BoundingBox as WitBoundingBox, @@ -677,6 +678,43 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { Ok(entities) } + async fn strike_lightning( + &mut self, + world: Resource, + pos: WitPosition, + effect_only: bool, + ) -> wasmtime::Result<()> { + let world_provider = self.get_world_res(&world)?.provider.clone(); + let internal_pos = super::events::from_wasm_position(pos); + world_provider + .strike_lightning(internal_pos, effect_only) + .await; + Ok(()) + } + + async fn ray_trace_blocks( + &mut self, + world: Resource, + start: WitPosition, + end: WitPosition, + ) -> wasmtime::Result> { + let world_provider = self.get_world_res(&world)?.provider.clone(); + let start_pos = super::events::from_wasm_position(start); + let end_pos = super::events::from_wasm_position(end); + let res = world_provider + .raycast(start_pos, end_pos, async |pos, w| { + !w.get_block_state(pos).is_air() + }) + .await; + Ok(res.map(|(p, _)| { + super::events::to_wasm_position(pumpkin_util::math::vector3::Vector3::new( + f64::from(p.0.x), + f64::from(p.0.y), + f64::from(p.0.z), + )) + })) + } + async fn get_block_entity( &mut self, world: Resource, diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index dfcfae5b4..02bcd7b55 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -448,6 +448,12 @@ impl Server { new_worlds.push(world.clone()); new_worlds }); + let mut event = + crate::plugin::api::events::world::world_init::WorldInitEvent::new(world.clone()); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(server.plugin_manager.fire(&server, &mut event)); + }); world }) .await diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index f66e22f22..1ea292f3c 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -4729,6 +4729,18 @@ impl World { self.spawn_entity(item_entity).await; } + pub async fn strike_lightning(self: &Arc, pos: Vector3, _effect_only: bool) { + use pumpkin_data::entity::EntityType; + use uuid::Uuid; + let lightning = crate::entity::r#type::from_type( + &EntityType::LIGHTNING_BOLT, + pos, + self, + Uuid::new_v4(), + ); + self.spawn_entity(lightning).await; + } + /* ItemScatterer.java */ pub async fn scatter_inventory( self: &Arc,