feat: extend plugin api

This commit is contained in:
Alexander Medvedev 2026-08-24 16:24:23 +02:00
parent 97fdea50d2
commit 0d55aa5411
63 changed files with 3291 additions and 381 deletions

View File

@ -233,6 +233,9 @@ impl BlockStateId {
// depends on generated impl: // depends on generated impl:
// pub(crate) const STATE_COUNT: u16; // 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 // SAFETY: There must never be a BlockStateId where self.0 >= BlockStateId::STATE_COUNT
#[inline] #[inline]

View File

@ -252,6 +252,9 @@ impl BlockId {
// depends on generated impl: // depends on generated impl:
// pub(crate) const BLOCK_COUNT: u16; // 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 // SAFETY: There must never be a BlockId where self.0 >= BlockId::BLOCK_COUNT
#[inline] #[inline]

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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::*;

View File

@ -18,6 +18,8 @@ use crate::{Context, Result, Server, wit::pumpkin::plugin::event::EventType};
/// Block events. /// Block events.
pub mod block; pub mod block;
/// Dialog events.
pub mod dialog;
/// Enchantment events. /// Enchantment events.
pub mod enchantment; pub mod enchantment;
/// Entity events. /// Entity events.
@ -40,6 +42,7 @@ pub mod vehicle;
pub mod world; pub mod world;
pub use block::*; pub use block::*;
pub use dialog::*;
pub use enchantment::*; pub use enchantment::*;
pub use entity::*; pub use entity::*;
pub use hanging::*; pub use hanging::*;

View File

@ -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)
}
}

View File

@ -6,8 +6,6 @@ pub mod async_player_pre_login;
pub mod bedrock_form_response; pub mod bedrock_form_response;
/// Player main hand change event. /// Player main hand change event.
pub mod changed_main_hand; pub mod changed_main_hand;
/// Custom inventory click action event.
pub mod custom_click_action;
/// Egg throw event. /// Egg throw event.
pub mod egg_throw; pub mod egg_throw;
/// Experience change event. /// Experience change event.
@ -155,7 +153,6 @@ pub use async_player_chat::*;
pub use async_player_pre_login::*; pub use async_player_pre_login::*;
pub use bedrock_form_response::*; pub use bedrock_form_response::*;
pub use changed_main_hand::*; pub use changed_main_hand::*;
pub use custom_click_action::*;
pub use egg_throw::*; pub use egg_throw::*;
pub use exp_change::*; pub use exp_change::*;
pub use fish::*; pub use fish::*;

View File

@ -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<Self> {
get_all_blocks()
}
/// Returns the names of all registered blocks.
#[must_use]
pub fn all_names() -> Vec<String> {
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<Self> {
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<Self> {
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<Self> {
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<BlockState> {
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<BlockState> {
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<u16> {
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<BlockState> {
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<Self> {
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)
}
}

View File

@ -2,6 +2,7 @@
mod advancement; mod advancement;
mod attributes; mod attributes;
mod block;
mod game_rules; mod game_rules;
pub mod player; pub mod player;
mod server_list_ping; mod server_list_ping;

View File

@ -145,7 +145,10 @@ pub use wit::pumpkin::plugin::item_stack::ItemStack;
pub use wit::pumpkin::plugin::player::Player; pub use wit::pumpkin::plugin::player::Player;
pub use wit::pumpkin::plugin::scoreboard::{CollisionRule, NametagVisibility, TeamSettings}; pub use wit::pumpkin::plugin::scoreboard::{CollisionRule, NametagVisibility, TeamSettings};
pub use wit::pumpkin::plugin::server::Dimension; 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}; pub use worldgen::{ChunkBuffer, ChunkGenerator, GenerationPhase, GeneratorManager};
/// Advancement WIT API re-exports. /// Advancement WIT API re-exports.
@ -157,7 +160,11 @@ pub mod advancement {
/// Java dialog WIT API re-exports. /// Java dialog WIT API re-exports.
pub mod java_dialog { 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. /// WIT-based logging subscriber.

@ -1 +1 @@
Subproject commit 2337e6f389293a6371667d495feace8e35570318 Subproject commit 060c913c3097ae730fb2c04b7835733dcc85a1a4

View File

@ -39,6 +39,7 @@ pub enum DialogNBTSource<'a> {
Nbt(&'a pumpkin_nbt::compound::NbtCompound), Nbt(&'a pumpkin_nbt::compound::NbtCompound),
} }
#[derive(Clone, Debug)]
pub struct Dialog { pub struct Dialog {
pub r#type: String, pub r#type: String,
pub title: TextComponent, pub title: TextComponent,
@ -52,11 +53,13 @@ pub struct Dialog {
pub external_title: Option<TextComponent>, pub external_title: Option<TextComponent>,
} }
#[derive(Clone, Debug)]
pub enum DialogBody { pub enum DialogBody {
PlainMessage { contents: TextComponent }, PlainMessage { contents: TextComponent },
Item { item: i32 }, // TODO: ItemStack serialization to NBT Item { item: i32 }, // TODO: ItemStack serialization to NBT
} }
#[derive(Clone, Debug)]
pub enum DialogInput { pub enum DialogInput {
Boolean { Boolean {
label: TextComponent, label: TextComponent,
@ -82,6 +85,7 @@ pub enum DialogInput {
}, },
} }
#[derive(Clone, Debug)]
pub struct ActionButton { pub struct ActionButton {
pub text: TextComponent, pub text: TextComponent,
pub tooltip: Option<TextComponent>, pub tooltip: Option<TextComponent>,
@ -89,6 +93,7 @@ pub struct ActionButton {
pub action: DialogAction, pub action: DialogAction,
} }
#[derive(Clone, Debug)]
pub enum DialogAction { pub enum DialogAction {
OpenUrl { OpenUrl {
url: String, url: String,
@ -99,6 +104,7 @@ pub enum DialogAction {
}, },
} }
#[derive(Clone, Debug)]
pub struct DialogLink { pub struct DialogLink {
pub label: crate::Label, pub label: crate::Label,
pub url: String, pub url: String,

View File

@ -483,6 +483,7 @@ impl PositionFlag {
} }
} }
#[derive(Clone, Debug)]
pub enum Label { pub enum Label {
BuiltIn(LinkType), BuiltIn(LinkType),
TextComponent(Box<TextComponent>), TextComponent(Box<TextComponent>),
@ -523,7 +524,7 @@ impl<'a> Link<'a> {
} }
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy, Debug)]
#[repr(i32)] #[repr(i32)]
pub enum LinkType { pub enum LinkType {
BugReport = 0, BugReport = 0,

View File

@ -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<S: Into<Cow<'static, str>>, P: Into<Cow<'static, str>>>(
selector: S,
separator: Option<P>,
) -> 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<K: Into<Cow<'static, str>>>(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. /// Appends a child component to this component.
/// ///
/// # Arguments /// # Arguments

View File

@ -160,6 +160,66 @@ async fn get_hinge(
#[pumpkin_block_from_tag("minecraft:doors")] #[pumpkin_block_from_tag("minecraft:doors")]
pub struct DoorBlock; 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<World>, 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 { impl BlockBehaviour for DoorBlock {
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
Box::pin(async move { Box::pin(async move {

View File

@ -14,4 +14,12 @@ pub trait Control: Send + Sync {
pub trait MoveControlTrait: Control { pub trait MoveControlTrait: Control {
fn tick(&mut self, mob: &dyn Mob); 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
}
} }

View File

@ -101,6 +101,27 @@ impl MoveControlTrait for MoveControl {
// Navigator owns movement input while this controller waits. // 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 { impl MoveControl {

View File

@ -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<dyn Fn(Difficulty) -> 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);
}
}

View File

@ -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());
}
}

View File

@ -23,32 +23,40 @@ impl GoalSelector {
} }
pub async fn remove_goal<G: Goal + 'static>(&mut self, mob: &dyn Mob) { pub async fn remove_goal<G: Goal + 'static>(&mut self, mob: &dyn Mob) {
let mut goals_to_remove = Vec::with_capacity(2); let mut stopped = self.remove_goal_sync::<G>();
for (i, prioritized_goal) in &mut self.goals.iter_mut().enumerate() { for goal in &mut stopped {
if TypeId::of::<G>() == prioritized_goal.type_id { goal.stop(mob).await;
if prioritized_goal.running {
prioritized_goal.stop(mob).await;
}
goals_to_remove.push(i);
} }
} }
for goal_idx in goals_to_remove { pub fn remove_goal_sync<G: Goal + 'static>(&mut self) -> Vec<PrioritizedGoal> {
self.goals.swap_remove(goal_idx); self.remove_goal_by_type_id(TypeId::of::<G>())
}
// This is very fast because arrays are on the stack and the compiler knows the size pub fn remove_goal_by_type_id(&mut self, type_id: TypeId) -> Vec<PrioritizedGoal> {
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 { for slot in &mut self.goals_by_control {
if *slot == usize::MAX { if *slot == usize::MAX {
continue; continue;
} }
// Update the idx if *slot == i {
if *slot == goal_idx {
*slot = usize::MAX; *slot = usize::MAX;
} else if *slot > goal_idx { } else if *slot == self.goals.len() {
*slot -= 1; *slot = i;
} }
} }
if goal.running {
stopped.push(goal);
} }
} else {
i += 1;
}
}
stopped
} }
pub fn clear(&mut self) -> Vec<PrioritizedGoal> { pub fn clear(&mut self) -> Vec<PrioritizedGoal> {

View File

@ -7,10 +7,12 @@ pub mod avoid_entity;
pub mod beg; pub mod beg;
pub mod blaze_attack; pub mod blaze_attack;
pub mod bow_attack; pub mod bow_attack;
pub mod break_door;
pub mod breed; pub mod breed;
pub mod chase_player; pub mod chase_player;
pub mod creeper_ignite; pub mod creeper_ignite;
pub mod destroy_egg; pub mod destroy_egg;
pub mod door_interact;
pub mod eat_grass; pub mod eat_grass;
pub mod escape_danger; pub mod escape_danger;
pub mod follow_owner; pub mod follow_owner;
@ -20,6 +22,8 @@ pub mod look_around;
pub mod look_at_entity; pub mod look_at_entity;
pub mod melee_attack; pub mod melee_attack;
pub mod move_to_target_pos; pub mod move_to_target_pos;
pub mod offer_flower;
pub mod open_door;
pub mod owner_hurt_by_target; pub mod owner_hurt_by_target;
pub mod owner_hurt_target; pub mod owner_hurt_target;
pub mod pathfind_to_raid; pub mod pathfind_to_raid;
@ -33,6 +37,7 @@ pub mod teleport_towards_player;
pub mod tempt; pub mod tempt;
pub(crate) mod track_target; pub(crate) mod track_target;
pub mod trade_with_player; pub mod trade_with_player;
pub mod try_find_water;
pub mod wander_around; pub mod wander_around;
pub mod work_at_job_site; pub mod work_at_job_site;
pub mod zombie_attack; pub mod zombie_attack;

View File

@ -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<i32>,
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));
}
}

View File

@ -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);
}
}

