diff --git a/crates/pumpkin-data/src/block_state.rs b/crates/pumpkin-data/src/block_state.rs index c3d6612dc..a21c224bb 100644 --- a/crates/pumpkin-data/src/block_state.rs +++ b/crates/pumpkin-data/src/block_state.rs @@ -233,6 +233,9 @@ impl BlockStateId { // depends on generated impl: // pub(crate) const STATE_COUNT: u16; + /// The total count of all registered block states. + pub const COUNT: u16 = Self::STATE_COUNT; + // SAFETY: There must never be a BlockStateId where self.0 >= BlockStateId::STATE_COUNT #[inline] diff --git a/crates/pumpkin-data/src/blocks.rs b/crates/pumpkin-data/src/blocks.rs index 33f77f106..95b357484 100644 --- a/crates/pumpkin-data/src/blocks.rs +++ b/crates/pumpkin-data/src/blocks.rs @@ -252,6 +252,9 @@ impl BlockId { // depends on generated impl: // pub(crate) const BLOCK_COUNT: u16; + /// The total count of all registered blocks. + pub const COUNT: u16 = Self::BLOCK_COUNT; + // SAFETY: There must never be a BlockId where self.0 >= BlockId::BLOCK_COUNT #[inline] diff --git a/crates/pumpkin-plugin-api/src/events/dialog/dialog_clear.rs b/crates/pumpkin-plugin-api/src/events/dialog/dialog_clear.rs new file mode 100644 index 000000000..4c2c774ad --- /dev/null +++ b/crates/pumpkin-plugin-api/src/events/dialog/dialog_clear.rs @@ -0,0 +1,22 @@ +use crate::wit::pumpkin::plugin::event::{DialogClearEventData, Event, EventType}; + +use super::super::FromIntoEvent; + +/// An event that occurs when a player's dialog is cleared. +pub struct DialogClearEvent; + +impl FromIntoEvent for DialogClearEvent { + const EVENT_TYPE: EventType = EventType::DialogClearEvent; + type Data = DialogClearEventData; + + fn data_from_event(event: Event) -> Self::Data { + match event { + Event::DialogClearEvent(data) => data, + _ => panic!("unexpected event"), + } + } + + fn data_into_event(data: Self::Data) -> Event { + Event::DialogClearEvent(data) + } +} diff --git a/crates/pumpkin-plugin-api/src/events/dialog/dialog_click_action.rs b/crates/pumpkin-plugin-api/src/events/dialog/dialog_click_action.rs new file mode 100644 index 000000000..6f29af329 --- /dev/null +++ b/crates/pumpkin-plugin-api/src/events/dialog/dialog_click_action.rs @@ -0,0 +1,22 @@ +use crate::wit::pumpkin::plugin::event::{DialogClickActionEventData, Event, EventType}; + +use super::super::FromIntoEvent; + +/// An event that occurs when a player clicks a custom dialog button. +pub struct DialogClickActionEvent; + +impl FromIntoEvent for DialogClickActionEvent { + const EVENT_TYPE: EventType = EventType::DialogClickActionEvent; + type Data = DialogClickActionEventData; + + fn data_from_event(event: Event) -> Self::Data { + match event { + Event::DialogClickActionEvent(data) => data, + _ => panic!("unexpected event"), + } + } + + fn data_into_event(data: Self::Data) -> Event { + Event::DialogClickActionEvent(data) + } +} diff --git a/crates/pumpkin-plugin-api/src/events/dialog/dialog_show.rs b/crates/pumpkin-plugin-api/src/events/dialog/dialog_show.rs new file mode 100644 index 000000000..2acb14bd1 --- /dev/null +++ b/crates/pumpkin-plugin-api/src/events/dialog/dialog_show.rs @@ -0,0 +1,22 @@ +use crate::wit::pumpkin::plugin::event::{DialogShowEventData, Event, EventType}; + +use super::super::FromIntoEvent; + +/// An event that occurs when a dialog is shown to a player. +pub struct DialogShowEvent; + +impl FromIntoEvent for DialogShowEvent { + const EVENT_TYPE: EventType = EventType::DialogShowEvent; + type Data = DialogShowEventData; + + fn data_from_event(event: Event) -> Self::Data { + match event { + Event::DialogShowEvent(data) => data, + _ => panic!("unexpected event"), + } + } + + fn data_into_event(data: Self::Data) -> Event { + Event::DialogShowEvent(data) + } +} diff --git a/crates/pumpkin-plugin-api/src/events/dialog/mod.rs b/crates/pumpkin-plugin-api/src/events/dialog/mod.rs new file mode 100644 index 000000000..42c44210d --- /dev/null +++ b/crates/pumpkin-plugin-api/src/events/dialog/mod.rs @@ -0,0 +1,10 @@ +/// Dialog clear event. +pub mod dialog_clear; +/// Dialog click action event. +pub mod dialog_click_action; +/// Dialog show event. +pub mod dialog_show; + +pub use dialog_clear::*; +pub use dialog_click_action::*; +pub use dialog_show::*; diff --git a/crates/pumpkin-plugin-api/src/events/mod.rs b/crates/pumpkin-plugin-api/src/events/mod.rs index 0857491b2..0e4cf902f 100644 --- a/crates/pumpkin-plugin-api/src/events/mod.rs +++ b/crates/pumpkin-plugin-api/src/events/mod.rs @@ -18,6 +18,8 @@ use crate::{Context, Result, Server, wit::pumpkin::plugin::event::EventType}; /// Block events. pub mod block; +/// Dialog events. +pub mod dialog; /// Enchantment events. pub mod enchantment; /// Entity events. @@ -40,6 +42,7 @@ pub mod vehicle; pub mod world; pub use block::*; +pub use dialog::*; pub use enchantment::*; pub use entity::*; pub use hanging::*; diff --git a/crates/pumpkin-plugin-api/src/events/player/custom_click_action.rs b/crates/pumpkin-plugin-api/src/events/player/custom_click_action.rs deleted file mode 100644 index b0830eb9d..000000000 --- a/crates/pumpkin-plugin-api/src/events/player/custom_click_action.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::wit::pumpkin::plugin::event::{CustomClickActionEventData, Event, EventType}; - -use super::super::FromIntoEvent; - -/// An event that occurs when a player clicks a custom dialog button. -pub struct CustomClickActionEvent; - -impl FromIntoEvent for CustomClickActionEvent { - const EVENT_TYPE: EventType = EventType::CustomClickActionEvent; - type Data = CustomClickActionEventData; - - fn data_from_event(event: Event) -> Self::Data { - match event { - Event::CustomClickActionEvent(data) => data, - _ => panic!("unexpected event"), - } - } - - fn data_into_event(data: Self::Data) -> Event { - Event::CustomClickActionEvent(data) - } -} diff --git a/crates/pumpkin-plugin-api/src/events/player/mod.rs b/crates/pumpkin-plugin-api/src/events/player/mod.rs index a3c6a3d20..175912f77 100644 --- a/crates/pumpkin-plugin-api/src/events/player/mod.rs +++ b/crates/pumpkin-plugin-api/src/events/player/mod.rs @@ -6,8 +6,6 @@ pub mod async_player_pre_login; pub mod bedrock_form_response; /// Player main hand change event. pub mod changed_main_hand; -/// Custom inventory click action event. -pub mod custom_click_action; /// Egg throw event. pub mod egg_throw; /// Experience change event. @@ -155,7 +153,6 @@ pub use async_player_chat::*; pub use async_player_pre_login::*; pub use bedrock_form_response::*; pub use changed_main_hand::*; -pub use custom_click_action::*; pub use egg_throw::*; pub use exp_change::*; pub use fish::*; diff --git a/crates/pumpkin-plugin-api/src/ext/block.rs b/crates/pumpkin-plugin-api/src/ext/block.rs new file mode 100644 index 000000000..c4af6eee8 --- /dev/null +++ b/crates/pumpkin-plugin-api/src/ext/block.rs @@ -0,0 +1,107 @@ +use crate::wit::pumpkin::plugin::world::{ + Block, BlockState, get_all_block_names, get_all_blocks, get_block_by_id, get_block_by_name, + get_block_count, get_block_from_state, get_block_from_state_id, get_block_properties, + get_block_state_by_id, get_block_state_count, get_default_state_from_block, + get_default_state_from_block_id, get_state_ids_for_block_id, get_states_for_block, + get_states_for_block_id, +}; + +impl Block { + /// Returns all registered blocks in the registry. + #[must_use] + pub fn all() -> Vec { + get_all_blocks() + } + + /// Returns the names of all registered blocks. + #[must_use] + pub fn all_names() -> Vec { + get_all_block_names() + } + + /// Returns the total number of registered block types. + #[must_use] + pub fn count() -> u32 { + get_block_count() + } + + /// Returns the total number of registered block states. + #[must_use] + pub fn total_state_count() -> u32 { + get_block_state_count() + } + + /// Gets a block definition by its numerical block ID. + #[must_use] + pub fn from_id(id: u16) -> Option { + get_block_by_id(id) + } + + /// Gets a block definition by its namespaced name (e.g., "minecraft:stone" or "stone"). + #[must_use] + pub fn from_name(name: &str) -> Option { + get_block_by_name(name) + } + + /// Gets the block definition for a given block state ID. + #[must_use] + pub fn from_state_id(state_id: u16) -> Option { + get_block_from_state_id(state_id) + } + + /// Gets the block definition for a given block state. + #[must_use] + pub fn from_state(state: &BlockState) -> Self { + get_block_from_state(state) + } + + /// Gets all valid block states for this block type. + #[must_use] + pub fn get_states(&self) -> Vec { + get_states_for_block(self) + } + + /// Gets all valid block states for a given numerical block ID. + #[must_use] + pub fn get_states_for_id(block_id: u16) -> Vec { + get_states_for_block_id(block_id) + } + + /// Gets all valid block state IDs for a given numerical block ID. + #[must_use] + pub fn get_state_ids_for_id(block_id: u16) -> Vec { + get_state_ids_for_block_id(block_id) + } + + /// Gets the default block state for this block. + #[must_use] + pub fn get_default_state(&self) -> BlockState { + get_default_state_from_block(self) + } + + /// Gets the default block state for a given numerical block ID. + #[must_use] + pub fn get_default_state_for_id(block_id: u16) -> Option { + get_default_state_from_block_id(block_id) + } +} + +impl BlockState { + /// Gets the parent block definition for this block state. + #[must_use] + pub fn get_block(&self) -> Block { + get_block_from_state(self) + } + + /// Gets a detailed block state by its numerical block state ID. + #[must_use] + pub fn from_id(state_id: u16) -> Option { + get_block_state_by_id(state_id) + } + + /// Gets property key-value pairs for this block state. + #[must_use] + pub fn get_properties(&self) -> Vec<(String, String)> { + get_block_properties(self.id) + } +} diff --git a/crates/pumpkin-plugin-api/src/ext/mod.rs b/crates/pumpkin-plugin-api/src/ext/mod.rs index 7ddb2956b..41f419c24 100644 --- a/crates/pumpkin-plugin-api/src/ext/mod.rs +++ b/crates/pumpkin-plugin-api/src/ext/mod.rs @@ -2,6 +2,7 @@ mod advancement; mod attributes; +mod block; mod game_rules; pub mod player; mod server_list_ping; diff --git a/crates/pumpkin-plugin-api/src/lib.rs b/crates/pumpkin-plugin-api/src/lib.rs index 210f20e7a..0bf68eede 100644 --- a/crates/pumpkin-plugin-api/src/lib.rs +++ b/crates/pumpkin-plugin-api/src/lib.rs @@ -145,7 +145,10 @@ pub use wit::pumpkin::plugin::item_stack::ItemStack; pub use wit::pumpkin::plugin::player::Player; pub use wit::pumpkin::plugin::scoreboard::{CollisionRule, NametagVisibility, TeamSettings}; pub use wit::pumpkin::plugin::server::Dimension; -pub use wit::pumpkin::plugin::world::World; +pub use wit::pumpkin::plugin::world::{ + Block, BlockDirection, BlockState, BlockStateInfo, Entity, Flammable, RayTraceBlockResult, + RayTraceEntityResult, RaycastResult, World, WorldBorder, +}; pub use worldgen::{ChunkBuffer, ChunkGenerator, GenerationPhase, GeneratorManager}; /// Advancement WIT API re-exports. @@ -157,7 +160,11 @@ pub mod advancement { /// Java dialog WIT API re-exports. pub mod java_dialog { - pub use crate::wit::pumpkin::plugin::java_dialogs::{ActionButton, DialogBody, DialogType}; + pub use crate::wit::pumpkin::plugin::java_dialogs::{ + Action, ActionButton, AfterAction, CustomClickAction, Dialog, DialogBody, DialogInput, + DialogInputBool, DialogInputNumberRange, DialogInputSingleOption, DialogInputText, + DialogType, Link, LinkLabel, LinkType, + }; } /// WIT-based logging subscriber. diff --git a/crates/pumpkin-plugin-wit b/crates/pumpkin-plugin-wit index 2337e6f38..060c913c3 160000 --- a/crates/pumpkin-plugin-wit +++ b/crates/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit 2337e6f389293a6371667d495feace8e35570318 +Subproject commit 060c913c3097ae730fb2c04b7835733dcc85a1a4 diff --git a/crates/pumpkin-protocol/src/java/client/dialog.rs b/crates/pumpkin-protocol/src/java/client/dialog.rs index b4f98a95f..363c512ed 100644 --- a/crates/pumpkin-protocol/src/java/client/dialog.rs +++ b/crates/pumpkin-protocol/src/java/client/dialog.rs @@ -39,6 +39,7 @@ pub enum DialogNBTSource<'a> { Nbt(&'a pumpkin_nbt::compound::NbtCompound), } +#[derive(Clone, Debug)] pub struct Dialog { pub r#type: String, pub title: TextComponent, @@ -52,11 +53,13 @@ pub struct Dialog { pub external_title: Option, } +#[derive(Clone, Debug)] pub enum DialogBody { PlainMessage { contents: TextComponent }, Item { item: i32 }, // TODO: ItemStack serialization to NBT } +#[derive(Clone, Debug)] pub enum DialogInput { Boolean { label: TextComponent, @@ -82,6 +85,7 @@ pub enum DialogInput { }, } +#[derive(Clone, Debug)] pub struct ActionButton { pub text: TextComponent, pub tooltip: Option, @@ -89,6 +93,7 @@ pub struct ActionButton { pub action: DialogAction, } +#[derive(Clone, Debug)] pub enum DialogAction { OpenUrl { url: String, @@ -99,6 +104,7 @@ pub enum DialogAction { }, } +#[derive(Clone, Debug)] pub struct DialogLink { pub label: crate::Label, pub url: String, diff --git a/crates/pumpkin-protocol/src/lib.rs b/crates/pumpkin-protocol/src/lib.rs index 729b03c67..048006094 100644 --- a/crates/pumpkin-protocol/src/lib.rs +++ b/crates/pumpkin-protocol/src/lib.rs @@ -483,6 +483,7 @@ impl PositionFlag { } } +#[derive(Clone, Debug)] pub enum Label { BuiltIn(LinkType), TextComponent(Box), @@ -523,7 +524,7 @@ impl<'a> Link<'a> { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] #[repr(i32)] pub enum LinkType { BugReport = 0, diff --git a/crates/pumpkin-util/src/text/mod.rs b/crates/pumpkin-util/src/text/mod.rs index fbad284aa..0da40e220 100644 --- a/crates/pumpkin-util/src/text/mod.rs +++ b/crates/pumpkin-util/src/text/mod.rs @@ -1383,6 +1383,47 @@ impl TextComponent { }) } + /// Creates a new text component displaying the name of one or more entities found by a selector. + /// + /// # Arguments + /// - `selector` – The entity selector string (e.g. `@e[type=pig]`). + /// - `separator` – Optional separator string between multiple entity names. + /// + /// # Returns + /// A new `TextComponent` displaying entity names. + #[must_use] + pub fn entity_names>, P: Into>>( + selector: S, + separator: Option

, + ) -> Self { + Self(TextComponentBase { + content: Box::new(TextContent::EntityNames { + selector: selector.into(), + separator: separator.map(Into::into), + }), + style: Box::new(Style::default()), + extra: vec![], + }) + } + + /// Creates a new text component displaying a keybind identifier. + /// + /// # Arguments + /// - `keybind` – The keybind identifier (e.g. `key.jump`, `key.forward`). + /// + /// # Returns + /// A new `TextComponent` displaying the configured key. + #[must_use] + pub fn keybind>>(keybind: K) -> Self { + Self(TextComponentBase { + content: Box::new(TextContent::Keybind { + keybind: keybind.into(), + }), + style: Box::new(Style::default()), + extra: vec![], + }) + } + /// Appends a child component to this component. /// /// # Arguments diff --git a/crates/pumpkin/src/block/blocks/doors.rs b/crates/pumpkin/src/block/blocks/doors.rs index 3c24f12c9..7eabbc23d 100644 --- a/crates/pumpkin/src/block/blocks/doors.rs +++ b/crates/pumpkin/src/block/blocks/doors.rs @@ -160,6 +160,66 @@ async fn get_hinge( #[pumpkin_block_from_tag("minecraft:doors")] pub struct DoorBlock; +impl DoorBlock { + #[must_use] + pub fn is_wooden_door(world: &World, block_pos: &BlockPos) -> bool { + let block = world.get_block(block_pos); + block.has_tag(&tag::Block::MINECRAFT_WOODEN_DOORS) + } + + #[must_use] + pub fn is_open(world: &World, block_pos: &BlockPos) -> bool { + let (block, block_state) = world.get_block_and_state_id(block_pos); + if !block.has_tag(&tag::Block::MINECRAFT_DOORS) { + return false; + } + let door_props = DoorProperties::from_state_id(block_state, block); + door_props.open + } + + pub async fn set_open(world: &Arc, block_pos: &BlockPos, open: bool) { + let (block, block_state) = world.get_block_and_state_id(block_pos); + if !block.has_tag(&tag::Block::MINECRAFT_DOORS) { + return; + } + let mut door_props = DoorProperties::from_state_id(block_state, block); + if door_props.open == open { + return; + } + door_props.open = open; + + let other_half = match door_props.half { + DoubleBlockHalf::Upper => BlockDirection::Down, + DoubleBlockHalf::Lower => BlockDirection::Up, + }; + let other_pos = block_pos.offset(other_half.to_offset()); + + let (other_block, other_state_id) = world.get_block_and_state_id(&other_pos); + + world.play_block_sound(get_sound(block, open), SoundCategory::Blocks, *block_pos); + + world + .set_block_state( + block_pos, + door_props.to_state_id(block), + BlockFlags::NOTIFY_LISTENERS, + ) + .await; + + if other_block.id == block.id { + let mut other_door_props = DoorProperties::from_state_id(other_state_id, other_block); + other_door_props.open = open; + world + .set_block_state( + &other_pos, + other_door_props.to_state_id(other_block), + BlockFlags::NOTIFY_LISTENERS, + ) + .await; + } + } +} + impl BlockBehaviour for DoorBlock { fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { diff --git a/crates/pumpkin/src/entity/ai/control/mod.rs b/crates/pumpkin/src/entity/ai/control/mod.rs index ccab9a02f..17e00f514 100644 --- a/crates/pumpkin/src/entity/ai/control/mod.rs +++ b/crates/pumpkin/src/entity/ai/control/mod.rs @@ -14,4 +14,12 @@ pub trait Control: Send + Sync { pub trait MoveControlTrait: Control { fn tick(&mut self, mob: &dyn Mob); + + fn set_wanted_position(&mut self, _x: f64, _y: f64, _z: f64, _speed_modifier: f64) {} + + fn strafe(&mut self, _forward: f32, _right: f32) {} + + fn has_wanted(&self) -> bool { + false + } } diff --git a/crates/pumpkin/src/entity/ai/control/move_control.rs b/crates/pumpkin/src/entity/ai/control/move_control.rs index cb7c15d9b..e2bfaa630 100644 --- a/crates/pumpkin/src/entity/ai/control/move_control.rs +++ b/crates/pumpkin/src/entity/ai/control/move_control.rs @@ -101,6 +101,27 @@ impl MoveControlTrait for MoveControl { // Navigator owns movement input while this controller waits. } + + fn set_wanted_position(&mut self, x: f64, y: f64, z: f64, speed_modifier: f64) { + self.wanted_x = x; + self.wanted_y = y; + self.wanted_z = z; + self.speed_modifier = speed_modifier; + if self.operation != Operation::Jumping { + self.operation = Operation::MoveTo; + } + } + + fn strafe(&mut self, forwards: f32, right: f32) { + self.operation = Operation::Strafe; + self.strafe_forwards = forwards; + self.strafe_right = right; + self.speed_modifier = 0.25; + } + + fn has_wanted(&self) -> bool { + self.operation == Operation::MoveTo + } } impl MoveControl { diff --git a/crates/pumpkin/src/entity/ai/goal/break_door.rs b/crates/pumpkin/src/entity/ai/goal/break_door.rs new file mode 100644 index 000000000..d8f374621 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/break_door.rs @@ -0,0 +1,228 @@ +use std::sync::Arc; + +use pumpkin_data::BlockStateId; +use pumpkin_data::world::WorldEvent; +use pumpkin_util::Difficulty; +use pumpkin_world::world::BlockFlags; +use rand::RngExt; + +use super::door_interact::DoorInteractGoal; +use super::{Controls, Goal, GoalFuture}; +use crate::block::blocks::doors::DoorBlock; +use crate::entity::mob::Mob; + +const DEFAULT_DOOR_BREAK_TIME: i32 = 240; + +pub type DifficultyPredicate = Arc bool + Send + Sync>; + +pub struct BreakDoorGoal { + pub door_interact_goal: DoorInteractGoal, + valid_difficulties: DifficultyPredicate, + pub break_time: i32, + pub last_break_progress: i32, + pub door_break_time: i32, +} + +impl BreakDoorGoal { + #[must_use] + pub fn new(valid_difficulties: DifficultyPredicate) -> Self { + Self { + door_interact_goal: DoorInteractGoal::new(), + valid_difficulties, + break_time: 0, + last_break_progress: -1, + door_break_time: -1, + } + } + + #[must_use] + pub fn with_door_break_time( + door_break_time: i32, + valid_difficulties: DifficultyPredicate, + ) -> Self { + Self { + door_interact_goal: DoorInteractGoal::new(), + valid_difficulties, + break_time: 0, + last_break_progress: -1, + door_break_time, + } + } + + #[must_use] + pub const fn get_door_break_time(&self) -> i32 { + if self.door_break_time > DEFAULT_DOOR_BREAK_TIME { + self.door_break_time + } else { + DEFAULT_DOOR_BREAK_TIME + } + } + + #[must_use] + pub fn is_valid_difficulty(&self, difficulty: Difficulty) -> bool { + (self.valid_difficulties)(difficulty) + } +} + +impl Default for BreakDoorGoal { + fn default() -> Self { + Self::new(Arc::new(|d| d == Difficulty::Hard)) + } +} + +impl Goal for BreakDoorGoal { + fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + if !self.door_interact_goal.can_use(mob) { + return false; + } + let world = mob.get_entity().world.load(); + let level_info = world.level_info.load(); + if !level_info.game_rules.mob_griefing { + return false; + } + self.is_valid_difficulty(level_info.difficulty) && !self.door_interact_goal.is_open(mob) + }) + } + + fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let world = mob.get_entity().world.load(); + let level_info = world.level_info.load(); + let mob_pos = mob.get_entity().pos.load(); + let door_pos = self.door_interact_goal.door_pos; + let center_x = f64::from(door_pos.0.x) + 0.5; + let center_y = f64::from(door_pos.0.y) + 0.5; + let center_z = f64::from(door_pos.0.z) + 0.5; + let dx = center_x - mob_pos.x; + let dy = center_y - mob_pos.y; + let dz = center_z - mob_pos.z; + let dist_sq = dx * dx + dy * dy + dz * dz; + + self.break_time <= self.get_door_break_time() + && !DoorBlock::is_open(&world, &door_pos) + && dist_sq < 4.0 + && self.is_valid_difficulty(level_info.difficulty) + }) + } + + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.door_interact_goal.start_interaction(mob); + self.break_time = 0; + self.last_break_progress = -1; + }) + } + + fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + let world = mob.get_entity().world.load(); + world.set_block_destroy_stage( + mob.get_entity().entity_id, + self.door_interact_goal.door_pos, + -1, + ); + }) + } + + fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.door_interact_goal.tick_interaction(mob); + let world = mob.get_entity().world.load_full(); + + if mob.get_random().random_range(0..20) == 0 { + world.sync_world_event( + WorldEvent::SoundZombieWoodenDoor, + self.door_interact_goal.door_pos, + 0, + ); + mob.get_mob_entity().living_entity.swing_hand().await; + } + + self.break_time += 1; + let progress = + (self.break_time as f32 / self.get_door_break_time() as f32 * 10.0) as i32; + if progress != self.last_break_progress { + world.set_block_destroy_stage( + mob.get_entity().entity_id, + self.door_interact_goal.door_pos, + progress as i8, + ); + self.last_break_progress = progress; + } + + let level_info = world.level_info.load(); + if self.break_time == self.get_door_break_time() + && self.is_valid_difficulty(level_info.difficulty) + { + let (_, block_state_id) = + world.get_block_and_state_id(&self.door_interact_goal.door_pos); + mob.break_door(self.door_interact_goal.door_pos).await; + world + .set_block_state( + &self.door_interact_goal.door_pos, + BlockStateId::AIR, + BlockFlags::NOTIFY_ALL, + ) + .await; + world.sync_world_event( + WorldEvent::SoundZombieDoorCrash, + self.door_interact_goal.door_pos, + 0, + ); + world.sync_world_event( + WorldEvent::ParticlesDestroyBlock, + self.door_interact_goal.door_pos, + i32::from(block_state_id.as_u16()), + ); + } + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn controls(&self) -> Controls { + Controls::empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn door_break_time_default_and_custom() { + let default_goal = BreakDoorGoal::default(); + assert_eq!(default_goal.get_door_break_time(), 240); + + let custom_goal = + BreakDoorGoal::with_door_break_time(300, Arc::new(|d| d == Difficulty::Hard)); + assert_eq!(custom_goal.get_door_break_time(), 300); + + let low_goal = + BreakDoorGoal::with_door_break_time(100, Arc::new(|d| d == Difficulty::Hard)); + assert_eq!(low_goal.get_door_break_time(), 240); + } + + #[test] + fn valid_difficulty() { + let hard_only = BreakDoorGoal::new(Arc::new(|d| d == Difficulty::Hard)); + assert!(hard_only.is_valid_difficulty(Difficulty::Hard)); + assert!(!hard_only.is_valid_difficulty(Difficulty::Normal)); + assert!(!hard_only.is_valid_difficulty(Difficulty::Easy)); + assert!(!hard_only.is_valid_difficulty(Difficulty::Peaceful)); + } + + #[test] + fn break_progress_calculation() { + let break_time = 120; + let total_time = 240; + let progress = (break_time as f32 / total_time as f32 * 10.0) as i32; + assert_eq!(progress, 5); + + let end_progress = (240.0f32 / 240.0f32 * 10.0) as i32; + assert_eq!(end_progress, 10); + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/door_interact.rs b/crates/pumpkin/src/entity/ai/goal/door_interact.rs new file mode 100644 index 000000000..bf4dd95a9 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/door_interact.rs @@ -0,0 +1,201 @@ +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_util::math::position::BlockPos; +use std::sync::atomic::Ordering; + +use super::{Controls, Goal, GoalFuture}; +use crate::block::blocks::doors::DoorBlock; +use crate::entity::mob::Mob; + +pub struct DoorInteractGoal { + pub door_pos: BlockPos, + pub has_door: bool, + pub passed: bool, + pub door_open_dir_x: f32, + pub door_open_dir_z: f32, +} + +impl Default for DoorInteractGoal { + fn default() -> Self { + Self::new() + } +} + +impl DoorInteractGoal { + #[must_use] + pub const fn new() -> Self { + Self { + door_pos: BlockPos::ZERO, + has_door: false, + passed: false, + door_open_dir_x: 0.0, + door_open_dir_z: 0.0, + } + } + + pub fn is_open(&mut self, mob: &dyn Mob) -> bool { + if !self.has_door { + return false; + } + let world = mob.get_entity().world.load(); + let (block, _) = world.get_block_and_state_id(&self.door_pos); + if !block.has_tag(&tag::Block::MINECRAFT_DOORS) { + self.has_door = false; + return false; + } + DoorBlock::is_open(&world, &self.door_pos) + } + + pub async fn set_open(&mut self, mob: &dyn Mob, open: bool) { + if self.has_door { + let world = mob.get_entity().world.load_full(); + let (block, _) = world.get_block_and_state_id(&self.door_pos); + if block.has_tag(&tag::Block::MINECRAFT_DOORS) { + DoorBlock::set_open(&world, &self.door_pos, open).await; + } + } + } + + pub fn can_use(&mut self, mob: &dyn Mob) -> bool { + if !mob + .get_entity() + .horizontal_collision + .load(Ordering::Relaxed) + { + return false; + } + + let navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + let Some(path) = navigator.get_path() else { + return false; + }; + + if path.is_done() { + return false; + } + + let world = mob.get_entity().world.load(); + let mob_pos = mob.get_entity().pos.load(); + let limit = (path.get_next_node_index() + 2).min(path.get_node_count()); + + for i in 0..limit { + let Some(node) = path.get_node(i) else { + continue; + }; + let door_pos = BlockPos::new(node.pos.0.x, node.pos.0.y + 1, node.pos.0.z); + let dx = mob_pos.x - f64::from(door_pos.0.x); + let dz = mob_pos.z - f64::from(door_pos.0.z); + let dist_sqr = dx * dx + dz * dz; + + if dist_sqr <= 2.25 { + self.door_pos = door_pos; + self.has_door = DoorBlock::is_wooden_door(&world, &self.door_pos); + if self.has_door { + return true; + } + } + } + + let above_pos = mob.get_entity().block_pos.load().up(); + self.door_pos = above_pos; + self.has_door = DoorBlock::is_wooden_door(&world, &self.door_pos); + self.has_door + } + + #[must_use] + pub const fn can_continue_to_use(&self) -> bool { + !self.passed + } + + pub fn start_interaction(&mut self, mob: &dyn Mob) { + self.passed = false; + let mob_pos = mob.get_entity().pos.load(); + self.door_open_dir_x = (self.door_pos.0.x as f32 + 0.5) - mob_pos.x as f32; + self.door_open_dir_z = (self.door_pos.0.z as f32 + 0.5) - mob_pos.z as f32; + } + + pub fn tick_interaction(&mut self, mob: &dyn Mob) { + let mob_pos = mob.get_entity().pos.load(); + let new_door_dir_x = (self.door_pos.0.x as f32 + 0.5) - mob_pos.x as f32; + let new_door_dir_z = (self.door_pos.0.z as f32 + 0.5) - mob_pos.z as f32; + let dot = self.door_open_dir_x * new_door_dir_x + self.door_open_dir_z * new_door_dir_z; + if dot < 0.0 { + self.passed = true; + } + } +} + +impl Goal for DoorInteractGoal { + fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { self.can_use(mob) }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { self.can_continue_to_use() }) + } + + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.start_interaction(mob); + }) + } + + fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.tick_interaction(mob); + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn controls(&self) -> Controls { + Controls::empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn passed_when_crossing_door() { + let mut goal = DoorInteractGoal::new(); + goal.door_pos = BlockPos::new(10, 64, 10); + + // Simulate start: mob at (10.5, 64.0, 8.0), moving towards door at Z=10.5 + // door_open_dir = (10.5 - 10.5, 10.5 - 8.0) = (0.0, 2.5) + goal.door_open_dir_x = 0.0; + goal.door_open_dir_z = 2.5; + goal.passed = false; + + assert!(goal.can_continue_to_use()); + + // Still before door: mob at (10.5, 64.0, 9.5) -> new_door_dir = (0.0, 1.0) + // dot = 0*0 + 2.5*1.0 = 2.5 > 0 + let new_x = 0.0f32; + let new_z = 1.0f32; + let dot = goal.door_open_dir_x * new_x + goal.door_open_dir_z * new_z; + if dot < 0.0 { + goal.passed = true; + } + assert!(!goal.passed); + assert!(goal.can_continue_to_use()); + + // Crossed door: mob at (10.5, 64.0, 12.0) -> new_door_dir = (0.0, -1.5) + // dot = 0*0 + 2.5*(-1.5) = -3.75 < 0 + let new_x2 = 0.0f32; + let new_z2 = -1.5f32; + let dot2 = goal.door_open_dir_x * new_x2 + goal.door_open_dir_z * new_z2; + if dot2 < 0.0 { + goal.passed = true; + } + assert!(goal.passed); + assert!(!goal.can_continue_to_use()); + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/goal_selector.rs b/crates/pumpkin/src/entity/ai/goal/goal_selector.rs index b534ca357..ed14e15ce 100644 --- a/crates/pumpkin/src/entity/ai/goal/goal_selector.rs +++ b/crates/pumpkin/src/entity/ai/goal/goal_selector.rs @@ -23,32 +23,40 @@ impl GoalSelector { } pub async fn remove_goal(&mut self, mob: &dyn Mob) { - let mut goals_to_remove = Vec::with_capacity(2); - for (i, prioritized_goal) in &mut self.goals.iter_mut().enumerate() { - if TypeId::of::() == prioritized_goal.type_id { - if prioritized_goal.running { - prioritized_goal.stop(mob).await; - } - goals_to_remove.push(i); - } + let mut stopped = self.remove_goal_sync::(); + for goal in &mut stopped { + goal.stop(mob).await; } + } - for goal_idx in goals_to_remove { - self.goals.swap_remove(goal_idx); + pub fn remove_goal_sync(&mut self) -> Vec { + self.remove_goal_by_type_id(TypeId::of::()) + } - // This is very fast because arrays are on the stack and the compiler knows the size - for slot in &mut self.goals_by_control { - if *slot == usize::MAX { - continue; - } - // Update the idx - if *slot == goal_idx { - *slot = usize::MAX; - } else if *slot > goal_idx { - *slot -= 1; + pub fn remove_goal_by_type_id(&mut self, type_id: TypeId) -> Vec { + let mut stopped = Vec::new(); + let mut i = 0; + while i < self.goals.len() { + if self.goals[i].type_id == type_id { + let goal = self.goals.swap_remove(i); + for slot in &mut self.goals_by_control { + if *slot == usize::MAX { + continue; + } + if *slot == i { + *slot = usize::MAX; + } else if *slot == self.goals.len() { + *slot = i; + } } + if goal.running { + stopped.push(goal); + } + } else { + i += 1; } } + stopped } pub fn clear(&mut self) -> Vec { diff --git a/crates/pumpkin/src/entity/ai/goal/mod.rs b/crates/pumpkin/src/entity/ai/goal/mod.rs index 159cad014..21fabbf96 100644 --- a/crates/pumpkin/src/entity/ai/goal/mod.rs +++ b/crates/pumpkin/src/entity/ai/goal/mod.rs @@ -7,10 +7,12 @@ pub mod avoid_entity; pub mod beg; pub mod blaze_attack; pub mod bow_attack; +pub mod break_door; pub mod breed; pub mod chase_player; pub mod creeper_ignite; pub mod destroy_egg; +pub mod door_interact; pub mod eat_grass; pub mod escape_danger; pub mod follow_owner; @@ -20,6 +22,8 @@ pub mod look_around; pub mod look_at_entity; pub mod melee_attack; pub mod move_to_target_pos; +pub mod offer_flower; +pub mod open_door; pub mod owner_hurt_by_target; pub mod owner_hurt_target; pub mod pathfind_to_raid; @@ -33,6 +37,7 @@ pub mod teleport_towards_player; pub mod tempt; pub(crate) mod track_target; pub mod trade_with_player; +pub mod try_find_water; pub mod wander_around; pub mod work_at_job_site; pub mod zombie_attack; diff --git a/crates/pumpkin/src/entity/ai/goal/offer_flower.rs b/crates/pumpkin/src/entity/ai/goal/offer_flower.rs new file mode 100644 index 000000000..04f6b9513 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/offer_flower.rs @@ -0,0 +1,185 @@ +use std::ops::BitOr; + +use pumpkin_data::entity::EntityStatus; +use pumpkin_data::tag::{self, Taggable}; +use rand::RngExt; + +use super::{Controls, Goal, GoalFuture}; +use crate::entity::mob::Mob; + +pub const OFFER_TICKS: i32 = 400; + +pub struct OfferFlowerGoal { + pub target_entity_id: Option, + pub tick: i32, +} + +impl Default for OfferFlowerGoal { + fn default() -> Self { + Self::new() + } +} + +impl OfferFlowerGoal { + #[must_use] + pub const fn new() -> Self { + Self { + target_entity_id: None, + tick: 0, + } + } +} + +impl Goal for OfferFlowerGoal { + fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let world = mob.get_entity().world.load(); + if world.level_time.lock().await.is_night() { + return false; + } + + if mob.get_random().random_range(0..8000) != 0 { + return false; + } + + let golem_entity = mob.get_entity(); + let golem_pos = golem_entity.pos.load(); + let bb = golem_entity.bounding_box.load().expand(6.0, 2.0, 6.0); + let nearby = world.get_entities_at_box(&bb); + + let mut closest: Option<(i32, f64)> = None; + + for candidate in nearby { + let cand_entity = candidate.get_entity(); + if cand_entity.entity_id == golem_entity.entity_id { + continue; + } + + if !cand_entity + .entity_type + .has_tag(&tag::EntityType::MINECRAFT_CANDIDATE_FOR_IRON_GOLEM_GIFT) + { + continue; + } + + let cand_pos = cand_entity.pos.load(); + let dx = cand_pos.x - golem_pos.x; + let dy = cand_pos.y - golem_pos.y; + let dz = cand_pos.z - golem_pos.z; + let dist_sq = dx * dx + dy * dy + dz * dz; + + if dist_sq <= 36.0 { + if let Some((_, closest_dist)) = closest { + if dist_sq < closest_dist { + closest = Some((cand_entity.entity_id, dist_sq)); + } + } else { + closest = Some((cand_entity.entity_id, dist_sq)); + } + } + } + + if let Some((id, _)) = closest { + self.target_entity_id = Some(id); + true + } else { + self.target_entity_id = None; + false + } + }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { self.tick > 0 }) + } + + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.tick = OFFER_TICKS; + if let Some(golem) = mob.as_iron_golem() { + golem.offer_flower(true); + } else { + let entity = mob.get_entity(); + let world = entity.world.load(); + world.send_entity_status(entity, EntityStatus::OfferFlower, None); + } + }) + } + + fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if let Some(golem) = mob.as_iron_golem() { + golem.offer_flower(false); + } else { + let entity = mob.get_entity(); + let world = entity.world.load(); + world.send_entity_status(entity, EntityStatus::StopOfferFlower, None); + } + + if self.tick == 0 + && let Some(target_id) = self.target_entity_id + { + let world = mob.get_entity().world.load(); + if let Some(target) = world.get_entity_by_id(target_id) { + let target_entity = target.get_entity(); + let bb = mob.get_entity().bounding_box.load().expand(6.0, 2.0, 6.0); + if target_entity + .entity_type + .has_tag(&tag::EntityType::MINECRAFT_ACCEPTS_IRON_GOLEM_GIFT) + && bb.intersects(&target_entity.bounding_box.load()) + { + // Target accepted gift + } + } + } + + self.target_entity_id = None; + }) + } + + fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if let Some(target_id) = self.target_entity_id { + let world = mob.get_entity().world.load(); + if let Some(target) = world.get_entity_by_id(target_id) { + let target_entity = target.get_entity(); + let target_pos = target_entity.pos.load(); + mob.get_mob_entity() + .look_control + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .look_at_with_range( + target_pos.x, + target_entity.get_eye_y(), + target_pos.z, + 30.0, + 30.0, + ); + } + } + self.tick -= 1; + }) + } + + fn controls(&self) -> Controls { + Controls::MOVE.bitor(Controls::LOOK) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn offer_flower_goal_lifecycle() { + let mut goal = OfferFlowerGoal::new(); + assert_eq!(goal.tick, 0); + assert!(goal.target_entity_id.is_none()); + + goal.tick = OFFER_TICKS; + assert_eq!(goal.tick, 400); + let controls = goal.controls(); + assert!(controls.get(Controls::MOVE)); + assert!(controls.get(Controls::LOOK)); + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/open_door.rs b/crates/pumpkin/src/entity/ai/goal/open_door.rs new file mode 100644 index 000000000..59ff900a8 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/open_door.rs @@ -0,0 +1,97 @@ +use super::door_interact::DoorInteractGoal; +use super::{Controls, Goal, GoalFuture}; +use crate::entity::mob::Mob; + +pub struct OpenDoorGoal { + pub door_interact_goal: DoorInteractGoal, + pub close_door: bool, + pub forget_time: i32, +} + +impl OpenDoorGoal { + #[must_use] + pub const fn new(close_door_after: bool) -> Self { + Self { + door_interact_goal: DoorInteractGoal::new(), + close_door: close_door_after, + forget_time: 0, + } + } +} + +impl Default for OpenDoorGoal { + fn default() -> Self { + Self::new(false) + } +} + +impl Goal for OpenDoorGoal { + fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { self.door_interact_goal.can_use(mob) }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + self.close_door && self.forget_time > 0 && self.door_interact_goal.can_continue_to_use() + }) + } + + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.door_interact_goal.start_interaction(mob); + self.forget_time = 20; + self.door_interact_goal.set_open(mob, true).await; + }) + } + + fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if self.close_door { + self.door_interact_goal.set_open(mob, false).await; + } + }) + } + + fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.forget_time -= 1; + self.door_interact_goal.tick_interaction(mob); + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn controls(&self) -> Controls { + Controls::empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn open_door_can_continue_to_use() { + let mut goal = OpenDoorGoal::new(true); + goal.forget_time = 20; + goal.door_interact_goal.passed = false; + + assert!(goal.close_door); + assert!(goal.forget_time > 0); + assert!(goal.door_interact_goal.can_continue_to_use()); + + // When forget_time expires + goal.forget_time = 0; + assert!( + !(goal.close_door + && goal.forget_time > 0 + && goal.door_interact_goal.can_continue_to_use()) + ); + + // When close_door is false + let goal_no_close = OpenDoorGoal::new(false); + assert!(!goal_no_close.close_door); + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs b/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs index e0fe2a9b1..f610a7305 100644 --- a/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs +++ b/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs @@ -24,7 +24,7 @@ impl OwnerHurtByTargetGoal { impl Goal for OwnerHurtByTargetGoal { fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { Box::pin(async { - if mob.is_sitting() { + if !mob.is_tamed() || mob.is_sitting() { return false; } @@ -105,3 +105,14 @@ impl Goal for OwnerHurtByTargetGoal { Controls::TARGET } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn owner_hurt_by_target_goal_controls() { + let goal = OwnerHurtByTargetGoal::new(); + assert!(goal.controls().get(Controls::TARGET)); + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs b/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs index 7a4943159..75b3d591b 100644 --- a/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs +++ b/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs @@ -24,7 +24,7 @@ impl OwnerHurtTargetGoal { impl Goal for OwnerHurtTargetGoal { fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { Box::pin(async { - if mob.is_sitting() { + if !mob.is_tamed() || mob.is_sitting() { return false; } @@ -105,3 +105,14 @@ impl Goal for OwnerHurtTargetGoal { Controls::TARGET } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn owner_hurt_target_goal_controls() { + let goal = OwnerHurtTargetGoal::new(); + assert!(goal.controls().get(Controls::TARGET)); + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/try_find_water.rs b/crates/pumpkin/src/entity/ai/goal/try_find_water.rs new file mode 100644 index 000000000..a4b3f9c6d --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/try_find_water.rs @@ -0,0 +1,125 @@ +use std::sync::atomic::Ordering; + +use pumpkin_data::fluid::Fluid; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_util::math::position::BlockPos; + +use super::{Controls, Goal, GoalFuture}; +use crate::entity::mob::Mob; +use crate::world::World; + +pub struct TryFindWaterGoal; + +impl Default for TryFindWaterGoal { + fn default() -> Self { + Self::new() + } +} + +impl TryFindWaterGoal { + #[must_use] + pub const fn new() -> Self { + Self + } + + pub fn is_water(world: &World, pos: &BlockPos) -> bool { + let (_, state_id) = world.get_block_and_state_id(pos); + if state_id.to_state().is_waterlogged() { + return true; + } + Fluid::from_state_id(state_id) + .is_some_and(|fluid| fluid.has_tag(&tag::Fluid::MINECRAFT_WATER)) + } + + #[must_use] + pub fn find_water_range( + pos: pumpkin_util::math::vector3::Vector3, + ) -> (BlockPos, BlockPos) { + let min_x = (pos.x - 2.0).floor() as i32; + let min_y = (pos.y - 2.0).floor() as i32; + let min_z = (pos.z - 2.0).floor() as i32; + let max_x = (pos.x + 2.0).floor() as i32; + let max_y = pos.y.floor() as i32; + let max_z = (pos.z + 2.0).floor() as i32; + + ( + BlockPos::new(min_x, min_y, min_z), + BlockPos::new(max_x, max_y, max_z), + ) + } +} + +impl Goal for TryFindWaterGoal { + fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let entity = mob.get_entity(); + if !entity.on_ground.load(Ordering::Relaxed) { + return false; + } + + let world = entity.world.load(); + let block_pos = entity.block_pos.load(); + !Self::is_water(&world, &block_pos) + }) + } + + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + let entity = mob.get_entity(); + let world = entity.world.load(); + let mob_pos = entity.pos.load(); + + let (min_pos, max_pos) = Self::find_water_range(mob_pos); + let mut water_pos: Option = None; + + 'outer: for x in min_pos.0.x..=max_pos.0.x { + for y in min_pos.0.y..=max_pos.0.y { + for z in min_pos.0.z..=max_pos.0.z { + let pos = BlockPos::new(x, y, z); + if Self::is_water(&world, &pos) { + water_pos = Some(pos); + break 'outer; + } + } + } + } + + if let Some(pos) = water_pos { + mob.get_mob_entity() + .move_control + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .set_wanted_position( + f64::from(pos.0.x), + f64::from(pos.0.y), + f64::from(pos.0.z), + 1.0, + ); + } + }) + } + + fn controls(&self) -> Controls { + Controls::empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pumpkin_util::math::vector3::Vector3; + + #[test] + fn find_water_range_calculation() { + let pos = Vector3::new(10.5, 64.0, -5.2); + let (min, max) = TryFindWaterGoal::find_water_range(pos); + + assert_eq!(min.0.x, 8); + assert_eq!(min.0.y, 62); + assert_eq!(min.0.z, -8); + + assert_eq!(max.0.x, 12); + assert_eq!(max.0.y, 64); + assert_eq!(max.0.z, -4); + } +} diff --git a/crates/pumpkin/src/entity/ai/pathfinder/mod.rs b/crates/pumpkin/src/entity/ai/pathfinder/mod.rs index da17b6ee2..cb8afb1c0 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/mod.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/mod.rs @@ -473,4 +473,14 @@ impl Navigator { pub fn is_idle(&self) -> bool { self.is_idle.load(Ordering::Relaxed) } + + #[must_use] + pub const fn get_path(&self) -> Option<&Path> { + self.current_path.as_ref() + } + + #[must_use] + pub const fn get_path_mut(&mut self) -> Option<&mut Path> { + self.current_path.as_mut() + } } diff --git a/crates/pumpkin/src/entity/mob/mod.rs b/crates/pumpkin/src/entity/mob/mod.rs index bc40dc0ce..633dc894a 100644 --- a/crates/pumpkin/src/entity/mob/mod.rs +++ b/crates/pumpkin/src/entity/mob/mod.rs @@ -650,6 +650,10 @@ pub trait Mob: EntityBase + Send + Sync { None } + fn as_iron_golem(&self) -> Option<&crate::entity::passive::iron_golem::IronGolemEntity> { + None + } + fn mob_write_nbt<'a>(&'a self, _nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async {}) } @@ -833,6 +837,11 @@ pub trait Mob: EntityBase + Send + Sync { .is_some_and(crate::entity::passive::tamable::TamableAnimal::is_in_sitting_pose) } + fn is_tamed(&self) -> bool { + self.as_tamable() + .is_some_and(crate::entity::passive::tamable::TamableAnimal::is_tame) + } + fn get_base_experience_reward(&self) -> u32 { self.get_entity().entity_type.experience_reward } diff --git a/crates/pumpkin/src/entity/mob/piglin.rs b/crates/pumpkin/src/entity/mob/piglin.rs index 99bffbc91..9545becf7 100644 --- a/crates/pumpkin/src/entity/mob/piglin.rs +++ b/crates/pumpkin/src/entity/mob/piglin.rs @@ -6,8 +6,8 @@ use crate::entity::{ Entity, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; @@ -34,6 +34,7 @@ impl PiglinEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); + goal_selector.add_goal(1, Box::new(OpenDoorGoal::new(true))); // Piglins use crossbows or swords, but for now we give them melee goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, true))); goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); diff --git a/crates/pumpkin/src/entity/mob/piglin_brute.rs b/crates/pumpkin/src/entity/mob/piglin_brute.rs index da77afabb..cabab7fa9 100644 --- a/crates/pumpkin/src/entity/mob/piglin_brute.rs +++ b/crates/pumpkin/src/entity/mob/piglin_brute.rs @@ -6,8 +6,8 @@ use crate::entity::{ Entity, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; @@ -34,6 +34,7 @@ impl PiglinBruteEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); + goal_selector.add_goal(1, Box::new(OpenDoorGoal::new(true))); goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, true))); goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( diff --git a/crates/pumpkin/src/entity/mob/vindicator.rs b/crates/pumpkin/src/entity/mob/vindicator.rs index f3024c847..2cc1c7f96 100644 --- a/crates/pumpkin/src/entity/mob/vindicator.rs +++ b/crates/pumpkin/src/entity/mob/vindicator.rs @@ -8,8 +8,8 @@ use crate::entity::{ Entity, NbtFuture, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{ Mob, MobEntity, @@ -48,6 +48,7 @@ impl VindicatorEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); + goal_selector.add_goal(1, Box::new(OpenDoorGoal::new(true))); goal_selector.add_goal(1, Box::new(ObtainRaidLeaderBannerGoal)); goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0))); goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, true))); diff --git a/crates/pumpkin/src/entity/mob/zombie/drowned.rs b/crates/pumpkin/src/entity/mob/zombie/drowned.rs index 8274f2c97..62f47884d 100644 --- a/crates/pumpkin/src/entity/mob/zombie/drowned.rs +++ b/crates/pumpkin/src/entity/mob/zombie/drowned.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::{ - Entity, + Entity, NbtFuture, mob::{Mob, MobEntity}, }; +use pumpkin_nbt::compound::NbtCompound; pub struct DrownedEntity { entity: Arc, @@ -32,10 +33,29 @@ impl DrownedEntity { mob_arc } + + #[must_use] + pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc { + let entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors); + let zombie = Self { entity }; + Arc::new(zombie) + } } impl Mob for DrownedEntity { fn get_mob_entity(&self) -> &MobEntity { &self.entity.mob_entity } + + fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.mob_write_nbt(nbt).await; + }) + } + + fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.mob_read_nbt(nbt).await; + }) + } } diff --git a/crates/pumpkin/src/entity/mob/zombie/husk.rs b/crates/pumpkin/src/entity/mob/zombie/husk.rs index 155cd525c..934ee61e2 100644 --- a/crates/pumpkin/src/entity/mob/zombie/husk.rs +++ b/crates/pumpkin/src/entity/mob/zombie/husk.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::{ - Entity, + Entity, NbtFuture, mob::{Mob, MobEntity}, }; +use pumpkin_nbt::compound::NbtCompound; pub struct HuskEntity { entity: Arc, @@ -16,10 +17,29 @@ impl HuskEntity { let zombie = Self { entity }; Arc::new(zombie) } + + #[must_use] + pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc { + let entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors); + let zombie = Self { entity }; + Arc::new(zombie) + } } impl Mob for HuskEntity { fn get_mob_entity(&self) -> &MobEntity { &self.entity.mob_entity } + + fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.mob_write_nbt(nbt).await; + }) + } + + fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.mob_read_nbt(nbt).await; + }) + } } diff --git a/crates/pumpkin/src/entity/mob/zombie/mod.rs b/crates/pumpkin/src/entity/mob/zombie/mod.rs index 8a2b57b7b..78375f031 100644 --- a/crates/pumpkin/src/entity/mob/zombie/mod.rs +++ b/crates/pumpkin/src/entity/mob/zombie/mod.rs @@ -1,4 +1,6 @@ use super::{Mob, MobEntity}; +use crate::entity::NbtFuture; +use crate::entity::ai::goal::break_door::BreakDoorGoal; use crate::entity::ai::goal::destroy_egg::DestroyEggGoal; use crate::entity::ai::goal::look_around::RandomLookAroundGoal; use crate::entity::ai::goal::revenge::RevengeGoal; @@ -7,9 +9,11 @@ use crate::entity::ai::goal::wander_around::WanderAroundGoal; use crate::entity::ai::goal::zombie_attack::ZombieAttackGoal; use crate::entity::{ Entity, - ai::goal::{active_target::ActiveTargetGoal, look_at_entity::LookAtEntityGoal}, + ai::goal::{Goal, active_target::ActiveTargetGoal, look_at_entity::LookAtEntityGoal}, }; use pumpkin_data::entity::EntityType; +use pumpkin_nbt::compound::NbtCompound; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; pub mod drowned; @@ -20,12 +24,20 @@ pub mod zombie_villager; pub struct ZombieEntityBase { pub mob_entity: MobEntity, + pub can_break_doors: AtomicBool, } impl ZombieEntityBase { pub fn new(entity: Entity) -> Arc { + Self::with_can_break_doors(entity, false) + } + + pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc { let mob_entity = MobEntity::new(entity); - let zombie = Self { mob_entity }; + let zombie = Self { + mob_entity, + can_break_doors: AtomicBool::new(can_break_doors), + }; let mob_arc = Arc::new(zombie); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -45,6 +57,9 @@ impl ZombieEntityBase { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); + if can_break_doors { + goal_selector.add_goal(1, Box::new(BreakDoorGoal::default())); + } goal_selector.add_goal(2, ZombieAttackGoal::new(1.0, false)); goal_selector.add_goal(4, DestroyEggGoal::new(1.0, 3)); goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(1.0))); @@ -75,10 +90,56 @@ impl ZombieEntityBase { mob_arc } + + #[must_use] + pub fn can_break_doors(&self) -> bool { + self.can_break_doors.load(Ordering::Relaxed) + } + + pub async fn set_can_break_doors(&self, can_break_doors: bool, mob: &dyn Mob) { + if self + .can_break_doors + .swap(can_break_doors, Ordering::Relaxed) + != can_break_doors + { + let mut stopped = { + let mut goal_selector = self + .mob_entity + .goals_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if can_break_doors { + goal_selector.add_goal(1, Box::new(BreakDoorGoal::default())); + Vec::new() + } else { + goal_selector.remove_goal_sync::() + } + }; + for goal in &mut stopped { + goal.stop(mob).await; + } + } + } } impl Mob for ZombieEntityBase { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + if self.can_break_doors() { + nbt.put_bool("CanBreakDoors", true); + } + }) + } + + fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + if let Some(can_break_doors) = nbt.get_bool("CanBreakDoors") { + self.set_can_break_doors(can_break_doors, self).await; + } + }) + } } diff --git a/crates/pumpkin/src/entity/mob/zombie/zombie.rs b/crates/pumpkin/src/entity/mob/zombie/zombie.rs index 0a3b248a4..7ab814d4d 100644 --- a/crates/pumpkin/src/entity/mob/zombie/zombie.rs +++ b/crates/pumpkin/src/entity/mob/zombie/zombie.rs @@ -1,6 +1,7 @@ -use crate::entity::Entity; use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::mob::{Mob, MobEntity}; +use crate::entity::{Entity, NbtFuture}; +use pumpkin_nbt::compound::NbtCompound; use std::sync::Arc; pub struct ZombieEntity { @@ -13,10 +14,29 @@ impl ZombieEntity { let zombie = Self { entity }; Arc::new(zombie) } + + #[must_use] + pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc { + let entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors); + let zombie = Self { entity }; + Arc::new(zombie) + } } impl Mob for ZombieEntity { fn get_mob_entity(&self) -> &MobEntity { &self.entity.mob_entity } + + fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.mob_write_nbt(nbt).await; + }) + } + + fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.mob_read_nbt(nbt).await; + }) + } } diff --git a/crates/pumpkin/src/entity/mob/zombie/zombie_villager.rs b/crates/pumpkin/src/entity/mob/zombie/zombie_villager.rs index 8a39dc2d4..0e42db918 100644 --- a/crates/pumpkin/src/entity/mob/zombie/zombie_villager.rs +++ b/crates/pumpkin/src/entity/mob/zombie/zombie_villager.rs @@ -1,6 +1,7 @@ -use crate::entity::Entity; use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::mob::{Mob, MobEntity}; +use crate::entity::{Entity, NbtFuture}; +use pumpkin_nbt::compound::NbtCompound; use std::sync::Arc; pub struct ZombieVillagerEntity { @@ -13,10 +14,29 @@ impl ZombieVillagerEntity { let zombie = Self { mob_entity }; Arc::new(zombie) } + + #[must_use] + pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc { + let mob_entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors); + let zombie = Self { mob_entity }; + Arc::new(zombie) + } } impl Mob for ZombieVillagerEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity.mob_entity } + + fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.mob_entity.mob_write_nbt(nbt).await; + }) + } + + fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.mob_entity.mob_read_nbt(nbt).await; + }) + } } diff --git a/crates/pumpkin/src/entity/passive/axolotl.rs b/crates/pumpkin/src/entity/passive/axolotl.rs index 2ef7d9f3b..945b71105 100644 --- a/crates/pumpkin/src/entity/passive/axolotl.rs +++ b/crates/pumpkin/src/entity/passive/axolotl.rs @@ -6,7 +6,7 @@ use crate::entity::{ Entity, ai::goal::{ look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; @@ -35,6 +35,7 @@ impl AxolotlEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( diff --git a/crates/pumpkin/src/entity/passive/dolphin.rs b/crates/pumpkin/src/entity/passive/dolphin.rs index ba89f0b61..50867bfe7 100644 --- a/crates/pumpkin/src/entity/passive/dolphin.rs +++ b/crates/pumpkin/src/entity/passive/dolphin.rs @@ -6,7 +6,7 @@ use crate::entity::{ Entity, ai::goal::{ look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; @@ -35,6 +35,7 @@ impl DolphinEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( diff --git a/crates/pumpkin/src/entity/passive/iron_golem.rs b/crates/pumpkin/src/entity/passive/iron_golem.rs index 8848192e4..91abb96a2 100644 --- a/crates/pumpkin/src/entity/passive/iron_golem.rs +++ b/crates/pumpkin/src/entity/passive/iron_golem.rs @@ -3,7 +3,7 @@ use std::sync::{ atomic::{AtomicBool, AtomicI32, Ordering}, }; -use pumpkin_data::entity::EntityType; +use pumpkin_data::entity::{EntityStatus, EntityType}; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; @@ -15,8 +15,8 @@ use crate::entity::{ Entity, EntityBase, EntityBaseFuture, NbtFuture, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, + offer_flower::OfferFlowerGoal, revenge::RevengeGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, player::Player, @@ -60,6 +60,7 @@ impl IronGolemEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(1, Box::new(MeleeAttackGoal::new(1.0, true))); + goal_selector.add_goal(5, Box::new(OfferFlowerGoal::new())); goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.6))); goal_selector.add_goal( 7, @@ -98,9 +99,29 @@ impl IronGolemEntity { None, ); } + + pub fn offer_flower(&self, offer: bool) { + let entity = self.get_entity(); + let world = entity.world.load(); + if offer { + self.offer_flower_tick.store(400, Ordering::Relaxed); + world.send_entity_status(entity, EntityStatus::OfferFlower, None); + } else { + self.offer_flower_tick.store(0, Ordering::Relaxed); + world.send_entity_status(entity, EntityStatus::StopOfferFlower, None); + } + } + + #[must_use] + pub fn get_offer_flower_tick(&self) -> i32 { + self.offer_flower_tick.load(Ordering::Relaxed) + } } impl Mob for IronGolemEntity { + fn as_iron_golem(&self) -> Option<&IronGolemEntity> { + Some(self) + } fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { nbt.put_bool("PlayerCreated", self.is_player_created()); diff --git a/crates/pumpkin/src/entity/passive/tadpole.rs b/crates/pumpkin/src/entity/passive/tadpole.rs index 0d327442a..5329291dd 100644 --- a/crates/pumpkin/src/entity/passive/tadpole.rs +++ b/crates/pumpkin/src/entity/passive/tadpole.rs @@ -6,7 +6,7 @@ use crate::entity::{ Entity, ai::goal::{ look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; @@ -32,6 +32,7 @@ impl TadpoleEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( diff --git a/crates/pumpkin/src/entity/passive/turtle.rs b/crates/pumpkin/src/entity/passive/turtle.rs index 8b8ff5325..aec7dedec 100644 --- a/crates/pumpkin/src/entity/passive/turtle.rs +++ b/crates/pumpkin/src/entity/passive/turtle.rs @@ -6,7 +6,8 @@ use crate::entity::{ Entity, ai::goal::{ breed::BreedGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, - swim::SwimGoal, tempt::TemptGoal, wander_around::WanderAroundGoal, + swim::SwimGoal, tempt::TemptGoal, try_find_water::TryFindWaterGoal, + wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; @@ -34,6 +35,7 @@ impl TurtleEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); goal_selector.add_goal(1, Box::new(SwimGoal::default())); goal_selector.add_goal(2, BreedGoal::new(1.0)); goal_selector.add_goal(3, Box::new(TemptGoal::new(1.1, TEMPT_ITEMS))); diff --git a/crates/pumpkin/src/entity/passive/villager/mod.rs b/crates/pumpkin/src/entity/passive/villager/mod.rs index 2a5e5dcab..6e9b0d7cd 100644 --- a/crates/pumpkin/src/entity/passive/villager/mod.rs +++ b/crates/pumpkin/src/entity/passive/villager/mod.rs @@ -39,7 +39,7 @@ use crate::entity::{ ai::{ goal::{ avoid_entity::AvoidEntityGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, swim::SwimGoal, + look_at_entity::LookAtEntityGoal, open_door::OpenDoorGoal, swim::SwimGoal, trade_with_player::TradeWithPlayerGoal, wander_around::WanderAroundGoal, work_at_job_site::WorkAtJobSiteGoal, }, @@ -371,6 +371,7 @@ impl VillagerEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); + goal_selector.add_goal(0, Box::new(OpenDoorGoal::new(true))); // Villagers avoid threats goal_selector.add_goal( 1, diff --git a/crates/pumpkin/src/entity/passive/wolf.rs b/crates/pumpkin/src/entity/passive/wolf.rs index 4d1a946ec..4c90d95f0 100644 --- a/crates/pumpkin/src/entity/passive/wolf.rs +++ b/crates/pumpkin/src/entity/passive/wolf.rs @@ -175,6 +175,54 @@ impl Mob for WolfEntity { Some(self) } + fn can_attack_with_owner(&self, target: &dyn EntityBase, owner: &dyn EntityBase) -> bool { + let target_entity = target.get_entity(); + let target_type = target_entity.entity_type; + if *target_type == EntityType::CREEPER + || *target_type == EntityType::GHAST + || *target_type == EntityType::ARMOR_STAND + { + return false; + } + + if *target_type == EntityType::WOLF { + if let Some(target_mob) = target.get_mob() + && let Some(tamable) = target_mob.as_tamable() + && tamable.is_tame() + && let Some(target_owner) = tamable.get_owner() + && let Some(owner_player) = owner.get_player() + && target_owner == owner_player.gameprofile.id + { + return false; + } + return true; + } + + if *target_type == EntityType::PLAYER { + if let Some(owner_player) = owner.get_player() + && let Some(target_player) = target.get_player() + { + if owner_player.gameprofile.id == target_player.gameprofile.id { + return false; + } + let world = target_player.world(); + if !world.level_info.load().game_rules.pvp { + return false; + } + } + return true; + } + + if let Some(target_mob) = target.get_mob() + && let Some(tamable) = target_mob.as_tamable() + && tamable.is_tame() + { + return false; + } + + true + } + fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { let variant_str = match self.variant.load(Ordering::Relaxed) { diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs index 6f98f296a..965879b48 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs @@ -1042,7 +1042,7 @@ impl JavaClient { &mut payload, &version, )?; - let mut event = crate::plugin::api::events::player::custom_click_action::CustomClickActionEvent::new( + let mut event = crate::plugin::api::events::dialog::dialog_click_action::DialogClickActionEvent::new( player.clone(), packet.action_id.to_string(), packet.payload.map(Bytes::copy_from_slice), diff --git a/crates/pumpkin/src/plugin/api/events/dialog/dialog_clear.rs b/crates/pumpkin/src/plugin/api/events/dialog/dialog_clear.rs new file mode 100644 index 000000000..1e4d326b8 --- /dev/null +++ b/crates/pumpkin/src/plugin/api/events/dialog/dialog_clear.rs @@ -0,0 +1,30 @@ +use pumpkin_macros::{Event, cancellable}; +use std::sync::Arc; + +use crate::entity::player::Player; + +use super::super::player::PlayerEvent; + +/// An event that occurs when a dialog is cleared for a player. +#[cancellable] +#[derive(Event, Clone)] +pub struct DialogClearEvent { + /// The player whose dialog is being cleared. + pub player: Arc, +} + +impl DialogClearEvent { + #[must_use] + pub const fn new(player: Arc) -> Self { + Self { + player, + cancelled: false, + } + } +} + +impl PlayerEvent for DialogClearEvent { + fn get_player(&self) -> &Arc { + &self.player + } +} diff --git a/crates/pumpkin/src/plugin/api/events/player/custom_click_action.rs b/crates/pumpkin/src/plugin/api/events/dialog/dialog_click_action.rs similarity index 73% rename from crates/pumpkin/src/plugin/api/events/player/custom_click_action.rs rename to crates/pumpkin/src/plugin/api/events/dialog/dialog_click_action.rs index 7866048dc..fe558ee39 100644 --- a/crates/pumpkin/src/plugin/api/events/player/custom_click_action.rs +++ b/crates/pumpkin/src/plugin/api/events/dialog/dialog_click_action.rs @@ -1,14 +1,15 @@ use bytes::Bytes; -use pumpkin_macros::Event; +use pumpkin_macros::{Event, cancellable}; use std::sync::Arc; use crate::entity::player::Player; -use super::PlayerEvent; +use super::super::player::PlayerEvent; /// An event that occurs when a player clicks a custom dialog button. +#[cancellable] #[derive(Event, Clone)] -pub struct CustomClickActionEvent { +pub struct DialogClickActionEvent { /// The player who clicked the button. pub player: Arc, /// The unique identifier for the action. @@ -17,18 +18,19 @@ pub struct CustomClickActionEvent { pub payload: Option, } -impl CustomClickActionEvent { +impl DialogClickActionEvent { #[must_use] pub const fn new(player: Arc, id: String, payload: Option) -> Self { Self { player, id, payload, + cancelled: false, } } } -impl PlayerEvent for CustomClickActionEvent { +impl PlayerEvent for DialogClickActionEvent { fn get_player(&self) -> &Arc { &self.player } diff --git a/crates/pumpkin/src/plugin/api/events/dialog/dialog_show.rs b/crates/pumpkin/src/plugin/api/events/dialog/dialog_show.rs new file mode 100644 index 000000000..cb332ccf6 --- /dev/null +++ b/crates/pumpkin/src/plugin/api/events/dialog/dialog_show.rs @@ -0,0 +1,34 @@ +use pumpkin_macros::{Event, cancellable}; +use pumpkin_protocol::java::client::dialog::Dialog; +use std::sync::Arc; + +use crate::entity::player::Player; + +use super::super::player::PlayerEvent; + +/// An event that occurs when a dialog is shown to a player. +#[cancellable] +#[derive(Event, Clone)] +pub struct DialogShowEvent { + /// The player receiving the dialog. + pub player: Arc, + /// The dialog being shown. + pub dialog: Dialog, +} + +impl DialogShowEvent { + #[must_use] + pub const fn new(player: Arc, dialog: Dialog) -> Self { + Self { + player, + dialog, + cancelled: false, + } + } +} + +impl PlayerEvent for DialogShowEvent { + fn get_player(&self) -> &Arc { + &self.player + } +} diff --git a/crates/pumpkin/src/plugin/api/events/dialog/mod.rs b/crates/pumpkin/src/plugin/api/events/dialog/mod.rs new file mode 100644 index 000000000..8728fc1a1 --- /dev/null +++ b/crates/pumpkin/src/plugin/api/events/dialog/mod.rs @@ -0,0 +1,7 @@ +pub mod dialog_clear; +pub mod dialog_click_action; +pub mod dialog_show; + +pub use dialog_clear::*; +pub use dialog_click_action::*; +pub use dialog_show::*; diff --git a/crates/pumpkin/src/plugin/api/events/mod.rs b/crates/pumpkin/src/plugin/api/events/mod.rs index 6cc83a8fc..cbd47122a 100644 --- a/crates/pumpkin/src/plugin/api/events/mod.rs +++ b/crates/pumpkin/src/plugin/api/events/mod.rs @@ -2,6 +2,7 @@ use std::any::Any; use std::sync::Arc; pub mod block; +pub mod dialog; pub mod enchantment; pub mod entity; pub mod hanging; diff --git a/crates/pumpkin/src/plugin/api/events/player/mod.rs b/crates/pumpkin/src/plugin/api/events/player/mod.rs index face5fcd7..a6aa3563f 100644 --- a/crates/pumpkin/src/plugin/api/events/player/mod.rs +++ b/crates/pumpkin/src/plugin/api/events/player/mod.rs @@ -2,7 +2,6 @@ pub mod async_player_chat; pub mod async_player_pre_login; pub mod bedrock_form_response; pub mod changed_main_hand; -pub mod custom_click_action; pub mod egg_throw; pub mod exp_change; pub mod fish; @@ -79,7 +78,6 @@ pub use async_player_chat::*; pub use async_player_pre_login::*; pub use bedrock_form_response::*; pub use changed_main_hand::*; -pub use custom_click_action::*; pub use egg_throw::*; pub use exp_change::*; pub use fish::*; diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs index 36a647ad8..f5e3121ac 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs @@ -48,8 +48,8 @@ fn register_player_event( ) { use crate::plugin::player::{ bedrock_form_response::BedrockFormResponseEvent, - changed_main_hand::PlayerChangedMainHandEvent, custom_click_action::CustomClickActionEvent, - egg_throw::PlayerEggThrowEvent, exp_change::PlayerExpChangeEvent, fish::PlayerFishEvent, + changed_main_hand::PlayerChangedMainHandEvent, egg_throw::PlayerEggThrowEvent, + exp_change::PlayerExpChangeEvent, fish::PlayerFishEvent, inventory_close::InventoryCloseEvent, inventory_interact::InventoryClickEvent, item_held::PlayerItemHeldEvent, player_change_world::PlayerChangeWorldEvent, player_chat::PlayerChatEvent, player_command_send::PlayerCommandSendEvent, @@ -155,9 +155,6 @@ fn register_player_event( EventType::BedrockFormResponseEvent => { register_typed_event::(resource, handler, priority, blocking); } - EventType::CustomClickActionEvent => { - register_typed_event::(resource, handler, priority, blocking); - } EventType::PlayerItemConsumeEvent => { register_typed_event::< crate::plugin::api::events::player::player_item_consume::PlayerItemConsumeEvent, @@ -1368,6 +1365,33 @@ fn register_raid_event( } } +fn register_dialog_event( + resource: &ContextResource, + handler: &Arc, + priority: crate::plugin::EventPriority, + blocking: bool, + event_type: EventType, +) { + use crate::plugin::api::events::dialog::{ + DialogClearEvent, DialogClickActionEvent, DialogShowEvent, + }; + + match event_type { + EventType::DialogClickActionEvent => { + register_typed_event::(resource, handler, priority, blocking); + } + EventType::DialogShowEvent => { + register_typed_event::(resource, handler, priority, blocking); + } + EventType::DialogClearEvent => { + register_typed_event::(resource, handler, priority, blocking); + } + _ => { + tracing::error!("non-dialog event should not be routed to register_dialog_event"); + } + } +} + fn register_server_event( resource: &ContextResource, handler: &Arc, @@ -1721,6 +1745,11 @@ impl pumpkin::plugin::context::HostContext for PluginHostState { | EventType::RaidTriggerEvent) => { register_raid_event(resource, &handler, priority, blocking, event_type); } + event_type @ (EventType::DialogClickActionEvent + | EventType::DialogShowEvent + | EventType::DialogClearEvent) => { + register_dialog_event(resource, &handler, priority, blocking, event_type); + } event_type => { register_player_event(resource, &handler, priority, blocking, event_type); } diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs index ada5d1471..8a1522c95 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs @@ -23,7 +23,9 @@ use crate::plugin::loader::wasm::wasm_host::{ uuid::Uuid, world::{ BlockPos as WitBlockPos, BoundingBox as WitBoundingBox, Entity, - EquipmentSlot as WitEquipmentSlot, HostEntity, RaycastResult as WitRaycastResult, + EquipmentSlot as WitEquipmentSlot, HostEntity, + RayTraceBlockResult as WitRayTraceBlockResult, + RayTraceEntityResult as WitRayTraceEntityResult, RaycastResult as WitRaycastResult, World, }, }, @@ -1281,7 +1283,7 @@ impl HostEntity for PluginHostState { &mut self, entity: Resource, max_distance: f64, - _fluid_handling: bool, + fluid_handling: bool, ) -> wasmtime::Result> { let entity = entity_from_resource(self, &entity)?; let start = entity.get_eye_pos(); @@ -1289,22 +1291,9 @@ impl HostEntity for PluginHostState { let end = start + direction * max_distance; let world = entity.get_entity().world.load_full(); - let hit = world - .raycast( - start, - end, - |pos: &pumpkin_util::math::position::BlockPos, w: &Arc| { - let pos = *pos; - let world = w.clone(); - async move { - let block = world.get_block_state(&pos); - !block.is_air() - } - }, - ) - .await; + let hit = world.ray_trace_block(start, end, fluid_handling); - Ok(hit.map(|(pos, face)| WitRaycastResult { + Ok(hit.map(|(pos, face, _)| WitRaycastResult { pos: WitBlockPos { x: pos.0.x, y: pos.0.y, @@ -1314,6 +1303,69 @@ impl HostEntity for PluginHostState { })) } + async fn ray_trace_block( + &mut self, + entity: Resource, + max_distance: f64, + include_fluids: bool, + ) -> wasmtime::Result> { + let entity = entity_from_resource(self, &entity)?; + let start = entity.get_eye_pos(); + let direction = entity.get_looking_vector(); + let end = start + direction * max_distance; + let world = entity.get_entity().world.load_full(); + + let hit = world.ray_trace_block(start, end, include_fluids); + + Ok(hit.map(|(pos, face, hit_pos)| WitRayTraceBlockResult { + pos: WitBlockPos { + x: pos.0.x, + y: pos.0.y, + z: pos.0.z, + }, + face: to_wasm_block_direction(face), + hit_pos: to_wasm_position(hit_pos), + })) + } + + async fn ray_trace_entity( + &mut self, + entity: Resource, + max_distance: f64, + ) -> wasmtime::Result> { + let entity_base = entity_from_resource(self, &entity)?; + let start = entity_base.get_eye_pos(); + let direction = entity_base.get_looking_vector(); + let end = start + direction * max_distance; + let world = entity_base.get_entity().world.load_full(); + let self_id = entity_base.get_entity().entity_id; + + let hits = world.ray_trace_entities(start, end); + for (hit_entity, hit_pos, distance) in hits { + if hit_entity.get_entity().entity_id != self_id { + let entity_res = self + .add_entity(hit_entity) + .map_err(|_| wasmtime::Error::msg("failed to add entity resource"))?; + return Ok(Some(WitRayTraceEntityResult { + entity: entity_res, + hit_pos: to_wasm_position(hit_pos), + distance, + })); + } + } + + Ok(None) + } + + async fn get_target_entity( + &mut self, + entity: Resource, + max_distance: f64, + ) -> wasmtime::Result>> { + let res = self.ray_trace_entity(entity, max_distance).await?; + Ok(res.map(|r| r.entity)) + } + async fn set_custom_data( &mut self, this: Resource, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/cleanup.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/cleanup.rs index 97206a23c..d3b133211 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/cleanup.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/cleanup.rs @@ -164,9 +164,53 @@ pub fn cleanup_event(event: &Event, state: &mut PluginHostState) { Event::BedrockFormResponseEvent(data) => { cleanup_player(state, &data.player); } - Event::CustomClickActionEvent(data) => { + Event::DialogClickActionEvent(data) => { cleanup_player(state, &data.player); } + Event::DialogClearEvent(data) => { + cleanup_player(state, &data.player); + } + Event::DialogShowEvent(data) => { + cleanup_player(state, &data.player); + cleanup_text_component(state, &data.dialog.title); + for body in &data.dialog.body { + match body { + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogBody::PlainMessage(c) => cleanup_text_component(state, c), + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogBody::Item(item) => cleanup_item_stack(state, item), + } + } + for input in &data.dialog.inputs { + use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogInput; + match input { + DialogInput::Bool(b) => cleanup_text_component(state, &b.label), + DialogInput::Text(t) => { + cleanup_text_component(state, &t.label); + cleanup_text_component(state, &t.placeholder); + } + DialogInput::NumberRange(n) => cleanup_text_component(state, &n.label), + DialogInput::SingleOption(s) => { + cleanup_text_component(state, &s.label); + for opt in &s.options { + cleanup_text_component(state, opt); + } + } + } + } + for button in &data.dialog.buttons { + cleanup_text_component(state, &button.text); + if let Some(tooltip) = &button.tooltip { + cleanup_text_component(state, tooltip); + } + } + for link in &data.dialog.links { + if let crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::LinkLabel::Custom(c) = &link.label { + cleanup_text_component(state, c); + } + } + if let Some(ext_title) = &data.dialog.external_title { + cleanup_text_component(state, ext_title); + } + } Event::ServerCommandEvent(_) => {} Event::ServerListPingEvent(data) => { cleanup_text_component(state, &data.motd); diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/dialog.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/dialog.rs new file mode 100644 index 000000000..b152f5497 --- /dev/null +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/dialog.rs @@ -0,0 +1,428 @@ +use bytes::Bytes; +use pumpkin_protocol::java::client::dialog::{ + ActionButton as ProtocolActionButton, Dialog as ProtocolDialog, DialogAction, + DialogBody as ProtocolDialogBody, DialogInput as ProtocolDialogInput, DialogLink, +}; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::plugin::api::events::dialog::{ + DialogClearEvent, DialogClickActionEvent, DialogShowEvent, +}; +use crate::plugin::loader::wasm::wasm_host::{ + state::PluginHostState, + wit::v0_1::{ + events::{ToFromWasmEvent, consume_player}, + player::text_component_from_resource, + pumpkin::plugin::{ + event::{DialogClearEventData, DialogClickActionEventData, DialogShowEventData, Event}, + java_dialogs::{ + Action, ActionButton, AfterAction, CustomClickAction, Dialog, DialogBody, + DialogInput, DialogInputBool, DialogInputNumberRange, DialogInputSingleOption, + DialogInputText, DialogType, Link, LinkLabel, LinkType, + }, + }, + }, +}; + +#[allow(clippy::too_many_lines)] +pub(crate) fn protocol_dialog_from_wasm( + state: &PluginHostState, + dialog: &Dialog, +) -> ProtocolDialog { + let title = text_component_from_resource(state, &dialog.title); + + let body: Vec<_> = dialog + .body + .iter() + .map(|b| match b { + DialogBody::PlainMessage(c) => ProtocolDialogBody::PlainMessage { + contents: text_component_from_resource(state, c), + }, + DialogBody::Item(_i) => ProtocolDialogBody::Item { item: 0 }, + }) + .collect(); + + let inputs: Vec<_> = dialog + .inputs + .iter() + .map(|i| match i { + DialogInput::Bool(b) => ProtocolDialogInput::Boolean { + label: text_component_from_resource(state, &b.label), + default_value: b.default_value, + }, + DialogInput::Text(t) => ProtocolDialogInput::Text { + label: text_component_from_resource(state, &t.label), + placeholder: text_component_from_resource(state, &t.placeholder), + default_value: t.default_value.clone(), + }, + DialogInput::NumberRange(n) => ProtocolDialogInput::NumberRange { + label: text_component_from_resource(state, &n.label), + min: n.min_value, + max: n.max_value, + initial: n.initial_value, + step: n.step, + label_format: n.label_format.clone(), + }, + DialogInput::SingleOption(s) => ProtocolDialogInput::SingleOption { + label: text_component_from_resource(state, &s.label), + options: s + .options + .iter() + .map(|o| text_component_from_resource(state, o)) + .collect(), + initial_index: s.initial_index, + }, + }) + .collect(); + + let buttons: Vec<_> = dialog + .buttons + .iter() + .map(|b| ProtocolActionButton { + text: text_component_from_resource(state, &b.text), + tooltip: b + .tooltip + .as_ref() + .map(|t| text_component_from_resource(state, t)), + width: b.width, + action: match &b.action { + Action::OpenUrl(u) => DialogAction::OpenUrl { url: u.clone() }, + Action::CustomClick(c) => DialogAction::Custom { + id: c.id.clone(), + payload: c.payload.clone(), + }, + }, + }) + .collect(); + + let links: Vec<_> = dialog + .links + .iter() + .map(|l| { + let label = match &l.label { + LinkLabel::BuiltIn(t) => { + let link_type = match t { + LinkType::BugReport => pumpkin_protocol::LinkType::BugReport, + LinkType::CommunityGuidelines => { + pumpkin_protocol::LinkType::CommunityGuidelines + } + LinkType::Support => pumpkin_protocol::LinkType::Support, + LinkType::Status => pumpkin_protocol::LinkType::Status, + LinkType::Feedback => pumpkin_protocol::LinkType::Feedback, + LinkType::Community => pumpkin_protocol::LinkType::Community, + LinkType::Website => pumpkin_protocol::LinkType::Website, + LinkType::Forums => pumpkin_protocol::LinkType::Forums, + LinkType::News => pumpkin_protocol::LinkType::News, + LinkType::Announcements => pumpkin_protocol::LinkType::Announcements, + }; + pumpkin_protocol::Label::BuiltIn(link_type) + } + LinkLabel::Custom(c) => pumpkin_protocol::Label::TextComponent(Box::new( + text_component_from_resource(state, c), + )), + }; + DialogLink { + label, + url: l.url.clone(), + } + }) + .collect(); + + ProtocolDialog { + r#type: match dialog.type_ { + DialogType::Notice => "minecraft:notice".to_string(), + DialogType::Confirmation => "minecraft:confirmation".to_string(), + DialogType::MultiAction => "minecraft:multi_action".to_string(), + DialogType::DialogList => "minecraft:dialog_list".to_string(), + DialogType::ServerLinks => "minecraft:server_links".to_string(), + }, + title, + body, + inputs, + buttons, + links, + exit_action: None, + after_action: dialog.after_action.map(|a| match a { + AfterAction::Peek => "peek".to_string(), + AfterAction::Pop => "pop".to_string(), + }), + can_close_with_escape: dialog.can_close_with_escape, + external_title: dialog + .external_title + .as_ref() + .map(|t| text_component_from_resource(state, t)), + } +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn protocol_dialog_to_wasm( + state: &mut PluginHostState, + dialog: &ProtocolDialog, +) -> Dialog { + let title = state + .add_text_component(dialog.title.clone()) + .expect("failed to add text component"); + + let type_ = match dialog.r#type.as_str() { + "minecraft:confirmation" => DialogType::Confirmation, + "minecraft:multi_action" => DialogType::MultiAction, + "minecraft:dialog_list" => DialogType::DialogList, + "minecraft:server_links" => DialogType::ServerLinks, + _ => DialogType::Notice, + }; + + let body = dialog + .body + .iter() + .map(|b| match b { + ProtocolDialogBody::PlainMessage { contents } => { + let comp = state + .add_text_component(contents.clone()) + .expect("failed to add text component"); + DialogBody::PlainMessage(comp) + } + ProtocolDialogBody::Item { item: _ } => { + let item_res = state + .add_item_stack(Arc::new(Mutex::new( + pumpkin_data::item_stack::ItemStack::new(0, &pumpkin_data::item::Item::AIR), + ))) + .expect("failed to add item stack resource"); + DialogBody::Item(item_res) + } + }) + .collect(); + + let inputs = dialog + .inputs + .iter() + .map(|i| match i { + ProtocolDialogInput::Boolean { + label, + default_value, + } => { + let lbl = state + .add_text_component(label.clone()) + .expect("failed to add text component"); + DialogInput::Bool(DialogInputBool { + label: lbl, + default_value: *default_value, + }) + } + ProtocolDialogInput::Text { + label, + placeholder, + default_value, + } => { + let lbl = state + .add_text_component(label.clone()) + .expect("failed to add text component"); + let ph = state + .add_text_component(placeholder.clone()) + .expect("failed to add text component"); + DialogInput::Text(DialogInputText { + label: lbl, + placeholder: ph, + default_value: default_value.clone(), + }) + } + ProtocolDialogInput::NumberRange { + label, + min, + max, + initial, + step, + label_format, + } => { + let lbl = state + .add_text_component(label.clone()) + .expect("failed to add text component"); + DialogInput::NumberRange(DialogInputNumberRange { + label: lbl, + min_value: *min, + max_value: *max, + initial_value: *initial, + step: *step, + label_format: label_format.clone(), + }) + } + ProtocolDialogInput::SingleOption { + label, + options, + initial_index, + } => { + let lbl = state + .add_text_component(label.clone()) + .expect("failed to add text component"); + let opts = options + .iter() + .map(|o| { + state + .add_text_component(o.clone()) + .expect("failed to add text component") + }) + .collect(); + DialogInput::SingleOption(DialogInputSingleOption { + label: lbl, + options: opts, + initial_index: *initial_index, + }) + } + }) + .collect(); + + let buttons = dialog + .buttons + .iter() + .map(|b| { + let text = state + .add_text_component(b.text.clone()) + .expect("failed to add text component"); + let tooltip = b.tooltip.as_ref().map(|t| { + state + .add_text_component(t.clone()) + .expect("failed to add text component") + }); + let action = match &b.action { + DialogAction::OpenUrl { url } => Action::OpenUrl(url.clone()), + DialogAction::Custom { id, payload } => Action::CustomClick(CustomClickAction { + id: id.clone(), + payload: payload.clone(), + }), + }; + ActionButton { + text, + tooltip, + width: b.width, + action, + } + }) + .collect(); + + let links = dialog + .links + .iter() + .map(|l| { + let label = match &l.label { + pumpkin_protocol::Label::BuiltIn(t) => LinkLabel::BuiltIn(match t { + pumpkin_protocol::LinkType::BugReport => LinkType::BugReport, + pumpkin_protocol::LinkType::CommunityGuidelines => { + LinkType::CommunityGuidelines + } + pumpkin_protocol::LinkType::Support => LinkType::Support, + pumpkin_protocol::LinkType::Status => LinkType::Status, + pumpkin_protocol::LinkType::Feedback => LinkType::Feedback, + pumpkin_protocol::LinkType::Community => LinkType::Community, + pumpkin_protocol::LinkType::Website => LinkType::Website, + pumpkin_protocol::LinkType::Forums => LinkType::Forums, + pumpkin_protocol::LinkType::News => LinkType::News, + pumpkin_protocol::LinkType::Announcements => LinkType::Announcements, + }), + pumpkin_protocol::Label::TextComponent(c) => { + let comp = state + .add_text_component((**c).clone()) + .expect("failed to add text component"); + LinkLabel::Custom(comp) + } + }; + Link { + label, + url: l.url.clone(), + } + }) + .collect(); + + let external_title = dialog.external_title.as_ref().map(|t| { + state + .add_text_component(t.clone()) + .expect("failed to add text component") + }); + + Dialog { + title, + type_, + body, + inputs, + buttons, + links, + after_action: dialog.after_action.as_deref().map(|a| match a { + "peek" => AfterAction::Peek, + _ => AfterAction::Pop, + }), + can_close_with_escape: dialog.can_close_with_escape, + external_title, + } +} + +impl ToFromWasmEvent for DialogClickActionEvent { + fn to_wasm_event(&self, state: &mut PluginHostState) -> Event { + Event::DialogClickActionEvent(DialogClickActionEventData { + player: state + .add_player(self.player.clone()) + .expect("failed to add player resource"), + id: self.id.clone(), + payload: self.payload.as_ref().map(|p| p.to_vec()), + cancelled: self.cancelled, + }) + } + + fn from_wasm_event(event: Event, state: &mut PluginHostState) -> Self { + match event { + Event::DialogClickActionEvent(data) => Self { + player: consume_player(state, &data.player), + id: data.id, + payload: data.payload.map(Bytes::from), + cancelled: data.cancelled, + }, + _ => panic!("unexpected event type"), + } + } +} + +impl ToFromWasmEvent for DialogClearEvent { + fn to_wasm_event(&self, state: &mut PluginHostState) -> Event { + Event::DialogClearEvent(DialogClearEventData { + player: state + .add_player(self.player.clone()) + .expect("failed to add player resource"), + cancelled: self.cancelled, + }) + } + + fn from_wasm_event(event: Event, state: &mut PluginHostState) -> Self { + match event { + Event::DialogClearEvent(data) => Self { + player: consume_player(state, &data.player), + cancelled: data.cancelled, + }, + _ => panic!("unexpected event type"), + } + } +} + +impl ToFromWasmEvent for DialogShowEvent { + fn to_wasm_event(&self, state: &mut PluginHostState) -> Event { + let dialog = protocol_dialog_to_wasm(state, &self.dialog); + Event::DialogShowEvent(DialogShowEventData { + player: state + .add_player(self.player.clone()) + .expect("failed to add player resource"), + dialog, + cancelled: self.cancelled, + }) + } + + fn from_wasm_event(event: Event, state: &mut PluginHostState) -> Self { + match event { + Event::DialogShowEvent(data) => { + let dialog = protocol_dialog_from_wasm(state, &data.dialog); + Self { + player: consume_player(state, &data.player), + dialog, + cancelled: data.cancelled, + } + } + _ => panic!("unexpected event type"), + } + } +} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs index 0d0809c77..5fccef1c9 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs @@ -25,6 +25,7 @@ use crate::{ pub mod block; pub mod cleanup; +pub mod dialog; pub mod enchantment; pub mod entity; pub mod hanging; diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs index fd5bd6e48..749b0a823 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use tokio::sync::Mutex; -use crate::plugin::api::events::player::custom_click_action::CustomClickActionEvent; use crate::plugin::{ loader::wasm::wasm_host::{ state::PluginHostState, @@ -17,12 +16,11 @@ use crate::plugin::{ gui::{from_wit_screen, to_wit_screen}, pumpkin::plugin::event::{ AsyncPlayerChatEventData, AsyncPlayerPreLoginEventData, - BedrockFormResponseEventData, CustomClickActionEventData, Event, - InteractAction as WasmInteractAction, InventoryClickEventData, - InventoryCloseEventData, PlayerAdvancementDoneEventData, PlayerAnimationEventData, - PlayerArmorStandManipulateEventData, PlayerBedEnterEventData, - PlayerBedLeaveEventData, PlayerBucketEmptyEventData, PlayerBucketEntityEventData, - PlayerBucketFillEventData, PlayerChangeWorldEventData, + BedrockFormResponseEventData, Event, InteractAction as WasmInteractAction, + InventoryClickEventData, InventoryCloseEventData, PlayerAdvancementDoneEventData, + PlayerAnimationEventData, PlayerArmorStandManipulateEventData, + PlayerBedEnterEventData, PlayerBedLeaveEventData, PlayerBucketEmptyEventData, + PlayerBucketEntityEventData, PlayerBucketFillEventData, PlayerChangeWorldEventData, PlayerChangedMainHandEventData, PlayerChangedWorldEventData, PlayerChannelEventData, PlayerChatEventData, PlayerCommandPreprocessEventData, PlayerCommandSendEventData, PlayerCustomPayloadEventData, PlayerDropItemEventData, @@ -892,29 +890,6 @@ impl ToFromWasmEvent for BedrockFormResponseEvent { } } -impl ToFromWasmEvent for CustomClickActionEvent { - fn to_wasm_event(&self, state: &mut PluginHostState) -> Event { - Event::CustomClickActionEvent(CustomClickActionEventData { - player: state - .add_player(self.player.clone()) - .expect("failed to add player resource"), - id: self.id.clone(), - payload: self.payload.as_ref().map(|p| p.to_vec()), - }) - } - - fn from_wasm_event(event: Event, state: &mut PluginHostState) -> Self { - match event { - Event::CustomClickActionEvent(data) => Self { - player: consume_player(state, &data.player), - id: data.id, - payload: data.payload.map(Bytes::from), - }, - _ => panic!("unexpected event type"), - } - } -} - impl ToFromWasmEvent for PlayerInteractEntityEvent { fn to_wasm_event(&self, state: &mut PluginHostState) -> Event { let player = state diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 8c79eaaa9..d3baba376 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -5,9 +5,7 @@ use wasmtime::component::Resource; use crate::plugin::api::gui::PluginScreenHandler; use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::forms::Form; -use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::{ - Action, AfterAction, Dialog, DialogBody, DialogInput, LinkLabel, LinkType, -}; +use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::Dialog; use crate::{ entity::{EntityBase, player::TitleMode}, net::DisconnectReason, @@ -39,10 +37,7 @@ use crate::{ use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_protocol::Property; use pumpkin_protocol::bedrock::client::modal_form_request::CModalFormRequest; -use pumpkin_protocol::java::client::dialog::{ - ActionButton as ProtocolActionButton, Dialog as ProtocolDialog, DialogAction, - DialogBody as ProtocolDialogBody, DialogInput as ProtocolDialogInput, DialogLink, DialogNBT, -}; +use pumpkin_protocol::java::client::dialog::DialogNBT; use pumpkin_util::permission::PermissionLvl; use pumpkin_util::translation::Locale; use std::str::FromStr; @@ -1956,6 +1951,69 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(player.has_item_cooldown(&item_id).await) } + async fn ray_trace_block( + &mut self, + player: Resource, + max_distance: f64, + include_fluids: bool, + ) -> wasmtime::Result> { + let player = player_from_resource(self, &player)?; + let start = player.living_entity.entity.get_eye_pos(); + let direction = player.living_entity.entity.get_looking_vector(); + let end = start + direction * max_distance; + let world = player.living_entity.entity.world.load_full(); + + let hit = world.ray_trace_block(start, end, include_fluids); + + Ok(hit.map(|(pos, face, hit_pos)| pumpkin::plugin::world::RayTraceBlockResult { + pos: pumpkin::plugin::world::BlockPos { + x: pos.0.x, + y: pos.0.y, + z: pos.0.z, + }, + face: crate::plugin::loader::wasm::wasm_host::wit::v0_1::world::to_wasm_block_direction(face), + hit_pos: to_wasm_position(hit_pos), + })) + } + + async fn ray_trace_entity( + &mut self, + player: Resource, + max_distance: f64, + ) -> wasmtime::Result> { + let player = player_from_resource(self, &player)?; + let start = player.living_entity.entity.get_eye_pos(); + let direction = player.living_entity.entity.get_looking_vector(); + let end = start + direction * max_distance; + let world = player.living_entity.entity.world.load_full(); + let self_id = player.living_entity.entity.entity_id; + + let hits = world.ray_trace_entities(start, end); + for (hit_entity, hit_pos, distance) in hits { + if hit_entity.get_entity().entity_id != self_id { + let entity_res = self + .add_entity(hit_entity) + .map_err(|_| wasmtime::Error::msg("failed to add entity resource"))?; + return Ok(Some(pumpkin::plugin::world::RayTraceEntityResult { + entity: entity_res, + hit_pos: to_wasm_position(hit_pos), + distance, + })); + } + } + + Ok(None) + } + + async fn get_target_entity( + &mut self, + player: Resource, + max_distance: f64, + ) -> wasmtime::Result>> { + let res = self.ray_trace_entity(player, max_distance).await?; + Ok(res.map(|r| r.entity)) + } + async fn get_target_block( &mut self, player: Resource, @@ -1982,6 +2040,16 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { })) } + async fn get_target_block_exact( + &mut self, + player: Resource, + max_distance: f64, + include_fluids: bool, + ) -> wasmtime::Result> { + self.ray_trace_block(player, max_distance, include_fluids) + .await + } + async fn launch_projectile( &mut self, _player: Resource, @@ -2963,129 +3031,18 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { .provider .clone(); - let title = text_component_from_resource(self, &dialog.title); + let protocol_dialog = super::events::dialog::protocol_dialog_from_wasm(self, &dialog); - let body: Vec<_> = dialog - .body - .iter() - .map(|b| match b { - DialogBody::PlainMessage(c) => ProtocolDialogBody::PlainMessage { - contents: text_component_from_resource(self, c), - }, - DialogBody::Item(_i) => { - // TODO: Map ItemStack correctly - ProtocolDialogBody::Item { item: 0 } - } - }) - .collect(); - - let inputs: Vec<_> = dialog - .inputs - .iter() - .map(|i| match i { - DialogInput::Bool(b) => ProtocolDialogInput::Boolean { - label: text_component_from_resource(self, &b.label), - default_value: b.default_value, - }, - DialogInput::Text(t) => ProtocolDialogInput::Text { - label: text_component_from_resource(self, &t.label), - placeholder: text_component_from_resource(self, &t.placeholder), - default_value: t.default_value.clone(), - }, - DialogInput::NumberRange(n) => ProtocolDialogInput::NumberRange { - label: text_component_from_resource(self, &n.label), - min: n.min_value, - max: n.max_value, - initial: n.initial_value, - step: n.step, - label_format: n.label_format.clone(), - }, - DialogInput::SingleOption(s) => ProtocolDialogInput::SingleOption { - label: text_component_from_resource(self, &s.label), - options: s - .options - .iter() - .map(|o| text_component_from_resource(self, o)) - .collect(), - initial_index: s.initial_index, - }, - }) - .collect(); - - let buttons: Vec<_> = dialog - .buttons - .iter() - .map(|b| ProtocolActionButton { - text: text_component_from_resource(self, &b.text), - tooltip: b - .tooltip - .as_ref() - .map(|t| text_component_from_resource(self, t)), - width: b.width, - action: match &b.action { - Action::OpenUrl(u) => DialogAction::OpenUrl { url: u.clone() }, - Action::CustomClick(c) => DialogAction::Custom { - id: c.id.clone(), - payload: c.payload.clone(), - }, - }, - }) - .collect(); - - let links: Vec<_> = dialog - .links - .iter() - .map(|l| { - let label = match &l.label { - LinkLabel::BuiltIn(t) => { - let link_type = match t { - LinkType::BugReport => pumpkin_protocol::LinkType::BugReport, - LinkType::CommunityGuidelines => { - pumpkin_protocol::LinkType::CommunityGuidelines - } - LinkType::Support => pumpkin_protocol::LinkType::Support, - LinkType::Status => pumpkin_protocol::LinkType::Status, - LinkType::Feedback => pumpkin_protocol::LinkType::Feedback, - LinkType::Community => pumpkin_protocol::LinkType::Community, - LinkType::Website => pumpkin_protocol::LinkType::Website, - LinkType::Forums => pumpkin_protocol::LinkType::Forums, - LinkType::News => pumpkin_protocol::LinkType::News, - LinkType::Announcements => pumpkin_protocol::LinkType::Announcements, - }; - pumpkin_protocol::Label::BuiltIn(link_type) - } - LinkLabel::Custom(c) => pumpkin_protocol::Label::TextComponent(Box::new( - text_component_from_resource(self, c), - )), - }; - DialogLink { - label, - url: l.url.clone(), - } - }) - .collect(); - - let protocol_dialog = ProtocolDialog { - r#type: match dialog.type_ { - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogType::Notice => "minecraft:notice".to_string(), - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogType::Confirmation => "minecraft:confirmation".to_string(), - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogType::MultiAction => "minecraft:multi_action".to_string(), - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogType::DialogList => "minecraft:dialog_list".to_string(), - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::DialogType::ServerLinks => "minecraft:server_links".to_string(), - }, - title, - body, - inputs, - buttons, - links, - exit_action: None, // TODO - after_action: dialog.after_action.map(|a| match a { - AfterAction::Peek => "peek".to_string(), - AfterAction::Pop => "pop".to_string(), - }), - can_close_with_escape: dialog.can_close_with_escape, - external_title: dialog.external_title.as_ref().map(|t| text_component_from_resource(self, t)), - }; + if let Some(server) = player.world().server.upgrade() { + let mut event = crate::plugin::api::events::dialog::dialog_show::DialogShowEvent::new( + player.clone(), + protocol_dialog.clone(), + ); + server.plugin_manager.fire(&server, &mut event).await; + if event.cancelled { + return Ok(()); + } + } if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { match client.connection_state.load() { @@ -3127,6 +3084,16 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { .provider .clone(); + if let Some(server) = player.world().server.upgrade() { + let mut event = crate::plugin::api::events::dialog::dialog_clear::DialogClearEvent::new( + player.clone(), + ); + server.plugin_manager.fire(&server, &mut event).await; + if event.cancelled { + return Ok(()); + } + } + if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { match client.connection_state.load() { pumpkin_protocol::ConnectionState::Config => { diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/text.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/text.rs index ae2653906..62d009257 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/text.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/text.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::str::FromStr; use wasmtime::component::Resource; use crate::plugin::loader::wasm::wasm_host::{ @@ -16,6 +17,7 @@ use pumpkin_util::text::{ color::{self, Color}, hover::HoverEvent, }; +use pumpkin_util::translation::Locale; // --- Trapping Helpers --- impl PluginHostState { @@ -92,11 +94,101 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState { for r in with { components.push(self.take_text(&r)?.provider); } + #[allow(deprecated)] let tc = InternalTextComponent::translate(key, components); self.add_text_component(tc) .map_err(|_| wasmtime::Error::msg("Failed to add text component")) } + async fn translate_cross( + &mut self, + java_key: String, + bedrock_key: String, + with: Vec>, + ) -> wasmtime::Result> { + let mut components = Vec::with_capacity(with.len()); + for r in with { + components.push(self.take_text(&r)?.provider); + } + #[allow(deprecated)] + let tc = InternalTextComponent::translate_cross(java_key, bedrock_key, components); + self.add_text_component(tc) + .map_err(|_| wasmtime::Error::msg("Failed to add text component")) + } + + async fn entity_names( + &mut self, + selector: String, + separator: Option, + ) -> wasmtime::Result> { + let tc = InternalTextComponent::entity_names(selector, separator); + self.add_text_component(tc) + .map_err(|_| wasmtime::Error::msg("Failed to add text component")) + } + + async fn keybind(&mut self, keybind: String) -> wasmtime::Result> { + let tc = InternalTextComponent::keybind(keybind); + self.add_text_component(tc) + .map_err(|_| wasmtime::Error::msg("Failed to add text component")) + } + + async fn custom( + &mut self, + namespace: String, + key: String, + locale: String, + with: Vec>, + ) -> wasmtime::Result> { + let loc = Locale::from_str(&locale).unwrap_or(Locale::EnUs); + let mut components = Vec::with_capacity(with.len()); + for r in with { + components.push(self.take_text(&r)?.provider); + } + let tc = InternalTextComponent::custom(namespace, key, loc, components); + self.add_text_component(tc) + .map_err(|_| wasmtime::Error::msg("Failed to add text component")) + } + + async fn from_legacy_string( + &mut self, + input: String, + ) -> wasmtime::Result> { + let tc = InternalTextComponent::from_legacy_string(&input); + self.add_text_component(tc) + .map_err(|_| wasmtime::Error::msg("Failed to add text component")) + } + + async fn from_legacy_string_with_code( + &mut self, + input: String, + code_symbol: char, + ) -> wasmtime::Result> { + let tc = InternalTextComponent::from_legacy_string_with_code(&input, code_symbol); + self.add_text_component(tc) + .map_err(|_| wasmtime::Error::msg("Failed to add text component")) + } + + async fn from_json( + &mut self, + json: String, + ) -> wasmtime::Result, String>> { + match serde_json::from_str::(&json) { + Ok(tc) => match self.add_text_component(tc) { + Ok(res) => Ok(Ok(res)), + Err(err) => Ok(Err(err.to_string())), + }, + Err(err) => Ok(Err(err.to_string())), + } + } + + async fn to_json( + &mut self, + text_component: Resource, + ) -> wasmtime::Result { + let tc = &self.get_text_ref(&text_component)?.provider; + Ok(serde_json::to_string(tc).unwrap_or_default()) + } + async fn add_child( &mut self, text_component: Resource, @@ -104,7 +196,6 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState { ) -> wasmtime::Result<()> { let child_tc = self.take_text(&child)?.provider; let parent = self.get_text_mut(&text_component)?; - // Cloning here as noted in your TODO until builder pattern supports &mut self parent.provider = parent.provider.clone().add_child(child_tc); Ok(()) } @@ -141,6 +232,17 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState { .into_vec()) } + async fn to_pretty_console( + &mut self, + text_component: Resource, + ) -> wasmtime::Result { + Ok(self + .get_text_ref(&text_component)? + .provider + .clone() + .to_pretty_console()) + } + async fn color_named( &mut self, res: Resource, @@ -161,6 +263,37 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState { Ok(()) } + async fn gradient_named( + &mut self, + res: Resource, + colors: Vec, + ) -> wasmtime::Result<()> { + let mapped: Vec<_> = colors.into_iter().map(map_named_color).collect(); + let parent = self.get_text_mut(&res)?; + parent.provider = parent.provider.clone().gradient_named(&mapped); + Ok(()) + } + + async fn gradient( + &mut self, + res: Resource, + colors: Vec, + ) -> wasmtime::Result<()> { + let mapped: Vec<_> = colors + .into_iter() + .map(|c| color::RGBColor::new(c.r, c.g, c.b)) + .collect(); + let parent = self.get_text_mut(&res)?; + parent.provider = parent.provider.clone().gradient(&mapped); + Ok(()) + } + + async fn rainbow(&mut self, res: Resource) -> wasmtime::Result<()> { + let parent = self.get_text_mut(&res)?; + parent.provider = parent.provider.clone().rainbow(); + Ok(()) + } + async fn bold(&mut self, res: Resource, value: bool) -> wasmtime::Result<()> { self.get_text_mut(&res)?.provider.0.style.bold = Some(value); Ok(()) @@ -233,6 +366,17 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState { Ok(()) } + async fn click_open_file( + &mut self, + res: Resource, + path: String, + ) -> wasmtime::Result<()> { + self.get_text_mut(&res)?.provider.0.style.click_event = Some(ClickEvent::OpenFile { + path: Cow::Owned(path), + }); + Ok(()) + } + async fn click_run_command( &mut self, res: Resource, @@ -255,6 +399,16 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState { Ok(()) } + async fn click_change_page( + &mut self, + res: Resource, + page: u32, + ) -> wasmtime::Result<()> { + self.get_text_mut(&res)?.provider.0.style.click_event = + Some(ClickEvent::ChangePage { page }); + Ok(()) + } + async fn click_copy_to_clipboard( &mut self, res: Resource, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs index dc4e68e12..cb1dff667 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs @@ -1,6 +1,6 @@ use pumpkin_data::block_properties::NoteblockInstrument as InternalNoteblockInstrument; use pumpkin_data::block_state::PistonBehavior; -use pumpkin_data::{BlockDirection as InternalBlockDirection, BlockStateId}; +use pumpkin_data::{BlockDirection as InternalBlockDirection, BlockId, BlockStateId}; use pumpkin_util::math::position::BlockPos; use pumpkin_world::chunk::ChunkHeightmapType; use pumpkin_world::chunk::io::Dirtiable; @@ -64,11 +64,13 @@ use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::game_rul GameRule as WitGameRule, GameRuleValue as WitGameRuleValue, }; 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, BlockStateInfo as WitBlockStateInfo, - BoundingBox as WitBoundingBox, Chunk as WitChunk, - NoteblockInstrument as WitNoteblockInstrument, PistonBehavior as WitPistonBehavior, - WorldBorder as WitWorldBorder, + Block as WitBlock, BlockDirection as WitBlockDirection, BlockEntity, BlockEntityType, + BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState, + BlockStateInfo as WitBlockStateInfo, BoundingBox as WitBoundingBox, Chunk as WitChunk, + Flammable as WitFlammable, NoteblockInstrument as WitNoteblockInstrument, + PistonBehavior as WitPistonBehavior, RayTraceBlockResult as WitRayTraceBlockResult, + RayTraceEntityResult as WitRayTraceEntityResult, WorldBorder as WitWorldBorder, + WorldSpawnLocation as WitWorldSpawnLocation, }; use crate::plugin::loader::wasm::wasm_host::{ state::{ @@ -154,6 +156,93 @@ pub(crate) const fn to_wit_bounding_box( } } +pub(crate) fn to_wit_block(block: &pumpkin_data::Block) -> WitBlock { + WitBlock { + id: block.id.as_u16(), + name: block.name.to_string(), + hardness: block.hardness, + blast_resistance: block.blast_resistance, + map_color: block.map_color, + slipperiness: block.slipperiness, + velocity_multiplier: block.velocity_multiplier, + jump_velocity_multiplier: block.jump_velocity_multiplier, + item_id: block.item_id, + default_state_id: block.default_state.id.as_u16(), + state_ids: block.states.iter().map(|s| s.id.as_u16()).collect(), + is_solid: block.is_solid(), + is_air: block.is_air(), + is_flammable: block.flammable.is_some(), + flammable: block.flammable.as_ref().map(|f| WitFlammable { + spread_chance: f.spread_chance, + burn_chance: f.burn_chance, + }), + } +} + +pub(crate) fn to_wit_block_state( + state: &pumpkin_data::BlockState, + pos: Option<&BlockPos>, +) -> WitBlockState { + let dummy_pos = BlockPos::new(0, 0, 0); + let internal_pos = pos.unwrap_or(&dummy_pos); + let block = pumpkin_data::Block::from_state_id(state.id); + let properties = block + .properties(state.id) + .map(|p| { + p.to_props() + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }) + .unwrap_or_default(); + + WitBlockState { + id: state.id.as_u16(), + block_id: block.id.as_u16(), + block_name: block.name.to_string(), + luminance: state.luminance, + opacity: state.opacity, + hardness: state.hardness, + is_air: state.is_air(), + is_liquid: state.is_liquid(), + is_solid: state.is_solid(), + is_full_cube: state.is_full_cube(), + has_random_ticks: state.has_random_ticks(), + piston_behavior: match state.piston_behavior { + PistonBehavior::Normal => WitPistonBehavior::Normal, + PistonBehavior::Destroy => WitPistonBehavior::Destroy, + PistonBehavior::Block => WitPistonBehavior::Block, + PistonBehavior::Ignore => WitPistonBehavior::Ignore, + PistonBehavior::PushOnly => WitPistonBehavior::PushOnly, + }, + burnable: state.burnable(), + tool_required: state.tool_required(), + sided_transparency: state.sided_transparency(), + replaceable: state.replaceable(), + is_solid_block: state.is_solid_block(), + block_entity_type: state.block_entity_type, + instrument: to_wit_noteblock_instrument(state.instrument), + collision_shapes: state + .get_block_collision_shapes_at(internal_pos) + .map(to_wit_bounding_box) + .collect(), + outline_shapes: state + .get_block_outline_shapes_at(internal_pos) + .map(to_wit_bounding_box) + .collect(), + down_side_solid: state.is_side_solid(InternalBlockDirection::Down), + up_side_solid: state.is_side_solid(InternalBlockDirection::Up), + north_side_solid: state.is_side_solid(InternalBlockDirection::North), + south_side_solid: state.is_side_solid(InternalBlockDirection::South), + west_side_solid: state.is_side_solid(InternalBlockDirection::West), + east_side_solid: state.is_side_solid(InternalBlockDirection::East), + down_center_solid: state.is_center_solid(InternalBlockDirection::Down), + up_center_solid: state.is_center_solid(InternalBlockDirection::Up), + map_color: block.map_color, + properties, + } +} + // --- Trapping Helpers --- impl PluginHostState { pub(crate) fn get_world_res(&self, res: &Resource) -> wasmtime::Result<&WorldResource> { @@ -331,6 +420,141 @@ impl pumpkin::plugin::world::Host for PluginHostState { })); Ok(result.ok()) } + + async fn get_block_by_id(&mut self, id: u16) -> wasmtime::Result> { + let block_id = BlockId::new(id); + Ok(block_id.map(|id| to_wit_block(pumpkin_data::Block::from_id(id)))) + } + + async fn get_block_by_name(&mut self, name: String) -> wasmtime::Result> { + Ok(pumpkin_data::Block::from_name(&name).map(to_wit_block)) + } + + async fn get_all_blocks(&mut self) -> wasmtime::Result> { + let mut blocks = Vec::with_capacity(BlockId::COUNT as usize); + for raw_id in 0..BlockId::COUNT { + if let Some(id) = BlockId::new(raw_id) { + blocks.push(to_wit_block(pumpkin_data::Block::from_id(id))); + } + } + Ok(blocks) + } + + async fn get_all_block_names(&mut self) -> wasmtime::Result> { + let mut names = Vec::with_capacity(BlockId::COUNT as usize); + for raw_id in 0..BlockId::COUNT { + if let Some(id) = BlockId::new(raw_id) { + names.push(pumpkin_data::Block::from_id(id).name.to_string()); + } + } + Ok(names) + } + + async fn get_block_count(&mut self) -> wasmtime::Result { + Ok(BlockId::COUNT as u32) + } + + async fn get_block_state_count(&mut self) -> wasmtime::Result { + Ok(BlockStateId::COUNT as u32) + } + + async fn get_states_for_block( + &mut self, + block: WitBlock, + ) -> wasmtime::Result> { + let block_id = BlockId::new_or_air(block.id); + let block_ref = pumpkin_data::Block::from_id(block_id); + Ok(block_ref + .states + .iter() + .map(|s| to_wit_block_state(s, None)) + .collect()) + } + + async fn get_states_for_block_id( + &mut self, + block_id: u16, + ) -> wasmtime::Result> { + let Some(id) = BlockId::new(block_id) else { + return Ok(Vec::new()); + }; + let block_ref = pumpkin_data::Block::from_id(id); + Ok(block_ref + .states + .iter() + .map(|s| to_wit_block_state(s, None)) + .collect()) + } + + async fn get_state_ids_for_block_id(&mut self, block_id: u16) -> wasmtime::Result> { + let Some(id) = BlockId::new(block_id) else { + return Ok(Vec::new()); + }; + let block_ref = pumpkin_data::Block::from_id(id); + Ok(block_ref.states.iter().map(|s| s.id.as_u16()).collect()) + } + + async fn get_block_properties( + &mut self, + state_id: u16, + ) -> wasmtime::Result> { + let bsid = BlockStateId::new_or_air(state_id); + let block = pumpkin_data::Block::from_state_id(bsid); + let props = block + .properties(bsid) + .map(|p| { + p.to_props() + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }) + .unwrap_or_default(); + Ok(props) + } + + async fn get_block_from_state_id( + &mut self, + state_id: u16, + ) -> wasmtime::Result> { + let bsid = BlockStateId::new(state_id); + Ok(bsid.map(|id| to_wit_block(pumpkin_data::Block::from_state_id(id)))) + } + + async fn get_block_from_state(&mut self, state: WitBlockState) -> wasmtime::Result { + let bsid = BlockStateId::new_or_air(state.id); + Ok(to_wit_block(pumpkin_data::Block::from_state_id(bsid))) + } + + async fn get_default_state_from_block( + &mut self, + block: WitBlock, + ) -> wasmtime::Result { + let block_id = BlockId::new_or_air(block.id); + let block_ref = pumpkin_data::Block::from_id(block_id); + Ok(to_wit_block_state(block_ref.default_state, None)) + } + + async fn get_default_state_from_block_id( + &mut self, + block_id: u16, + ) -> wasmtime::Result> { + let block_id = BlockId::new(block_id); + Ok(block_id.map(|id| { + let block = pumpkin_data::Block::from_id(id); + to_wit_block_state(block.default_state, None) + })) + } + + async fn get_block_state_by_id( + &mut self, + state_id: u16, + ) -> wasmtime::Result> { + let bsid = BlockStateId::new(state_id); + Ok(bsid.map(|id| { + let state = pumpkin_data::BlockState::from_id(id); + to_wit_block_state(state, None) + })) + } } impl pumpkin::plugin::particles::Host for PluginHostState {} impl pumpkin::plugin::sounds::Host for PluginHostState {} @@ -344,6 +568,13 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { .to_string()) } + async fn get_border( + &mut self, + world: Resource, + ) -> wasmtime::Result> { + self.get_world_border(world).await + } + async fn get_world_border( &mut self, world: Resource, @@ -352,6 +583,22 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { self.add_world_border(world_res.provider.clone()) } + async fn get_spawn_location( + &mut self, + world: Resource, + ) -> wasmtime::Result { + let (pos, yaw, pitch) = self.get_world_res(&world)?.provider.get_spawn_location(); + Ok(WitWorldSpawnLocation { + pos: WitBlockPos { + x: pos.0.x, + y: pos.0.y, + z: pos.0.z, + }, + yaw, + pitch, + }) + } + async fn get_chunk( &mut self, world: Resource, @@ -396,49 +643,78 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { let world_ref = self.get_world_res(&world)?; let internal_pos = BlockPos::new(pos.x, pos.y, pos.z); let state = world_ref.provider.get_block_state(&internal_pos); + Ok(to_wit_block_state(state, Some(&internal_pos))) + } - Ok(WitBlockState { - id: state.id.as_u16(), - luminance: state.luminance, - opacity: state.opacity, - hardness: state.hardness, - is_air: state.is_air(), - is_liquid: state.is_liquid(), - is_solid: state.is_solid(), - is_full_cube: state.is_full_cube(), - has_random_ticks: state.has_random_ticks(), - piston_behavior: match state.piston_behavior { - PistonBehavior::Normal => WitPistonBehavior::Normal, - PistonBehavior::Destroy => WitPistonBehavior::Destroy, - PistonBehavior::Block => WitPistonBehavior::Block, - PistonBehavior::Ignore => WitPistonBehavior::Ignore, - PistonBehavior::PushOnly => WitPistonBehavior::PushOnly, - }, - burnable: state.burnable(), - tool_required: state.tool_required(), - sided_transparency: state.sided_transparency(), - replaceable: state.replaceable(), - is_solid_block: state.is_solid_block(), - block_entity_type: state.block_entity_type, - instrument: to_wit_noteblock_instrument(state.instrument), - collision_shapes: state - .get_block_collision_shapes_at(&internal_pos) - .map(to_wit_bounding_box) - .collect(), - outline_shapes: state - .get_block_outline_shapes_at(&internal_pos) - .map(to_wit_bounding_box) - .collect(), - down_side_solid: state.is_side_solid(InternalBlockDirection::Down), - up_side_solid: state.is_side_solid(InternalBlockDirection::Up), - north_side_solid: state.is_side_solid(InternalBlockDirection::North), - south_side_solid: state.is_side_solid(InternalBlockDirection::South), - west_side_solid: state.is_side_solid(InternalBlockDirection::West), - east_side_solid: state.is_side_solid(InternalBlockDirection::East), - down_center_solid: state.is_center_solid(InternalBlockDirection::Down), - up_center_solid: state.is_center_solid(InternalBlockDirection::Up), - map_color: pumpkin_data::Block::from_state_id(state.id).map_color, - }) + async fn get_block( + &mut self, + world: Resource, + pos: WitBlockPos, + ) -> wasmtime::Result { + let world_ref = self.get_world_res(&world)?; + let internal_pos = BlockPos::new(pos.x, pos.y, pos.z); + let state = world_ref.provider.get_block_state(&internal_pos); + let block = pumpkin_data::Block::from_state_id(state.id); + Ok(to_wit_block(block)) + } + + async fn get_block_id( + &mut self, + world: Resource, + pos: WitBlockPos, + ) -> wasmtime::Result { + let world_ref = self.get_world_res(&world)?; + let internal_pos = BlockPos::new(pos.x, pos.y, pos.z); + let state = world_ref.provider.get_block_state(&internal_pos); + Ok(pumpkin_data::BlockId::from_state_id(state.id).as_u16()) + } + + async fn set_block( + &mut self, + world: Resource, + pos: WitBlockPos, + block: WitBlock, + update_flags: WitBlockFlags, + ) -> wasmtime::Result<()> { + let block_id = BlockId::new_or_air(block.id); + let default_state_id = pumpkin_data::Block::from_id(block_id) + .default_state + .id + .as_u16(); + self.set_block_state(world, pos, default_state_id, update_flags) + .await + } + + async fn set_block_by_id( + &mut self, + world: Resource, + pos: WitBlockPos, + block_id: u16, + update_flags: WitBlockFlags, + ) -> wasmtime::Result<()> { + let Some(id) = BlockId::new(block_id) else { + return Err(wasmtime::Error::msg("Invalid BlockId")); + }; + let default_state_id = pumpkin_data::Block::from_id(id).default_state.id.as_u16(); + self.set_block_state(world, pos, default_state_id, update_flags) + .await + } + + async fn set_block_by_name( + &mut self, + world: Resource, + pos: WitBlockPos, + name: String, + update_flags: WitBlockFlags, + ) -> wasmtime::Result { + if let Some(block) = pumpkin_data::Block::from_name(&name) { + let default_state_id = block.default_state.id.as_u16(); + self.set_block_state(world, pos, default_state_id, update_flags) + .await?; + Ok(true) + } else { + Ok(false) + } } async fn set_block_state( @@ -866,6 +1142,77 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { })) } + async fn ray_trace_block( + &mut self, + world: Resource, + start: WitPosition, + end: WitPosition, + include_fluids: bool, + ) -> 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.ray_trace_block(start_pos, end_pos, include_fluids); + Ok(res.map(|(pos, face, hit_pos)| WitRayTraceBlockResult { + pos: WitBlockPos { + x: pos.0.x, + y: pos.0.y, + z: pos.0.z, + }, + face: to_wasm_block_direction(face), + hit_pos: super::events::to_wasm_position(hit_pos), + })) + } + + async fn ray_trace_entity( + &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); + if let Some((entity, hit_pos, distance)) = + world_provider.ray_trace_entity(start_pos, end_pos) + { + let entity_res = self + .add_entity(entity) + .map_err(|_| wasmtime::Error::msg("failed to add entity resource"))?; + Ok(Some(WitRayTraceEntityResult { + entity: entity_res, + hit_pos: super::events::to_wasm_position(hit_pos), + distance, + })) + } else { + Ok(None) + } + } + + async fn ray_trace_entities( + &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 hits = world_provider.ray_trace_entities(start_pos, end_pos); + let mut results = Vec::with_capacity(hits.len()); + for (entity, hit_pos, distance) in hits { + let entity_res = self + .add_entity(entity) + .map_err(|_| wasmtime::Error::msg("failed to add entity resource"))?; + results.push(WitRayTraceEntityResult { + entity: entity_res, + hit_pos: super::events::to_wasm_position(hit_pos), + distance, + }); + } + Ok(results) + } + async fn get_block_entity( &mut self, world: Resource, @@ -1097,49 +1444,52 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState { .unwrap_or(BlockStateId::AIR); let state = id.to_state(); let world_pos = BlockPos::new(chunk_data.x * 16 + pos.x, pos.y, chunk_data.z * 16 + pos.z); + Ok(to_wit_block_state(state, Some(&world_pos))) + } - Ok(WitBlockState { - id: id.as_u16(), - luminance: state.luminance, - opacity: state.opacity, - hardness: state.hardness, - is_air: state.is_air(), - is_liquid: state.is_liquid(), - is_solid: state.is_solid(), - is_full_cube: state.is_full_cube(), - has_random_ticks: state.has_random_ticks(), - piston_behavior: match state.piston_behavior { - PistonBehavior::Normal => WitPistonBehavior::Normal, - PistonBehavior::Destroy => WitPistonBehavior::Destroy, - PistonBehavior::Block => WitPistonBehavior::Block, - PistonBehavior::Ignore => WitPistonBehavior::Ignore, - PistonBehavior::PushOnly => WitPistonBehavior::PushOnly, - }, - burnable: state.burnable(), - tool_required: state.tool_required(), - sided_transparency: state.sided_transparency(), - replaceable: state.replaceable(), - is_solid_block: state.is_solid_block(), - block_entity_type: state.block_entity_type, - instrument: to_wit_noteblock_instrument(state.instrument), - collision_shapes: state - .get_block_collision_shapes_at(&world_pos) - .map(to_wit_bounding_box) - .collect(), - outline_shapes: state - .get_block_outline_shapes_at(&world_pos) - .map(to_wit_bounding_box) - .collect(), - down_side_solid: state.is_side_solid(InternalBlockDirection::Down), - up_side_solid: state.is_side_solid(InternalBlockDirection::Up), - north_side_solid: state.is_side_solid(InternalBlockDirection::North), - south_side_solid: state.is_side_solid(InternalBlockDirection::South), - west_side_solid: state.is_side_solid(InternalBlockDirection::West), - east_side_solid: state.is_side_solid(InternalBlockDirection::East), - down_center_solid: state.is_center_solid(InternalBlockDirection::Down), - up_center_solid: state.is_center_solid(InternalBlockDirection::Up), - map_color: pumpkin_data::Block::from_state_id(state.id).map_color, - }) + async fn get_block( + &mut self, + chunk: Resource, + pos: WitBlockPos, + ) -> wasmtime::Result { + let chunk_res = self.get_chunk_res(&chunk)?; + let (_, chunk_data) = &chunk_res.provider; + let Some(chunk_data) = chunk_data.upgrade() else { + return Err(wasmtime::Error::msg("Chunk unloaded")); + }; + let id = chunk_data + .section + .get_block_absolute_y(pos.x as usize, pos.y, pos.z as usize) + .unwrap_or(BlockStateId::AIR); + let block = pumpkin_data::Block::from_state_id(id); + Ok(to_wit_block(block)) + } + + async fn set_block( + &mut self, + chunk: Resource, + pos: WitBlockPos, + block: WitBlock, + ) -> wasmtime::Result<()> { + let block_id = BlockId::new_or_air(block.id); + let default_state_id = pumpkin_data::Block::from_id(block_id) + .default_state + .id + .as_u16(); + self.set_block_state(chunk, pos, default_state_id).await + } + + async fn set_block_by_id( + &mut self, + chunk: Resource, + pos: WitBlockPos, + block_id: u16, + ) -> wasmtime::Result<()> { + let Some(id) = BlockId::new(block_id) else { + return Err(wasmtime::Error::msg("Invalid BlockId")); + }; + let default_state_id = pumpkin_data::Block::from_id(id).default_state.id.as_u16(); + self.set_block_state(chunk, pos, default_state_id).await } async fn set_block_state( @@ -1355,6 +1705,17 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState { Ok(border_res.provider.worldborder.lock().await.center_z) } + async fn get_center( + &mut self, + border: Resource, + ) -> wasmtime::Result { + let border_res = self.get_world_border_res(&border)?; + let guard = border_res.provider.worldborder.lock().await; + Ok(super::events::to_wasm_position( + pumpkin_util::math::vector3::Vector3::new(guard.center_x, 0.0, guard.center_z), + )) + } + async fn set_center( &mut self, border: Resource, @@ -1372,6 +1733,10 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState { Ok(border_res.provider.worldborder.lock().await.new_diameter) } + async fn get_size(&mut self, border: Resource) -> wasmtime::Result { + self.get_diameter(border).await + } + async fn set_diameter( &mut self, border: Resource, @@ -1388,6 +1753,41 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState { Ok(()) } + async fn set_size( + &mut self, + border: Resource, + size: f64, + ) -> wasmtime::Result<()> { + self.set_diameter(border, size, None).await + } + + async fn set_size_transition( + &mut self, + border: Resource, + new_size: f64, + time_seconds: u64, + ) -> wasmtime::Result<()> { + let speed_millis = time_seconds.saturating_mul(1000); + self.set_diameter(border, new_size, Some(speed_millis)) + .await + } + + async fn get_target_diameter( + &mut self, + border: Resource, + ) -> wasmtime::Result { + let border_res = self.get_world_border_res(&border)?; + Ok(border_res.provider.worldborder.lock().await.new_diameter) + } + + async fn get_target_speed( + &mut self, + border: Resource, + ) -> wasmtime::Result { + let border_res = self.get_world_border_res(&border)?; + Ok(border_res.provider.worldborder.lock().await.speed) + } + async fn get_warning_distance( &mut self, border: Resource, @@ -1434,6 +1834,76 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState { Ok(()) } + async fn get_warning_time( + &mut self, + border: Resource, + ) -> wasmtime::Result { + self.get_warning_delay(border).await + } + + async fn set_warning_time( + &mut self, + border: Resource, + time: i32, + ) -> wasmtime::Result<()> { + self.set_warning_delay(border, time).await + } + + async fn get_damage_buffer( + &mut self, + border: Resource, + ) -> wasmtime::Result { + let border_res = self.get_world_border_res(&border)?; + Ok(f64::from( + border_res.provider.worldborder.lock().await.buffer, + )) + } + + async fn set_damage_buffer( + &mut self, + border: Resource, + buffer: f64, + ) -> wasmtime::Result<()> { + let border_res = self.get_world_border_res(&border)?; + border_res + .provider + .worldborder + .lock() + .await + .set_damage_buffer(buffer as f32); + Ok(()) + } + + async fn get_damage_amount( + &mut self, + border: Resource, + ) -> wasmtime::Result { + let border_res = self.get_world_border_res(&border)?; + Ok(f64::from( + border_res + .provider + .worldborder + .lock() + .await + .damage_per_block, + )) + } + + async fn set_damage_amount( + &mut self, + border: Resource, + damage: f64, + ) -> wasmtime::Result<()> { + let border_res = self.get_world_border_res(&border)?; + border_res + .provider + .worldborder + .lock() + .await + .set_damage_per_block(damage as f32); + Ok(()) + } + async fn contains( &mut self, border: Resource, @@ -1444,6 +1914,27 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState { Ok(border_res.provider.worldborder.lock().await.contains(x, z)) } + async fn contains_pos( + &mut self, + border: Resource, + pos: WitPosition, + ) -> wasmtime::Result { + let border_res = self.get_world_border_res(&border)?; + Ok(border_res + .provider + .worldborder + .lock() + .await + .contains(pos.0, pos.2)) + } + + async fn reset(&mut self, border: Resource) -> wasmtime::Result<()> { + let border_res = self.get_world_border_res(&border)?; + let world = border_res.provider.clone(); + world.worldborder.lock().await.reset(&world); + Ok(()) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { self.resource_table .delete::(Resource::new_own(rep.rep())) diff --git a/crates/pumpkin/src/world/border.rs b/crates/pumpkin/src/world/border.rs index 373b1c63d..77f2015b0 100644 --- a/crates/pumpkin/src/world/border.rs +++ b/crates/pumpkin/src/world/border.rs @@ -100,6 +100,38 @@ impl Worldborder { world.broadcast_packet_all(&CSetBorderWarningDistance::new(self.warning_blocks.into())); } + pub const fn set_damage_buffer(&mut self, buffer: f32) { + self.buffer = buffer; + } + + pub const fn set_damage_per_block(&mut self, damage: f32) { + self.damage_per_block = damage; + } + + pub fn reset(&mut self, world: &World) { + self.center_x = 0.0; + self.center_z = 0.0; + self.old_diameter = 29_999_984.0; + self.new_diameter = 29_999_984.0; + self.speed = 0; + self.portal_teleport_boundary = 29_999_984; + self.warning_blocks = 5; + self.warning_time = 15; + self.damage_per_block = 0.2; + self.buffer = 5.0; + + world.broadcast_packet_all(&CInitializeWorldBorder::new( + self.center_x, + self.center_z, + self.old_diameter, + self.new_diameter, + self.speed.into(), + self.portal_teleport_boundary.into(), + self.warning_blocks.into(), + self.warning_time.into(), + )); + } + #[must_use] pub fn contains(&self, x: f64, z: f64) -> bool { let half = self.new_diameter / 2.0; diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index 20ded47f9..5b3f979e4 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -460,6 +460,17 @@ impl World { .unwrap_or("world") } + /// Returns the configured shared world spawn block position and rotation. + #[must_use] + pub fn get_spawn_location(&self) -> (BlockPos, f32, f32) { + let level_info = self.level_info.load(); + ( + BlockPos::new(level_info.spawn_x, level_info.spawn_y, level_info.spawn_z), + level_info.spawn_yaw, + level_info.spawn_pitch, + ) + } + pub async fn shutdown(&self) { for entity in self.entities.load().iter() { self.save_entity(entity).await; @@ -5469,6 +5480,12 @@ impl World { &CWorldEvent::new(world_event as i32, position, data, false), ); } + + pub fn set_block_destroy_stage(&self, entity_id: i32, location: BlockPos, stage: i8) { + let chunk_pos = location.chunk_position(); + let packet = CSetBlockDestroyStage::new(entity_id.into(), location, stage); + self.broadcast_to_chunk(chunk_pos, &packet); + } #[must_use] pub fn is_valid(dest: BlockPos) -> bool { Self::is_valid_horizontally(dest) && Self::is_valid_vertically(dest.0.y) @@ -6017,12 +6034,13 @@ impl World { }); } - fn intersects_aabb_with_direction( + #[must_use] + pub fn intersects_aabb_with_hit( from: Vector3, to: Vector3, min: Vector3, max: Vector3, - ) -> Option { + ) -> Option<(f64, BlockDirection, Vector3)> { let dir = to.sub(&from); let mut tmin: f64 = 0.0; let mut tmax: f64 = 1.0; @@ -6031,7 +6049,7 @@ impl World { let mut hit_is_min = false; macro_rules! check_axis { - ($axis:ident, $dir_axis:ident, $min_axis:ident, $max_axis:ident, $direction_min:expr, $direction_max:expr) => {{ + ($axis:ident, $dir_axis:ident, $min_axis:ident, $max_axis:ident) => {{ if dir.$dir_axis.abs() < 1e-8 { if from.$dir_axis < min.$min_axis || from.$dir_axis > max.$max_axis { return None; @@ -6041,7 +6059,6 @@ impl World { let t_near = (min.$min_axis - from.$dir_axis) * inv_d; let t_far = (max.$max_axis - from.$dir_axis) * inv_d; - // Determine entry and exit points based on ray direction let (t_entry, t_exit, is_min_face) = if inv_d >= 0.0 { (t_near, t_far, true) } else { @@ -6061,19 +6078,70 @@ impl World { }}; } - check_axis!(x, x, x, x, BlockDirection::West, BlockDirection::East); - check_axis!(y, y, y, y, BlockDirection::Down, BlockDirection::Up); - check_axis!(z, z, z, z, BlockDirection::North, BlockDirection::South); + check_axis!(x, x, x, x); + check_axis!(y, y, y, y); + check_axis!(z, z, z, z); - match (hit_axis, hit_is_min) { - (Some("x"), true) => Some(BlockDirection::West), - (Some("x"), false) => Some(BlockDirection::East), - (Some("y"), true) => Some(BlockDirection::Down), - (Some("y"), false) => Some(BlockDirection::Up), - (Some("z"), true) => Some(BlockDirection::North), - (Some("z"), false) => Some(BlockDirection::South), - _ => None, + if tmax < 0.0 || tmin > 1.0 { + return None; } + + let direction = match (hit_axis, hit_is_min) { + (Some("x"), true) => BlockDirection::West, + (Some("x"), false) => BlockDirection::East, + (Some("y"), true) => BlockDirection::Down, + (Some("y"), false) => BlockDirection::Up, + (Some("z"), true) => BlockDirection::North, + (Some("z"), false) => BlockDirection::South, + _ => { + if dir.y < 0.0 { + BlockDirection::Up + } else if dir.y > 0.0 { + BlockDirection::Down + } else { + BlockDirection::North + } + } + }; + + let t_hit = tmin.max(0.0); + let hit_pos = from + dir * t_hit; + Some((t_hit, direction, hit_pos)) + } + + pub fn ray_outline_check_detailed( + &self, + block_pos: &BlockPos, + from: Vector3, + to: Vector3, + ) -> Option<(BlockDirection, Vector3)> { + let state = self.get_block_state(block_pos); + + if state.outline_shapes.is_empty() { + let block_min = block_pos.0.to_f64(); + let block_max = block_min.add_raw(1.0, 1.0, 1.0); + return Self::intersects_aabb_with_hit(from, to, block_min, block_max) + .map(|(_, dir, hit_pos)| (dir, hit_pos)); + } + + let bounding_boxes = state.get_block_outline_shapes_at(block_pos); + let mut closest_hit: Option<(f64, BlockDirection, Vector3)> = None; + + for shape in bounding_boxes { + let world_min = shape.min.add(&block_pos.0.to_f64()); + let world_max = shape.max.add(&block_pos.0.to_f64()); + + if let Some((t, dir, hit_pos)) = + Self::intersects_aabb_with_hit(from, to, world_min, world_max) + && closest_hit + .as_ref() + .is_none_or(|(closest_t, _, _)| t < *closest_t) + { + closest_hit = Some((t, dir, hit_pos)); + } + } + + closest_hit.map(|(_, dir, hit_pos)| (dir, hit_pos)) } fn ray_outline_check( @@ -6082,25 +6150,201 @@ impl World { from: Vector3, to: Vector3, ) -> (bool, Option) { - let state = self.get_block_state(block_pos); + if let Some((dir, _)) = self.ray_outline_check_detailed(block_pos, from, to) { + (true, Some(dir)) + } else { + let state = self.get_block_state(block_pos); + if state.outline_shapes.is_empty() { + (true, None) + } else { + (false, None) + } + } + } - if state.outline_shapes.is_empty() { - return (true, None); + #[allow(clippy::too_many_lines)] + pub fn ray_trace_block( + &self, + start_pos: Vector3, + end_pos: Vector3, + include_fluids: bool, + ) -> Option<(BlockPos, BlockDirection, Vector3)> { + if start_pos == end_pos { + return None; } - let bounding_boxes = state.get_block_outline_shapes_at(block_pos); + let adjust = -1.0e-7f64; + let to = end_pos.lerp(&start_pos, adjust); + let from = start_pos.lerp(&end_pos, adjust); - for shape in bounding_boxes { - let world_min = shape.min.add(&block_pos.0.to_f64()); - let world_max = shape.max.add(&block_pos.0.to_f64()); + let mut block = BlockPos::floored(from.x, from.y, from.z); - let direction = Self::intersects_aabb_with_direction(from, to, world_min, world_max); - if direction.is_some() { - return (true, direction); + let state = self.get_block_state(&block); + let valid_start = if include_fluids { + !state.is_air() + } else { + !state.is_air() && !state.is_liquid() + }; + if valid_start + && let Some((dir, hit_pos)) = self.ray_outline_check_detailed(&block, from, to) + { + return Some((block, dir, hit_pos)); + } + + let difference = to.sub(&from); + let step = difference.sign(); + + let delta = Vector3::new( + if step.x == 0 { + f64::MAX + } else { + (f64::from(step.x)) / difference.x + }, + if step.y == 0 { + f64::MAX + } else { + (f64::from(step.y)) / difference.y + }, + if step.z == 0 { + f64::MAX + } else { + (f64::from(step.z)) / difference.z + }, + ); + + let mut next = Vector3::new( + delta.x + * (if step.x > 0 { + 1.0 - (from.x - from.x.floor()) + } else { + from.x - from.x.floor() + }), + delta.y + * (if step.y > 0 { + 1.0 - (from.y - from.y.floor()) + } else { + from.y - from.y.floor() + }), + delta.z + * (if step.z > 0 { + 1.0 - (from.z - from.z.floor()) + } else { + from.z - from.z.floor() + }), + ); + + while next.x <= 1.0 || next.y <= 1.0 || next.z <= 1.0 { + let block_direction = match (next.x, next.y, next.z) { + (x, y, z) if x < y && x < z => { + block.0.x += step.x; + next.x += delta.x; + if step.x > 0 { + BlockDirection::West + } else { + BlockDirection::East + } + } + (_, y, z) if y < z => { + block.0.y += step.y; + next.y += delta.y; + if step.y > 0 { + BlockDirection::Down + } else { + BlockDirection::Up + } + } + _ => { + block.0.z += step.z; + next.z += delta.z; + if step.z > 0 { + BlockDirection::North + } else { + BlockDirection::South + } + } + }; + + let state = self.get_block_state(&block); + let hit = if include_fluids { + !state.is_air() + } else { + !state.is_air() && !state.is_liquid() + }; + + if hit { + if let Some((dir, hit_pos)) = self.ray_outline_check_detailed(&block, from, to) { + return Some((block, dir, hit_pos)); + } + let block_min = block.0.to_f64(); + let block_max = block_min.add_raw(1.0, 1.0, 1.0); + if let Some((_, dir, hit_pos)) = + Self::intersects_aabb_with_hit(from, to, block_min, block_max) + { + return Some((block, dir, hit_pos)); + } + return Some((block, block_direction, to)); } } - (false, None) + None + } + + pub fn ray_trace_entities( + &self, + start: Vector3, + end: Vector3, + ) -> Vec<(Arc, Vector3, f64)> { + if start == end { + return Vec::new(); + } + + let min_x = start.x.min(end.x) - 1.0; + let max_x = start.x.max(end.x) + 1.0; + let min_y = start.y.min(end.y) - 1.0; + let max_y = start.y.max(end.y) + 1.0; + let min_z = start.z.min(end.z) - 1.0; + let max_z = start.z.max(end.z) + 1.0; + let ray_box = BoundingBox::new( + Vector3::new(min_x, min_y, min_z), + Vector3::new(max_x, max_y, max_z), + ); + + let mut hits = Vec::new(); + + for entity in self.entities.load().iter() { + let bb = entity.get_entity().bounding_box.load(); + if bb.intersects(&ray_box) + && let Some((t, _, hit_pos)) = + Self::intersects_aabb_with_hit(start, end, bb.min, bb.max) + { + let distance = (hit_pos - start).length(); + hits.push((entity.clone(), hit_pos, distance, t)); + } + } + + for player in self.players.load().iter() { + let bb = player.get_entity().bounding_box.load(); + if bb.intersects(&ray_box) + && let Some((t, _, hit_pos)) = + Self::intersects_aabb_with_hit(start, end, bb.min, bb.max) + { + let distance = (hit_pos - start).length(); + hits.push((player.clone() as Arc, hit_pos, distance, t)); + } + } + + hits.sort_by(|a, b| a.3.partial_cmp(&b.3).unwrap_or(std::cmp::Ordering::Equal)); + hits.into_iter() + .map(|(ent, hit_pos, dist, _)| (ent, hit_pos, dist)) + .collect() + } + + pub fn ray_trace_entity( + &self, + start: Vector3, + end: Vector3, + ) -> Option<(Arc, Vector3, f64)> { + self.ray_trace_entities(start, end).into_iter().next() } pub async fn raycast(