View File

@ -24,7 +24,7 @@ impl OwnerHurtByTargetGoal {
impl Goal for OwnerHurtByTargetGoal { impl Goal for OwnerHurtByTargetGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async { Box::pin(async {
if mob.is_sitting() { if !mob.is_tamed() || mob.is_sitting() {
return false; return false;
} }
@ -105,3 +105,14 @@ impl Goal for OwnerHurtByTargetGoal {
Controls::TARGET 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));
}
}

View File

@ -24,7 +24,7 @@ impl OwnerHurtTargetGoal {
impl Goal for OwnerHurtTargetGoal { impl Goal for OwnerHurtTargetGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async { Box::pin(async {
if mob.is_sitting() { if !mob.is_tamed() || mob.is_sitting() {
return false; return false;
} }
@ -105,3 +105,14 @@ impl Goal for OwnerHurtTargetGoal {
Controls::TARGET 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));
}
}

View File

@ -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<f64>,
) -> (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<BlockPos> = 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);
}
}

View File

@ -473,4 +473,14 @@ impl Navigator {
pub fn is_idle(&self) -> bool { pub fn is_idle(&self) -> bool {
self.is_idle.load(Ordering::Relaxed) 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()
}
} }

View File

@ -650,6 +650,10 @@ pub trait Mob: EntityBase + Send + Sync {
None 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, ()> { fn mob_write_nbt<'a>(&'a self, _nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {}) 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) .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 { fn get_base_experience_reward(&self) -> u32 {
self.get_entity().entity_type.experience_reward self.get_entity().entity_type.experience_reward
} }

View File

@ -6,8 +6,8 @@ use crate::entity::{
Entity, Entity,
ai::goal::{ ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal,
wander_around::WanderAroundGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
}, },
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
@ -34,6 +34,7 @@ impl PiglinEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default())); 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 // 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(2, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));

View File

@ -6,8 +6,8 @@ use crate::entity::{
Entity, Entity,
ai::goal::{ ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal,
wander_around::WanderAroundGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
}, },
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
@ -34,6 +34,7 @@ impl PiglinBruteEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default())); 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(2, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal( goal_selector.add_goal(

View File

@ -8,8 +8,8 @@ use crate::entity::{
Entity, NbtFuture, Entity, NbtFuture,
ai::goal::{ ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal,
wander_around::WanderAroundGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
}, },
mob::{ mob::{
Mob, MobEntity, Mob, MobEntity,
@ -48,6 +48,7 @@ impl VindicatorEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default())); 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(1, Box::new(ObtainRaidLeaderBannerGoal));
goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0))); goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0)));
goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, true))); goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, true)));

View File

@ -2,9 +2,10 @@ use std::sync::Arc;
use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::mob::zombie::ZombieEntityBase;
use crate::entity::{ use crate::entity::{
Entity, Entity, NbtFuture,
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
use pumpkin_nbt::compound::NbtCompound;
pub struct DrownedEntity { pub struct DrownedEntity {
entity: Arc<ZombieEntityBase>, entity: Arc<ZombieEntityBase>,
@ -32,10 +33,29 @@ impl DrownedEntity {
mob_arc mob_arc
} }
#[must_use]
pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc<Self> {
let entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors);
let zombie = Self { entity };
Arc::new(zombie)
}
} }
impl Mob for DrownedEntity { impl Mob for DrownedEntity {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
&self.entity.mob_entity &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;
})
}
} }

View File

@ -2,9 +2,10 @@ use std::sync::Arc;
use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::mob::zombie::ZombieEntityBase;
use crate::entity::{ use crate::entity::{
Entity, Entity, NbtFuture,
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
use pumpkin_nbt::compound::NbtCompound;
pub struct HuskEntity { pub struct HuskEntity {
entity: Arc<ZombieEntityBase>, entity: Arc<ZombieEntityBase>,
@ -16,10 +17,29 @@ impl HuskEntity {
let zombie = Self { entity }; let zombie = Self { entity };
Arc::new(zombie) Arc::new(zombie)
} }
#[must_use]
pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc<Self> {
let entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors);
let zombie = Self { entity };
Arc::new(zombie)
}
} }
impl Mob for HuskEntity { impl Mob for HuskEntity {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
&self.entity.mob_entity &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;
})
}
} }

View File

@ -1,4 +1,6 @@
use super::{Mob, MobEntity}; 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::destroy_egg::DestroyEggGoal;
use crate::entity::ai::goal::look_around::RandomLookAroundGoal; use crate::entity::ai::goal::look_around::RandomLookAroundGoal;
use crate::entity::ai::goal::revenge::RevengeGoal; 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::ai::goal::zombie_attack::ZombieAttackGoal;
use crate::entity::{ use crate::entity::{
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_data::entity::EntityType;
use pumpkin_nbt::compound::NbtCompound;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
pub mod drowned; pub mod drowned;
@ -20,12 +24,20 @@ pub mod zombie_villager;
pub struct ZombieEntityBase { pub struct ZombieEntityBase {
pub mob_entity: MobEntity, pub mob_entity: MobEntity,
pub can_break_doors: AtomicBool,
} }
impl ZombieEntityBase { impl ZombieEntityBase {
pub fn new(entity: Entity) -> Arc<Self> { pub fn new(entity: Entity) -> Arc<Self> {
Self::with_can_break_doors(entity, false)
}
pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc<Self> {
let mob_entity = MobEntity::new(entity); 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_arc = Arc::new(zombie);
let mob_weak: Weak<dyn Mob> = { let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone(); let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@ -45,6 +57,9 @@ impl ZombieEntityBase {
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default())); 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(2, ZombieAttackGoal::new(1.0, false));
goal_selector.add_goal(4, DestroyEggGoal::new(1.0, 3)); goal_selector.add_goal(4, DestroyEggGoal::new(1.0, 3));
goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(1.0)));
@ -75,10 +90,56 @@ impl ZombieEntityBase {
mob_arc 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::<BreakDoorGoal>()
}
};
for goal in &mut stopped {
goal.stop(mob).await;
}
}
}
} }
impl Mob for ZombieEntityBase { impl Mob for ZombieEntityBase {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity &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;
}
})
}
} }

View File

@ -1,6 +1,7 @@
use crate::entity::Entity;
use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::mob::zombie::ZombieEntityBase;
use crate::entity::mob::{Mob, MobEntity}; use crate::entity::mob::{Mob, MobEntity};
use crate::entity::{Entity, NbtFuture};
use pumpkin_nbt::compound::NbtCompound;
use std::sync::Arc; use std::sync::Arc;
pub struct ZombieEntity { pub struct ZombieEntity {
@ -13,10 +14,29 @@ impl ZombieEntity {
let zombie = Self { entity }; let zombie = Self { entity };
Arc::new(zombie) Arc::new(zombie)
} }
#[must_use]
pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc<Self> {
let entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors);
let zombie = Self { entity };
Arc::new(zombie)
}
} }
impl Mob for ZombieEntity { impl Mob for ZombieEntity {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
&self.entity.mob_entity &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;
})
}
} }

View File

@ -1,6 +1,7 @@
use crate::entity::Entity;
use crate::entity::mob::zombie::ZombieEntityBase; use crate::entity::mob::zombie::ZombieEntityBase;
use crate::entity::mob::{Mob, MobEntity}; use crate::entity::mob::{Mob, MobEntity};
use crate::entity::{Entity, NbtFuture};
use pumpkin_nbt::compound::NbtCompound;
use std::sync::Arc; use std::sync::Arc;
pub struct ZombieVillagerEntity { pub struct ZombieVillagerEntity {
@ -13,10 +14,29 @@ impl ZombieVillagerEntity {
let zombie = Self { mob_entity }; let zombie = Self { mob_entity };
Arc::new(zombie) Arc::new(zombie)
} }
#[must_use]
pub fn with_can_break_doors(entity: Entity, can_break_doors: bool) -> Arc<Self> {
let mob_entity = ZombieEntityBase::with_can_break_doors(entity, can_break_doors);
let zombie = Self { mob_entity };
Arc::new(zombie)
}
} }
impl Mob for ZombieVillagerEntity { impl Mob for ZombieVillagerEntity {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity.mob_entity &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;
})
}
} }

View File

@ -6,7 +6,7 @@ use crate::entity::{
Entity, Entity,
ai::goal::{ ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal, try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal,
}, },
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
@ -35,6 +35,7 @@ impl AxolotlEntity {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .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(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal( goal_selector.add_goal(

View File

@ -6,7 +6,7 @@ use crate::entity::{
Entity, Entity,
ai::goal::{ ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal, try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal,
}, },
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
@ -35,6 +35,7 @@ impl DolphinEntity {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .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(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal( goal_selector.add_goal(

View File

@ -3,7 +3,7 @@ use std::sync::{
atomic::{AtomicBool, AtomicI32, Ordering}, atomic::{AtomicBool, AtomicI32, Ordering},
}; };
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::{EntityStatus, EntityType};
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::sound::{Sound, SoundCategory};
@ -15,8 +15,8 @@ use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NbtFuture, Entity, EntityBase, EntityBaseFuture, NbtFuture,
ai::goal::{ ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal,
wander_around::WanderAroundGoal, offer_flower::OfferFlowerGoal, revenge::RevengeGoal, wander_around::WanderAroundGoal,
}, },
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
player::Player, player::Player,
@ -60,6 +60,7 @@ impl IronGolemEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(1, Box::new(MeleeAttackGoal::new(1.0, true))); 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(6, Box::new(WanderAroundGoal::new(0.6)));
goal_selector.add_goal( goal_selector.add_goal(
7, 7,
@ -98,9 +99,29 @@ impl IronGolemEntity {
None, 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 { 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, ()> { fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
nbt.put_bool("PlayerCreated", self.is_player_created()); nbt.put_bool("PlayerCreated", self.is_player_created());

View File

@ -6,7 +6,7 @@ use crate::entity::{
Entity, Entity,
ai::goal::{ ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal, try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal,
}, },
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
@ -32,6 +32,7 @@ impl TadpoleEntity {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .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(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal( goal_selector.add_goal(

View File

@ -6,7 +6,8 @@ use crate::entity::{
Entity, Entity,
ai::goal::{ ai::goal::{
breed::BreedGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, 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}, mob::{Mob, MobEntity},
}; };
@ -34,6 +35,7 @@ impl TurtleEntity {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .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(1, Box::new(SwimGoal::default()));
goal_selector.add_goal(2, BreedGoal::new(1.0)); goal_selector.add_goal(2, BreedGoal::new(1.0));
goal_selector.add_goal(3, Box::new(TemptGoal::new(1.1, TEMPT_ITEMS))); goal_selector.add_goal(3, Box::new(TemptGoal::new(1.1, TEMPT_ITEMS)));

View File

@ -39,7 +39,7 @@ use crate::entity::{
ai::{ ai::{
goal::{ goal::{
avoid_entity::AvoidEntityGoal, look_around::RandomLookAroundGoal, 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, trade_with_player::TradeWithPlayerGoal, wander_around::WanderAroundGoal,
work_at_job_site::WorkAtJobSiteGoal, work_at_job_site::WorkAtJobSiteGoal,
}, },
@ -371,6 +371,7 @@ impl VillagerEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(0, Box::new(OpenDoorGoal::new(true)));
// Villagers avoid threats // Villagers avoid threats
goal_selector.add_goal( goal_selector.add_goal(
1, 1,

View File

@ -175,6 +175,54 @@ impl Mob for WolfEntity {
Some(self) 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, ()> { fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async { Box::pin(async {
let variant_str = match self.variant.load(Ordering::Relaxed) { let variant_str = match self.variant.load(Ordering::Relaxed) {

View File

@ -1042,7 +1042,7 @@ impl JavaClient {
&mut payload, &mut payload,
&version, &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(), player.clone(),
packet.action_id.to_string(), packet.action_id.to_string(),
packet.payload.map(Bytes::copy_from_slice), packet.payload.map(Bytes::copy_from_slice),

View File

@ -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<Player>,
}
impl DialogClearEvent {
#[must_use]
pub const fn new(player: Arc<Player>) -> Self {
Self {
player,
cancelled: false,
}
}
}
impl PlayerEvent for DialogClearEvent {
fn get_player(&self) -> &Arc<Player> {
&self.player
}
}

View File

@ -1,14 +1,15 @@
use bytes::Bytes; use bytes::Bytes;
use pumpkin_macros::Event; use pumpkin_macros::{Event, cancellable};
use std::sync::Arc; use std::sync::Arc;
use crate::entity::player::Player; 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. /// An event that occurs when a player clicks a custom dialog button.
#[cancellable]
#[derive(Event, Clone)] #[derive(Event, Clone)]
pub struct CustomClickActionEvent { pub struct DialogClickActionEvent {
/// The player who clicked the button. /// The player who clicked the button.
pub player: Arc<Player>, pub player: Arc<Player>,
/// The unique identifier for the action. /// The unique identifier for the action.
@ -17,18 +18,19 @@ pub struct CustomClickActionEvent {
pub payload: Option<Bytes>, pub payload: Option<Bytes>,
} }
impl CustomClickActionEvent { impl DialogClickActionEvent {
#[must_use] #[must_use]
pub const fn new(player: Arc<Player>, id: String, payload: Option<Bytes>) -> Self { pub const fn new(player: Arc<Player>, id: String, payload: Option<Bytes>) -> Self {
Self { Self {
player, player,
id, id,
payload, payload,
cancelled: false,
} }
} }
} }
impl PlayerEvent for CustomClickActionEvent { impl PlayerEvent for DialogClickActionEvent {
fn get_player(&self) -> &Arc<Player> { fn get_player(&self) -> &Arc<Player> {
&self.player &self.player
} }

View File

@ -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<Player>,
/// The dialog being shown.
pub dialog: Dialog,
}
impl DialogShowEvent {
#[must_use]
pub const fn new(player: Arc<Player>, dialog: Dialog) -> Self {
Self {
player,
dialog,
cancelled: false,
}
}
}
impl PlayerEvent for DialogShowEvent {
fn get_player(&self) -> &Arc<Player> {
&self.player
}
}

View File

@ -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::*;

View File

@ -2,6 +2,7 @@ use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
pub mod block; pub mod block;
pub mod dialog;
pub mod enchantment; pub mod enchantment;
pub mod entity; pub mod entity;
pub mod hanging; pub mod hanging;

View File

@ -2,7 +2,6 @@ pub mod async_player_chat;
pub mod async_player_pre_login; pub mod async_player_pre_login;
pub mod bedrock_form_response; pub mod bedrock_form_response;
pub mod changed_main_hand; pub mod changed_main_hand;
pub mod custom_click_action;
pub mod egg_throw; pub mod egg_throw;
pub mod exp_change; pub mod exp_change;
pub mod fish; pub mod fish;
@ -79,7 +78,6 @@ pub use async_player_chat::*;
pub use async_player_pre_login::*; pub use async_player_pre_login::*;
pub use bedrock_form_response::*; pub use bedrock_form_response::*;
pub use changed_main_hand::*; pub use changed_main_hand::*;
pub use custom_click_action::*;
pub use egg_throw::*; pub use egg_throw::*;
pub use exp_change::*; pub use exp_change::*;
pub use fish::*; pub use fish::*;

View File

@ -48,8 +48,8 @@ fn register_player_event(
) { ) {
use crate::plugin::player::{ use crate::plugin::player::{
bedrock_form_response::BedrockFormResponseEvent, bedrock_form_response::BedrockFormResponseEvent,
changed_main_hand::PlayerChangedMainHandEvent, custom_click_action::CustomClickActionEvent, changed_main_hand::PlayerChangedMainHandEvent, egg_throw::PlayerEggThrowEvent,
egg_throw::PlayerEggThrowEvent, exp_change::PlayerExpChangeEvent, fish::PlayerFishEvent, exp_change::PlayerExpChangeEvent, fish::PlayerFishEvent,
inventory_close::InventoryCloseEvent, inventory_interact::InventoryClickEvent, inventory_close::InventoryCloseEvent, inventory_interact::InventoryClickEvent,
item_held::PlayerItemHeldEvent, player_change_world::PlayerChangeWorldEvent, item_held::PlayerItemHeldEvent, player_change_world::PlayerChangeWorldEvent,
player_chat::PlayerChatEvent, player_command_send::PlayerCommandSendEvent, player_chat::PlayerChatEvent, player_command_send::PlayerCommandSendEvent,
@ -155,9 +155,6 @@ fn register_player_event(
EventType::BedrockFormResponseEvent => { EventType::BedrockFormResponseEvent => {
register_typed_event::<BedrockFormResponseEvent>(resource, handler, priority, blocking); register_typed_event::<BedrockFormResponseEvent>(resource, handler, priority, blocking);
} }
EventType::CustomClickActionEvent => {
register_typed_event::<CustomClickActionEvent>(resource, handler, priority, blocking);
}
EventType::PlayerItemConsumeEvent => { EventType::PlayerItemConsumeEvent => {
register_typed_event::< register_typed_event::<
crate::plugin::api::events::player::player_item_consume::PlayerItemConsumeEvent, 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<WasmPluginEventHandler>,
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::<DialogClickActionEvent>(resource, handler, priority, blocking);
}
EventType::DialogShowEvent => {
register_typed_event::<DialogShowEvent>(resource, handler, priority, blocking);
}
EventType::DialogClearEvent => {
register_typed_event::<DialogClearEvent>(resource, handler, priority, blocking);
}
_ => {
tracing::error!("non-dialog event should not be routed to register_dialog_event");
}
}
}
fn register_server_event( fn register_server_event(
resource: &ContextResource, resource: &ContextResource,
handler: &Arc<WasmPluginEventHandler>, handler: &Arc<WasmPluginEventHandler>,
@ -1721,6 +1745,11 @@ impl pumpkin::plugin::context::HostContext for PluginHostState {
| EventType::RaidTriggerEvent) => { | EventType::RaidTriggerEvent) => {
register_raid_event(resource, &handler, priority, blocking, event_type); 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 => { event_type => {
register_player_event(resource, &handler, priority, blocking, event_type); register_player_event(resource, &handler, priority, blocking, event_type);
} }

View File

@ -23,7 +23,9 @@ use crate::plugin::loader::wasm::wasm_host::{
uuid::Uuid, uuid::Uuid,
world::{ world::{
BlockPos as WitBlockPos, BoundingBox as WitBoundingBox, Entity, 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, World,
}, },
}, },
@ -1281,7 +1283,7 @@ impl HostEntity for PluginHostState {
&mut self, &mut self,
entity: Resource<Entity>, entity: Resource<Entity>,
max_distance: f64, max_distance: f64,
_fluid_handling: bool, fluid_handling: bool,
) -> wasmtime::Result<Option<WitRaycastResult>> { ) -> wasmtime::Result<Option<WitRaycastResult>> {
let entity = entity_from_resource(self, &entity)?; let entity = entity_from_resource(self, &entity)?;
let start = entity.get_eye_pos(); let start = entity.get_eye_pos();
@ -1289,22 +1291,9 @@ impl HostEntity for PluginHostState {
let end = start + direction * max_distance; let end = start + direction * max_distance;
let world = entity.get_entity().world.load_full(); let world = entity.get_entity().world.load_full();
let hit = world let hit = world.ray_trace_block(start, end, fluid_handling);
.raycast(
start,
end,
|pos: &pumpkin_util::math::position::BlockPos, w: &Arc<crate::world::World>| {
let pos = *pos;
let world = w.clone();
async move {
let block = world.get_block_state(&pos);
!block.is_air()
}
},
)
.await;
Ok(hit.map(|(pos, face)| WitRaycastResult { Ok(hit.map(|(pos, face, _)| WitRaycastResult {
pos: WitBlockPos { pos: WitBlockPos {
x: pos.0.x, x: pos.0.x,
y: pos.0.y, y: pos.0.y,
@ -1314,6 +1303,69 @@ impl HostEntity for PluginHostState {
})) }))
} }
async fn ray_trace_block(
&mut self,
entity: Resource<Entity>,
max_distance: f64,
include_fluids: bool,
) -> wasmtime::Result<Option<WitRayTraceBlockResult>> {
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<Entity>,
max_distance: f64,
) -> wasmtime::Result<Option<WitRayTraceEntityResult>> {
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<Entity>,
max_distance: f64,
) -> wasmtime::Result<Option<Resource<Entity>>> {
let res = self.ray_trace_entity(entity, max_distance).await?;
Ok(res.map(|r| r.entity))
}
async fn set_custom_data( async fn set_custom_data(
&mut self, &mut self,
this: Resource<Entity>, this: Resource<Entity>,

View File

@ -164,9 +164,53 @@ pub fn cleanup_event(event: &Event, state: &mut PluginHostState) {
Event::BedrockFormResponseEvent(data) => { Event::BedrockFormResponseEvent(data) => {
cleanup_player(state, &data.player); cleanup_player(state, &data.player);
} }
Event::CustomClickActionEvent(data) => { Event::DialogClickActionEvent(data) => {
cleanup_player(state, &data.player); 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::ServerCommandEvent(_) => {}
Event::ServerListPingEvent(data) => { Event::ServerListPingEvent(data) => {
cleanup_text_component(state, &data.motd); cleanup_text_component(state, &data.motd);

View File

@ -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"),
}
}
}

View File

@ -25,6 +25,7 @@ use crate::{
pub mod block; pub mod block;
pub mod cleanup; pub mod cleanup;
pub mod dialog;
pub mod enchantment; pub mod enchantment;
pub mod entity; pub mod entity;
pub mod hanging; pub mod hanging;

View File

@ -1,7 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::plugin::api::events::player::custom_click_action::CustomClickActionEvent;
use crate::plugin::{ use crate::plugin::{
loader::wasm::wasm_host::{ loader::wasm::wasm_host::{
state::PluginHostState, state::PluginHostState,
@ -17,12 +16,11 @@ use crate::plugin::{
gui::{from_wit_screen, to_wit_screen}, gui::{from_wit_screen, to_wit_screen},
pumpkin::plugin::event::{ pumpkin::plugin::event::{
AsyncPlayerChatEventData, AsyncPlayerPreLoginEventData, AsyncPlayerChatEventData, AsyncPlayerPreLoginEventData,
BedrockFormResponseEventData, CustomClickActionEventData, Event, BedrockFormResponseEventData, Event, InteractAction as WasmInteractAction,
InteractAction as WasmInteractAction, InventoryClickEventData, InventoryClickEventData, InventoryCloseEventData, PlayerAdvancementDoneEventData,
InventoryCloseEventData, PlayerAdvancementDoneEventData, PlayerAnimationEventData, PlayerAnimationEventData, PlayerArmorStandManipulateEventData,
PlayerArmorStandManipulateEventData, PlayerBedEnterEventData, PlayerBedEnterEventData, PlayerBedLeaveEventData, PlayerBucketEmptyEventData,
PlayerBedLeaveEventData, PlayerBucketEmptyEventData, PlayerBucketEntityEventData, PlayerBucketEntityEventData, PlayerBucketFillEventData, PlayerChangeWorldEventData,
PlayerBucketFillEventData, PlayerChangeWorldEventData,
PlayerChangedMainHandEventData, PlayerChangedWorldEventData, PlayerChangedMainHandEventData, PlayerChangedWorldEventData,
PlayerChannelEventData, PlayerChatEventData, PlayerCommandPreprocessEventData, PlayerChannelEventData, PlayerChatEventData, PlayerCommandPreprocessEventData,
PlayerCommandSendEventData, PlayerCustomPayloadEventData, PlayerDropItemEventData, 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 { impl ToFromWasmEvent for PlayerInteractEntityEvent {
fn to_wasm_event(&self, state: &mut PluginHostState) -> Event { fn to_wasm_event(&self, state: &mut PluginHostState) -> Event {
let player = state let player = state

View File

@ -5,9 +5,7 @@ use wasmtime::component::Resource;
use crate::plugin::api::gui::PluginScreenHandler; 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::forms::Form;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::{ use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::java_dialogs::Dialog;
Action, AfterAction, Dialog, DialogBody, DialogInput, LinkLabel, LinkType,
};
use crate::{ use crate::{
entity::{EntityBase, player::TitleMode}, entity::{EntityBase, player::TitleMode},
net::DisconnectReason, net::DisconnectReason,
@ -39,10 +37,7 @@ use crate::{
use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_protocol::Property; use pumpkin_protocol::Property;
use pumpkin_protocol::bedrock::client::modal_form_request::CModalFormRequest; use pumpkin_protocol::bedrock::client::modal_form_request::CModalFormRequest;
use pumpkin_protocol::java::client::dialog::{ use pumpkin_protocol::java::client::dialog::DialogNBT;
ActionButton as ProtocolActionButton, Dialog as ProtocolDialog, DialogAction,
DialogBody as ProtocolDialogBody, DialogInput as ProtocolDialogInput, DialogLink, DialogNBT,
};
use pumpkin_util::permission::PermissionLvl; use pumpkin_util::permission::PermissionLvl;
use pumpkin_util::translation::Locale; use pumpkin_util::translation::Locale;
use std::str::FromStr; use std::str::FromStr;
@ -1956,6 +1951,69 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(player.has_item_cooldown(&item_id).await) Ok(player.has_item_cooldown(&item_id).await)
} }
async fn ray_trace_block(
&mut self,
player: Resource<Player>,
max_distance: f64,
include_fluids: bool,
) -> wasmtime::Result<Option<pumpkin::plugin::world::RayTraceBlockResult>> {
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<Player>,
max_distance: f64,
) -> wasmtime::Result<Option<pumpkin::plugin::world::RayTraceEntityResult>> {
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<Player>,
max_distance: f64,
) -> wasmtime::Result<Option<Resource<pumpkin::plugin::world::Entity>>> {
let res = self.ray_trace_entity(player, max_distance).await?;
Ok(res.map(|r| r.entity))
}
async fn get_target_block( async fn get_target_block(
&mut self, &mut self,
player: Resource<Player>, player: Resource<Player>,
@ -1982,6 +2040,16 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
})) }))
} }
async fn get_target_block_exact(
&mut self,
player: Resource<Player>,
max_distance: f64,
include_fluids: bool,
) -> wasmtime::Result<Option<pumpkin::plugin::world::RayTraceBlockResult>> {
self.ray_trace_block(player, max_distance, include_fluids)
.await
}
async fn launch_projectile( async fn launch_projectile(
&mut self, &mut self,
_player: Resource<Player>, _player: Resource<Player>,
@ -2963,129 +3031,18 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
.provider .provider
.clone(); .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 if let Some(server) = player.world().server.upgrade() {
.body let mut event = crate::plugin::api::events::dialog::dialog_show::DialogShowEvent::new(
.iter() player.clone(),
.map(|b| match b { protocol_dialog.clone(),
DialogBody::PlainMessage(c) => ProtocolDialogBody::PlainMessage { );
contents: text_component_from_resource(self, c), server.plugin_manager.fire(&server, &mut event).await;
}, if event.cancelled {
DialogBody::Item(_i) => { return Ok(());
// 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 crate::net::ClientPlatform::Java(client) = player.client.as_ref() { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
match client.connection_state.load() { match client.connection_state.load() {
@ -3127,6 +3084,16 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
.provider .provider
.clone(); .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() { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
match client.connection_state.load() { match client.connection_state.load() {
pumpkin_protocol::ConnectionState::Config => { pumpkin_protocol::ConnectionState::Config => {

View File

@ -1,4 +1,5 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::str::FromStr;
use wasmtime::component::Resource; use wasmtime::component::Resource;
use crate::plugin::loader::wasm::wasm_host::{ use crate::plugin::loader::wasm::wasm_host::{
@ -16,6 +17,7 @@ use pumpkin_util::text::{
color::{self, Color}, color::{self, Color},
hover::HoverEvent, hover::HoverEvent,
}; };
use pumpkin_util::translation::Locale;
// --- Trapping Helpers --- // --- Trapping Helpers ---
impl PluginHostState { impl PluginHostState {
@ -92,11 +94,101 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState {
for r in with { for r in with {
components.push(self.take_text(&r)?.provider); components.push(self.take_text(&r)?.provider);
} }
#[allow(deprecated)]
let tc = InternalTextComponent::translate(key, components); let tc = InternalTextComponent::translate(key, components);
self.add_text_component(tc) self.add_text_component(tc)
.map_err(|_| wasmtime::Error::msg("Failed to add text component")) .map_err(|_| wasmtime::Error::msg("Failed to add text component"))
} }
async fn translate_cross(
&mut self,
java_key: String,
bedrock_key: String,
with: Vec<Resource<TextComponent>>,
) -> wasmtime::Result<Resource<TextComponent>> {
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<String>,
) -> wasmtime::Result<Resource<TextComponent>> {
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<Resource<TextComponent>> {
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<Resource<TextComponent>>,
) -> wasmtime::Result<Resource<TextComponent>> {
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<Resource<TextComponent>> {
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<Resource<TextComponent>> {
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<Result<Resource<TextComponent>, String>> {
match serde_json::from_str::<InternalTextComponent>(&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<TextComponent>,
) -> wasmtime::Result<String> {
let tc = &self.get_text_ref(&text_component)?.provider;
Ok(serde_json::to_string(tc).unwrap_or_default())
}
async fn add_child( async fn add_child(
&mut self, &mut self,
text_component: Resource<TextComponent>, text_component: Resource<TextComponent>,
@ -104,7 +196,6 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState {
) -> wasmtime::Result<()> { ) -> wasmtime::Result<()> {
let child_tc = self.take_text(&child)?.provider; let child_tc = self.take_text(&child)?.provider;
let parent = self.get_text_mut(&text_component)?; 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); parent.provider = parent.provider.clone().add_child(child_tc);
Ok(()) Ok(())
} }
@ -141,6 +232,17 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState {
.into_vec()) .into_vec())
} }
async fn to_pretty_console(
&mut self,
text_component: Resource<TextComponent>,
) -> wasmtime::Result<String> {
Ok(self
.get_text_ref(&text_component)?
.provider
.clone()
.to_pretty_console())
}
async fn color_named( async fn color_named(
&mut self, &mut self,
res: Resource<TextComponent>, res: Resource<TextComponent>,
@ -161,6 +263,37 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState {
Ok(()) Ok(())
} }
async fn gradient_named(
&mut self,
res: Resource<TextComponent>,
colors: Vec<NamedColor>,
) -> 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<TextComponent>,
colors: Vec<RgbColor>,
) -> 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<TextComponent>) -> wasmtime::Result<()> {
let parent = self.get_text_mut(&res)?;
parent.provider = parent.provider.clone().rainbow();
Ok(())
}
async fn bold(&mut self, res: Resource<TextComponent>, value: bool) -> wasmtime::Result<()> { async fn bold(&mut self, res: Resource<TextComponent>, value: bool) -> wasmtime::Result<()> {
self.get_text_mut(&res)?.provider.0.style.bold = Some(value); self.get_text_mut(&res)?.provider.0.style.bold = Some(value);
Ok(()) Ok(())
@ -233,6 +366,17 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState {
Ok(()) Ok(())
} }
async fn click_open_file(
&mut self,
res: Resource<TextComponent>,
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( async fn click_run_command(
&mut self, &mut self,
res: Resource<TextComponent>, res: Resource<TextComponent>,
@ -255,6 +399,16 @@ impl pumpkin::plugin::text::HostTextComponent for PluginHostState {
Ok(()) Ok(())
} }
async fn click_change_page(
&mut self,
res: Resource<TextComponent>,
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( async fn click_copy_to_clipboard(
&mut self, &mut self,
res: Resource<TextComponent>, res: Resource<TextComponent>,

View File

@ -1,6 +1,6 @@
use pumpkin_data::block_properties::NoteblockInstrument as InternalNoteblockInstrument; use pumpkin_data::block_properties::NoteblockInstrument as InternalNoteblockInstrument;
use pumpkin_data::block_state::PistonBehavior; 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_util::math::position::BlockPos;
use pumpkin_world::chunk::ChunkHeightmapType; use pumpkin_world::chunk::ChunkHeightmapType;
use pumpkin_world::chunk::io::Dirtiable; 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, GameRule as WitGameRule, GameRuleValue as WitGameRuleValue,
}; };
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::{ use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::{
BlockDirection as WitBlockDirection, BlockEntity, BlockEntityType, BlockFlags as WitBlockFlags, Block as WitBlock, BlockDirection as WitBlockDirection, BlockEntity, BlockEntityType,
BlockPos as WitBlockPos, BlockState as WitBlockState, BlockStateInfo as WitBlockStateInfo, BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState,
BoundingBox as WitBoundingBox, Chunk as WitChunk, BlockStateInfo as WitBlockStateInfo, BoundingBox as WitBoundingBox, Chunk as WitChunk,
NoteblockInstrument as WitNoteblockInstrument, PistonBehavior as WitPistonBehavior, Flammable as WitFlammable, NoteblockInstrument as WitNoteblockInstrument,
WorldBorder as WitWorldBorder, PistonBehavior as WitPistonBehavior, RayTraceBlockResult as WitRayTraceBlockResult,
RayTraceEntityResult as WitRayTraceEntityResult, WorldBorder as WitWorldBorder,
WorldSpawnLocation as WitWorldSpawnLocation,
}; };
use crate::plugin::loader::wasm::wasm_host::{ use crate::plugin::loader::wasm::wasm_host::{
state::{ 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 --- // --- Trapping Helpers ---
impl PluginHostState { impl PluginHostState {
pub(crate) fn get_world_res(&self, res: &Resource<World>) -> wasmtime::Result<&WorldResource> { pub(crate) fn get_world_res(&self, res: &Resource<World>) -> wasmtime::Result<&WorldResource> {
@ -331,6 +420,141 @@ impl pumpkin::plugin::world::Host for PluginHostState {
})); }));
Ok(result.ok()) Ok(result.ok())
} }
async fn get_block_by_id(&mut self, id: u16) -> wasmtime::Result<Option<WitBlock>> {
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<Option<WitBlock>> {
Ok(pumpkin_data::Block::from_name(&name).map(to_wit_block))
}
async fn get_all_blocks(&mut self) -> wasmtime::Result<Vec<WitBlock>> {
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<Vec<String>> {
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<u32> {
Ok(BlockId::COUNT as u32)
}
async fn get_block_state_count(&mut self) -> wasmtime::Result<u32> {
Ok(BlockStateId::COUNT as u32)
}
async fn get_states_for_block(
&mut self,
block: WitBlock,
) -> wasmtime::Result<Vec<WitBlockState>> {
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<Vec<WitBlockState>> {
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<Vec<u16>> {
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<Vec<(String, String)>> {
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<Option<WitBlock>> {
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<WitBlock> {
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<WitBlockState> {
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<Option<WitBlockState>> {
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<Option<WitBlockState>> {
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::particles::Host for PluginHostState {}
impl pumpkin::plugin::sounds::Host for PluginHostState {} impl pumpkin::plugin::sounds::Host for PluginHostState {}
@ -344,6 +568,13 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
.to_string()) .to_string())
} }
async fn get_border(
&mut self,
world: Resource<World>,
) -> wasmtime::Result<Resource<WitWorldBorder>> {
self.get_world_border(world).await
}
async fn get_world_border( async fn get_world_border(
&mut self, &mut self,
world: Resource<World>, world: Resource<World>,
@ -352,6 +583,22 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
self.add_world_border(world_res.provider.clone()) self.add_world_border(world_res.provider.clone())
} }
async fn get_spawn_location(
&mut self,
world: Resource<World>,
) -> wasmtime::Result<WitWorldSpawnLocation> {
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( async fn get_chunk(
&mut self, &mut self,
world: Resource<World>, world: Resource<World>,
@ -396,49 +643,78 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
let world_ref = self.get_world_res(&world)?; let world_ref = self.get_world_res(&world)?;
let internal_pos = BlockPos::new(pos.x, pos.y, pos.z); let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
let state = world_ref.provider.get_block_state(&internal_pos); let state = world_ref.provider.get_block_state(&internal_pos);
Ok(to_wit_block_state(state, Some(&internal_pos)))
}
Ok(WitBlockState { async fn get_block(
id: state.id.as_u16(), &mut self,
luminance: state.luminance, world: Resource<World>,
opacity: state.opacity, pos: WitBlockPos,
hardness: state.hardness, ) -> wasmtime::Result<WitBlock> {
is_air: state.is_air(), let world_ref = self.get_world_res(&world)?;
is_liquid: state.is_liquid(), let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
is_solid: state.is_solid(), let state = world_ref.provider.get_block_state(&internal_pos);
is_full_cube: state.is_full_cube(), let block = pumpkin_data::Block::from_state_id(state.id);
has_random_ticks: state.has_random_ticks(), Ok(to_wit_block(block))
piston_behavior: match state.piston_behavior { }
PistonBehavior::Normal => WitPistonBehavior::Normal,
PistonBehavior::Destroy => WitPistonBehavior::Destroy, async fn get_block_id(
PistonBehavior::Block => WitPistonBehavior::Block, &mut self,
PistonBehavior::Ignore => WitPistonBehavior::Ignore, world: Resource<World>,
PistonBehavior::PushOnly => WitPistonBehavior::PushOnly, pos: WitBlockPos,
}, ) -> wasmtime::Result<u16> {
burnable: state.burnable(), let world_ref = self.get_world_res(&world)?;
tool_required: state.tool_required(), let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
sided_transparency: state.sided_transparency(), let state = world_ref.provider.get_block_state(&internal_pos);
replaceable: state.replaceable(), Ok(pumpkin_data::BlockId::from_state_id(state.id).as_u16())
is_solid_block: state.is_solid_block(), }
block_entity_type: state.block_entity_type,
instrument: to_wit_noteblock_instrument(state.instrument), async fn set_block(
collision_shapes: state &mut self,
.get_block_collision_shapes_at(&internal_pos) world: Resource<World>,
.map(to_wit_bounding_box) pos: WitBlockPos,
.collect(), block: WitBlock,
outline_shapes: state update_flags: WitBlockFlags,
.get_block_outline_shapes_at(&internal_pos) ) -> wasmtime::Result<()> {
.map(to_wit_bounding_box) let block_id = BlockId::new_or_air(block.id);
.collect(), let default_state_id = pumpkin_data::Block::from_id(block_id)
down_side_solid: state.is_side_solid(InternalBlockDirection::Down), .default_state
up_side_solid: state.is_side_solid(InternalBlockDirection::Up), .id
north_side_solid: state.is_side_solid(InternalBlockDirection::North), .as_u16();
south_side_solid: state.is_side_solid(InternalBlockDirection::South), self.set_block_state(world, pos, default_state_id, update_flags)
west_side_solid: state.is_side_solid(InternalBlockDirection::West), .await
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), async fn set_block_by_id(
map_color: pumpkin_data::Block::from_state_id(state.id).map_color, &mut self,
}) world: Resource<World>,
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<World>,
pos: WitBlockPos,
name: String,
update_flags: WitBlockFlags,
) -> wasmtime::Result<bool> {
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( 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<World>,
start: WitPosition,
end: WitPosition,
include_fluids: bool,
) -> wasmtime::Result<Option<WitRayTraceBlockResult>> {
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<World>,
start: WitPosition,
end: WitPosition,
) -> wasmtime::Result<Option<WitRayTraceEntityResult>> {
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<World>,
start: WitPosition,
end: WitPosition,
) -> wasmtime::Result<Vec<WitRayTraceEntityResult>> {
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( async fn get_block_entity(
&mut self, &mut self,
world: Resource<World>, world: Resource<World>,
@ -1097,49 +1444,52 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
.unwrap_or(BlockStateId::AIR); .unwrap_or(BlockStateId::AIR);
let state = id.to_state(); let state = id.to_state();
let world_pos = BlockPos::new(chunk_data.x * 16 + pos.x, pos.y, chunk_data.z * 16 + pos.z); 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 { async fn get_block(
id: id.as_u16(), &mut self,
luminance: state.luminance, chunk: Resource<WitChunk>,
opacity: state.opacity, pos: WitBlockPos,
hardness: state.hardness, ) -> wasmtime::Result<WitBlock> {
is_air: state.is_air(), let chunk_res = self.get_chunk_res(&chunk)?;
is_liquid: state.is_liquid(), let (_, chunk_data) = &chunk_res.provider;
is_solid: state.is_solid(), let Some(chunk_data) = chunk_data.upgrade() else {
is_full_cube: state.is_full_cube(), return Err(wasmtime::Error::msg("Chunk unloaded"));
has_random_ticks: state.has_random_ticks(), };
piston_behavior: match state.piston_behavior { let id = chunk_data
PistonBehavior::Normal => WitPistonBehavior::Normal, .section
PistonBehavior::Destroy => WitPistonBehavior::Destroy, .get_block_absolute_y(pos.x as usize, pos.y, pos.z as usize)
PistonBehavior::Block => WitPistonBehavior::Block, .unwrap_or(BlockStateId::AIR);
PistonBehavior::Ignore => WitPistonBehavior::Ignore, let block = pumpkin_data::Block::from_state_id(id);
PistonBehavior::PushOnly => WitPistonBehavior::PushOnly, Ok(to_wit_block(block))
}, }
burnable: state.burnable(),
tool_required: state.tool_required(), async fn set_block(
sided_transparency: state.sided_transparency(), &mut self,
replaceable: state.replaceable(), chunk: Resource<WitChunk>,
is_solid_block: state.is_solid_block(), pos: WitBlockPos,
block_entity_type: state.block_entity_type, block: WitBlock,
instrument: to_wit_noteblock_instrument(state.instrument), ) -> wasmtime::Result<()> {
collision_shapes: state let block_id = BlockId::new_or_air(block.id);
.get_block_collision_shapes_at(&world_pos) let default_state_id = pumpkin_data::Block::from_id(block_id)
.map(to_wit_bounding_box) .default_state
.collect(), .id
outline_shapes: state .as_u16();
.get_block_outline_shapes_at(&world_pos) self.set_block_state(chunk, pos, default_state_id).await
.map(to_wit_bounding_box) }
.collect(),
down_side_solid: state.is_side_solid(InternalBlockDirection::Down), async fn set_block_by_id(
up_side_solid: state.is_side_solid(InternalBlockDirection::Up), &mut self,
north_side_solid: state.is_side_solid(InternalBlockDirection::North), chunk: Resource<WitChunk>,
south_side_solid: state.is_side_solid(InternalBlockDirection::South), pos: WitBlockPos,
west_side_solid: state.is_side_solid(InternalBlockDirection::West), block_id: u16,
east_side_solid: state.is_side_solid(InternalBlockDirection::East), ) -> wasmtime::Result<()> {
down_center_solid: state.is_center_solid(InternalBlockDirection::Down), let Some(id) = BlockId::new(block_id) else {
up_center_solid: state.is_center_solid(InternalBlockDirection::Up), return Err(wasmtime::Error::msg("Invalid BlockId"));
map_color: pumpkin_data::Block::from_state_id(state.id).map_color, };
}) 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( 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) Ok(border_res.provider.worldborder.lock().await.center_z)
} }
async fn get_center(
&mut self,
border: Resource<WitWorldBorder>,
) -> wasmtime::Result<WitPosition> {
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( async fn set_center(
&mut self, &mut self,
border: Resource<WitWorldBorder>, border: Resource<WitWorldBorder>,
@ -1372,6 +1733,10 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState {
Ok(border_res.provider.worldborder.lock().await.new_diameter) Ok(border_res.provider.worldborder.lock().await.new_diameter)
} }
async fn get_size(&mut self, border: Resource<WitWorldBorder>) -> wasmtime::Result<f64> {
self.get_diameter(border).await
}
async fn set_diameter( async fn set_diameter(
&mut self, &mut self,
border: Resource<WitWorldBorder>, border: Resource<WitWorldBorder>,
@ -1388,6 +1753,41 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState {
Ok(()) Ok(())
} }
async fn set_size(
&mut self,
border: Resource<WitWorldBorder>,
size: f64,
) -> wasmtime::Result<()> {
self.set_diameter(border, size, None).await
}
async fn set_size_transition(
&mut self,
border: Resource<WitWorldBorder>,
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<WitWorldBorder>,
) -> wasmtime::Result<f64> {
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<WitWorldBorder>,
) -> wasmtime::Result<i64> {
let border_res = self.get_world_border_res(&border)?;
Ok(border_res.provider.worldborder.lock().await.speed)
}
async fn get_warning_distance( async fn get_warning_distance(
&mut self, &mut self,
border: Resource<WitWorldBorder>, border: Resource<WitWorldBorder>,
@ -1434,6 +1834,76 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState {
Ok(()) Ok(())
} }
async fn get_warning_time(
&mut self,
border: Resource<WitWorldBorder>,
) -> wasmtime::Result<i32> {
self.get_warning_delay(border).await
}
async fn set_warning_time(
&mut self,
border: Resource<WitWorldBorder>,
time: i32,
) -> wasmtime::Result<()> {
self.set_warning_delay(border, time).await
}
async fn get_damage_buffer(
&mut self,
border: Resource<WitWorldBorder>,
) -> wasmtime::Result<f64> {
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<WitWorldBorder>,
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<WitWorldBorder>,
) -> wasmtime::Result<f64> {
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<WitWorldBorder>,
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( async fn contains(
&mut self, &mut self,
border: Resource<WitWorldBorder>, border: Resource<WitWorldBorder>,
@ -1444,6 +1914,27 @@ impl pumpkin::plugin::world::HostWorldBorder for PluginHostState {
Ok(border_res.provider.worldborder.lock().await.contains(x, z)) Ok(border_res.provider.worldborder.lock().await.contains(x, z))
} }
async fn contains_pos(
&mut self,
border: Resource<WitWorldBorder>,
pos: WitPosition,
) -> wasmtime::Result<bool> {
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<WitWorldBorder>) -> 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<WitWorldBorder>) -> wasmtime::Result<()> { async fn drop(&mut self, rep: Resource<WitWorldBorder>) -> wasmtime::Result<()> {
self.resource_table self.resource_table
.delete::<WorldBorderResource>(Resource::new_own(rep.rep())) .delete::<WorldBorderResource>(Resource::new_own(rep.rep()))

View File

@ -100,6 +100,38 @@ impl Worldborder {
world.broadcast_packet_all(&CSetBorderWarningDistance::new(self.warning_blocks.into())); 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] #[must_use]
pub fn contains(&self, x: f64, z: f64) -> bool { pub fn contains(&self, x: f64, z: f64) -> bool {
let half = self.new_diameter / 2.0; let half = self.new_diameter / 2.0;

View File

@ -460,6 +460,17 @@ impl World {
.unwrap_or("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) { pub async fn shutdown(&self) {
for entity in self.entities.load().iter() { for entity in self.entities.load().iter() {
self.save_entity(entity).await; self.save_entity(entity).await;
@ -5469,6 +5480,12 @@ impl World {
&CWorldEvent::new(world_event as i32, position, data, false), &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] #[must_use]
pub fn is_valid(dest: BlockPos) -> bool { pub fn is_valid(dest: BlockPos) -> bool {
Self::is_valid_horizontally(dest) && Self::is_valid_vertically(dest.0.y) 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<f64>, from: Vector3<f64>,
to: Vector3<f64>, to: Vector3<f64>,
min: Vector3<f64>, min: Vector3<f64>,
max: Vector3<f64>, max: Vector3<f64>,
) -> Option<BlockDirection> { ) -> Option<(f64, BlockDirection, Vector3<f64>)> {
let dir = to.sub(&from); let dir = to.sub(&from);
let mut tmin: f64 = 0.0; let mut tmin: f64 = 0.0;
let mut tmax: f64 = 1.0; let mut tmax: f64 = 1.0;
@ -6031,7 +6049,7 @@ impl World {
let mut hit_is_min = false; let mut hit_is_min = false;
macro_rules! check_axis { 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 dir.$dir_axis.abs() < 1e-8 {
if from.$dir_axis < min.$min_axis || from.$dir_axis > max.$max_axis { if from.$dir_axis < min.$min_axis || from.$dir_axis > max.$max_axis {
return None; return None;
@ -6041,7 +6059,6 @@ impl World {
let t_near = (min.$min_axis - from.$dir_axis) * inv_d; let t_near = (min.$min_axis - from.$dir_axis) * inv_d;
let t_far = (max.$max_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 { let (t_entry, t_exit, is_min_face) = if inv_d >= 0.0 {
(t_near, t_far, true) (t_near, t_far, true)
} else { } else {
@ -6061,19 +6078,70 @@ impl World {
}}; }};
} }
check_axis!(x, x, x, x, BlockDirection::West, BlockDirection::East); check_axis!(x, x, x, x);
check_axis!(y, y, y, y, BlockDirection::Down, BlockDirection::Up); check_axis!(y, y, y, y);
check_axis!(z, z, z, z, BlockDirection::North, BlockDirection::South); check_axis!(z, z, z, z);
match (hit_axis, hit_is_min) { if tmax < 0.0 || tmin > 1.0 {
(Some("x"), true) => Some(BlockDirection::West), return None;
(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,
} }
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<f64>,
to: Vector3<f64>,
) -> Option<(BlockDirection, Vector3<f64>)> {
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<f64>)> = 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( fn ray_outline_check(
@ -6082,26 +6150,202 @@ impl World {
from: Vector3<f64>, from: Vector3<f64>,
to: Vector3<f64>, to: Vector3<f64>,
) -> (bool, Option<BlockDirection>) { ) -> (bool, Option<BlockDirection>) {
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); let state = self.get_block_state(block_pos);
if state.outline_shapes.is_empty() { if state.outline_shapes.is_empty() {
return (true, None); (true, None)
} } else {
let bounding_boxes = state.get_block_outline_shapes_at(block_pos);
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 direction = Self::intersects_aabb_with_direction(from, to, world_min, world_max);
if direction.is_some() {
return (true, direction);
}
}
(false, None) (false, None)
} }
}
}
#[allow(clippy::too_many_lines)]
pub fn ray_trace_block(
&self,
start_pos: Vector3<f64>,
end_pos: Vector3<f64>,
include_fluids: bool,
) -> Option<(BlockPos, BlockDirection, Vector3<f64>)> {
if start_pos == end_pos {
return None;
}
let adjust = -1.0e-7f64;
let to = end_pos.lerp(&start_pos, adjust);
let from = start_pos.lerp(&end_pos, adjust);
let mut block = BlockPos::floored(from.x, from.y, from.z);
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));
}
}
None
}
pub fn ray_trace_entities(
&self,
start: Vector3<f64>,
end: Vector3<f64>,
) -> Vec<(Arc<dyn EntityBase>, Vector3<f64>, 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<dyn EntityBase>, 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<f64>,
end: Vector3<f64>,
) -> Option<(Arc<dyn EntityBase>, Vector3<f64>, f64)> {
self.ray_trace_entities(start, end).into_iter().next()
}
pub async fn raycast( pub async fn raycast(
self: &Arc<Self>, self: &Arc<Self>,