From 800f7d70e59e0e8b8b5d1ec154fab563f1aa31f6 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Thu, 27 Aug 2026 12:36:26 +0200 Subject: [PATCH] feat: more plugin entity stuff --- Cargo.lock | 21 - Cargo.toml | 3 +- crates/pumpkin-inventory/Cargo.toml | 1 - crates/pumpkin-plugin-api/src/datapack.rs | 31 + crates/pumpkin-plugin-api/src/inventory.rs | 32 + crates/pumpkin-plugin-api/src/lib.rs | 28 +- crates/pumpkin-plugin-api/src/mobs.rs | 689 +++++++++++++ crates/pumpkin-plugin-wit | 2 +- crates/pumpkin-world/Cargo.toml | 1 - crates/pumpkin-world/benches/chunk_io.rs | 7 +- .../pumpkin-world/src/chunk/format/anvil.rs | 11 +- .../pumpkin-world/src/chunk/format/linear.rs | 1 - crates/pumpkin-world/src/chunk/format/mod.rs | 22 +- crates/pumpkin-world/src/chunk/format/pump.rs | 11 +- .../src/chunk/io/file_manager.rs | 464 ++++----- crates/pumpkin-world/src/chunk/io/mod.rs | 16 +- crates/pumpkin-world/src/chunk/mod.rs | 3 +- .../src/chunk_system/schedule.rs | 152 +-- .../src/chunk_system/worker_logic.rs | 67 +- crates/pumpkin-world/src/dimension.rs | 3 +- crates/pumpkin-world/src/level.rs | 104 +- crates/pumpkin/src/block/entities/mod.rs | 18 +- .../pumpkin/src/block/entities/shulker_box.rs | 15 +- .../pumpkin/src/command/commands/datapack.rs | 74 +- .../src/command/commands/fetchprofile.rs | 80 +- .../pumpkin/src/command/commands/fillbiome.rs | 64 +- crates/pumpkin/src/command/commands/plugin.rs | 141 +-- .../pumpkin/src/command/commands/plugins.rs | 2 +- crates/pumpkin/src/command/commands/ride.rs | 10 +- crates/pumpkin/src/command/commands/rotate.rs | 19 +- .../pumpkin/src/command/commands/saveall.rs | 38 +- .../pumpkin/src/command/commands/spectate.rs | 17 +- .../src/command/commands/spreadplayers.rs | 2 +- .../pumpkin/src/command/commands/teleport.rs | 27 +- crates/pumpkin/src/data/datapack/mod.rs | 326 +++++- crates/pumpkin/src/data/player_server.rs | 2 +- crates/pumpkin/src/entity/mob/creeper.rs | 50 + crates/pumpkin/src/entity/mob/evoker.rs | 10 +- crates/pumpkin/src/entity/mob/mod.rs | 4 - crates/pumpkin/src/entity/mob/shulker.rs | 20 +- .../pumpkin/src/entity/mob/zombie/zombie.rs | 43 + crates/pumpkin/src/entity/mod.rs | 31 +- .../pumpkin/src/entity/passive/iron_golem.rs | 3 - crates/pumpkin/src/entity/passive/sheep.rs | 4 - crates/pumpkin/src/entity/passive/wolf.rs | 18 + crates/pumpkin/src/entity/player.rs | 4 +- crates/pumpkin/src/item/items/shears.rs | 4 +- crates/pumpkin/src/main.rs | 5 + .../src/net/java/config/known_packs.rs | 87 +- crates/pumpkin/src/net/java/mod.rs | 103 +- crates/pumpkin/src/net/query.rs | 1 - crates/pumpkin/src/plugin/api/context.rs | 4 +- .../src/plugin/loader/wasm/wasm_host/state.rs | 60 ++ .../wasm/wasm_host/wit/v0_1/block_entity.rs | 18 + .../wasm/wasm_host/wit/v0_1/datapack.rs | 154 +++ .../loader/wasm/wasm_host/wit/v0_1/entity.rs | 739 +------------- .../wasm/wasm_host/wit/v0_1/events/entity.rs | 2 +- .../loader/wasm/wasm_host/wit/v0_1/gui.rs | 17 + .../wasm/wasm_host/wit/v0_1/inventory.rs | 568 +++++++++++ .../wasm/wasm_host/wit/v0_1/living_entity.rs | 502 ++++++++++ .../loader/wasm/wasm_host/wit/v0_1/mob.rs | 941 ++++++++++++++++++ .../loader/wasm/wasm_host/wit/v0_1/mod.rs | 8 + .../loader/wasm/wasm_host/wit/v0_1/player.rs | 30 +- .../loader/wasm/wasm_host/wit/v0_1/server.rs | 12 + .../loader/wasm/wasm_host/wit/v0_1/world.rs | 10 +- crates/pumpkin/src/plugin/mod.rs | 16 +- crates/pumpkin/src/server/mod.rs | 23 +- crates/pumpkin/src/world/mod.rs | 71 +- 68 files changed, 4366 insertions(+), 1700 deletions(-) create mode 100644 crates/pumpkin-plugin-api/src/datapack.rs create mode 100644 crates/pumpkin-plugin-api/src/inventory.rs create mode 100644 crates/pumpkin-plugin-api/src/mobs.rs create mode 100644 crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/datapack.rs create mode 100644 crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/inventory.rs create mode 100644 crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/living_entity.rs create mode 100644 crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs diff --git a/Cargo.lock b/Cargo.lock index 7e1ce0d38..53caf7733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,19 +1179,6 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" -[[package]] -name = "crossfire" -version = "3.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111ce8f7abfbac38b4bc4f32a3a2dda1a8034b873c90c7899f302cc9dbbc05ec" -dependencies = [ - "crossbeam-utils", - "futures-core", - "parking_lot", - "pointers", - "smallvec", -] - [[package]] name = "crunchy" version = "0.2.4" @@ -3098,12 +3085,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "pointers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcdc93847ad24990939cce6e1804361e903efcb5f99daa5abd87943a9d6d7ba" - [[package]] name = "poly1305" version = "0.8.0" @@ -3464,7 +3445,6 @@ dependencies = [ "pumpkin-world", "rand", "thiserror 2.0.20", - "tokio", "tracing", ] @@ -3581,7 +3561,6 @@ dependencies = [ "bytes", "criterion", "crossbeam", - "crossfire", "dashmap", "flate2", "futures", diff --git a/Cargo.toml b/Cargo.toml index 53c4dde7a..1ce0b495e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,7 +131,7 @@ thiserror = { version = "2.0", default-features = false } bytes = { version = "1.12", default-features = false, features = ["std"] } # Concurrency/Parallelism and Synchronization -futures = { version = "0.3", default-features = false, features = ["executor"] } +futures = { version = "0.3", default-features = false, features = ["std"] } rayon = { version = "1.12", default-features = false } crossbeam = { version = "0.8", default-features = false, features = ["std"] } @@ -153,7 +153,6 @@ console-subscriber = { version = "0.5.0", default-features = false } crc-fast = { version = "1.10.0", default-features = false, features = ["std"] } criterion = { version = "0.8", default-features = false } crossbeam-utils = { version = "0.8.22", default-features = false, features = ["std"] } -crossfire = { version = "3.1.19", default-features = false, features = ["compat"] } crypto-bigint = { version = "0.7.5", default-features = false } dashmap = { version = "6.2", default-features = false } ecdsa = { version = "0.17.0", default-features = false, features = ["std"] } diff --git a/crates/pumpkin-inventory/Cargo.toml b/crates/pumpkin-inventory/Cargo.toml index 93bd8277c..df39b40b2 100644 --- a/crates/pumpkin-inventory/Cargo.toml +++ b/crates/pumpkin-inventory/Cargo.toml @@ -24,7 +24,6 @@ pumpkin-util.workspace = true rand.workspace = true tracing.workspace = true -tokio.workspace = true thiserror.workspace = true [lints] diff --git a/crates/pumpkin-plugin-api/src/datapack.rs b/crates/pumpkin-plugin-api/src/datapack.rs new file mode 100644 index 000000000..2daae571d --- /dev/null +++ b/crates/pumpkin-plugin-api/src/datapack.rs @@ -0,0 +1,31 @@ +//! Plugin datapack management and querying utilities. +//! +//! This module provides interfaces to inspect, query, enable, disable, and reload +//! datapacks on the server. +//! +//! # Examples +//! +//! ## Listing Datapacks +//! ```rust,ignore +//! use pumpkin_plugin_api::Server; +//! +//! fn log_datapacks(server: &Server) { +//! let manager = server.get_datapack_manager(); +//! for pack in manager.list_all_packs() { +//! println!("Datapack {}: enabled = {}", pack.name, pack.is_enabled); +//! } +//! } +//! ``` +//! +//! ## Enabling and Reloading Datapacks +//! ```rust,ignore +//! use pumpkin_plugin_api::{Server, datapack::EnablePosition}; +//! +//! fn enable_custom_pack(server: &Server) -> Result<(), String> { +//! let manager = server.get_datapack_manager(); +//! manager.enable_pack("my_custom_pack", EnablePosition::Last)?; +//! Ok(()) +//! } +//! ``` + +pub use crate::wit::pumpkin::plugin::datapack::{DatapackInfo, DatapackManager, EnablePosition}; diff --git a/crates/pumpkin-plugin-api/src/inventory.rs b/crates/pumpkin-plugin-api/src/inventory.rs new file mode 100644 index 000000000..06b1eb8d5 --- /dev/null +++ b/crates/pumpkin-plugin-api/src/inventory.rs @@ -0,0 +1,32 @@ +//! Plugin inventory and container management utilities. +//! +//! This module provides a unified API for inspecting, modifying, and interacting +//! with player inventories, ender chests, custom GUIs, and container block entities. +//! +//! # Examples +//! +//! ## Inspecting and Modifying a Player's Inventory +//! ```rust,ignore +//! use pumpkin_plugin_api::{Player, ItemStack}; +//! +//! fn equip_player(player: &Player) { +//! let inv = player.get_inventory(); +//! inv.set_helmet(Some(ItemStack::new("minecraft:diamond_helmet", 1))); +//! inv.set_boots(Some(ItemStack::new("minecraft:diamond_boots", 1))); +//! +//! let storage = inv.as_inventory(); +//! storage.set_item(0, Some(ItemStack::new("minecraft:diamond_sword", 1))); +//! } +//! ``` +//! +//! ## Interacting with Custom GUIs +//! ```rust,ignore +//! use pumpkin_plugin_api::gui::Gui; +//! +//! fn setup_gui(gui: &Gui) { +//! let inv = gui.get_inventory(); +//! inv.clear(); +//! } +//! ``` + +pub use crate::wit::pumpkin::plugin::inventory::{Inventory, PlayerInventory}; diff --git a/crates/pumpkin-plugin-api/src/lib.rs b/crates/pumpkin-plugin-api/src/lib.rs index 5963563ec..947b69166 100644 --- a/crates/pumpkin-plugin-api/src/lib.rs +++ b/crates/pumpkin-plugin-api/src/lib.rs @@ -76,6 +76,8 @@ use crate::{ /// Plugin command registration and handling utilities. pub mod commands; +/// Datapack management and query utilities. +pub mod datapack; /// Display and interaction entity utilities and builders. pub mod display; /// Custom enchantment registration and builder utilities. @@ -85,6 +87,10 @@ pub mod events; mod ext; /// Bedrock UI form builders. pub mod forms; +/// Unified inventory and container management utilities. +pub mod inventory; +/// Specialized mob entity wrappers and helpers. +pub mod mobs; /// Constants for plugin permissions. /// /// Use these in your `PluginMetadata` to request access to specific host features. @@ -110,18 +116,19 @@ pub use wit::pumpkin::plugin::{ advancement as advancement_wit, bedrock_packets, block_entity, boss_bar, command as command_wit, common, context::{self, Context, MarketplaceMetadata, Server}, - damage_types as damage_types_wit, data_components, display as display_wit, - enchantments as enchantments_wit, entity, + damage_types as damage_types_wit, data_components, datapack as datapack_wit, + display as display_wit, enchantments as enchantments_wit, entity, entity_types::EntityType, event::{self as events_wit, EventType}, - gui, i18n, ipc, item_stack, java_dialogs, java_packets, particles, permission, player, - recipe as recipe_wit, scoreboard, screens as screens_wit, server, statistics as statistics_wit, - text, uuid, world, + gui, i18n, inventory as inventory_wit, ipc, item_stack, java_dialogs, java_packets, particles, + permission, player, recipe as recipe_wit, scoreboard, screens as screens_wit, server, + statistics as statistics_wit, text, uuid, world, }; // Convenience re-exports of commonly-used plugin types so plugin authors can // name them directly (e.g. build an `ItemStack` for a GUI or `/give`). pub use damage_types_wit::DamageType; +pub use datapack::{DatapackInfo, DatapackManager, EnablePosition}; pub use display::{ BillboardMode, BlockDisplayEntity, DisplayEntity, DisplayEntityExt, DisplayTransformation, EntityDisplayExt, InteractionEntity, ItemDisplayEntity, ItemDisplayEntityExt, ItemDisplayMode, @@ -134,6 +141,13 @@ pub use enchantment::{ }; pub use events::{EventHandler, FromIntoEvent}; pub use ext::player::PlayerEnderChestExt; +pub use inventory::{Inventory, PlayerInventory}; +pub use mobs::{ + Ageable, AgeableData, Cat, CatData, Creeper, CreeperData, DyeColor, Enderman, EndermanData, + EntityCastExt, Fox, FoxData, IronGolem, IronGolemData, MobCast, MobData, Sheep, SheepData, + Shulker, ShulkerData, Slime, SlimeData, Villager, VillagerData, VillagerProfession, Wolf, + WolfData, Zombie, ZombieData, +}; pub use recipe::{ CookingRecipeBuilder, Ingredient, RecipeCategory, RecipeError, RecipeManager, RegistrableRecipe, ShapedRecipeBuilder, ShapelessRecipeBuilder, @@ -146,8 +160,8 @@ pub use wit::pumpkin::plugin::player::Player; pub use wit::pumpkin::plugin::scoreboard::{CollisionRule, NametagVisibility, TeamSettings}; pub use wit::pumpkin::plugin::server::Dimension; pub use wit::pumpkin::plugin::world::{ - Block, BlockDirection, BlockState, BlockStateInfo, Entity, Flammable, RayTraceBlockResult, - RayTraceEntityResult, RaycastResult, World, WorldBorder, + Block, BlockDirection, BlockState, BlockStateInfo, Entity, Flammable, LivingEntity, Mob, + PathNodeType, RayTraceBlockResult, RayTraceEntityResult, RaycastResult, World, WorldBorder, }; pub use worldgen::{ChunkBuffer, ChunkGenerator, GenerationPhase, GeneratorManager}; diff --git a/crates/pumpkin-plugin-api/src/mobs.rs b/crates/pumpkin-plugin-api/src/mobs.rs new file mode 100644 index 000000000..76474031c --- /dev/null +++ b/crates/pumpkin-plugin-api/src/mobs.rs @@ -0,0 +1,689 @@ +//! Specialized typed wrappers for major Minecraft mob entities. +//! +//! Provides typed accessors and ergonomic helpers (`Sheep::from_entity`, `Wolf::from_mob`, etc.) +//! without needing dozens of WIT resources. + +use std::ops::Deref; + +pub use crate::wit::pumpkin::plugin::uuid::Uuid; +pub use crate::wit::pumpkin::plugin::world::{ + AgeableData, BlockDirection, CatData, CreeperData, DyeColor, EndermanData, Entity, FoxData, + IronGolemData, LivingEntity, Mob, MobData, SheepData, ShulkerData, SlimeData, VillagerData, + VillagerProfession, WolfData, ZombieData, +}; + +/// Trait implemented by all specialized mob wrappers to allow generic downcasting via `.cast::()`. +pub trait MobCast<'a>: Sized { + /// Attempts to wrap a [`Mob`] reference if the underlying entity data matches. + fn from_mob(mob: &'a Mob) -> Option; + + /// Attempts to wrap an [`Entity`] reference if it is an AI mob matching this type. + fn from_entity(entity: &'a Entity) -> Option { + let mob = entity.as_mob()?; + // Note: as_mob() returns a new Resource handle, so we extract via MobData check. + Self::from_mob_owned(mob) + } + + /// Attempts to wrap a [`LivingEntity`] reference if it is an AI mob matching this type. + fn from_living(living: &'a LivingEntity) -> Option { + let mob = living.as_mob()?; + Self::from_mob_owned(mob) + } + + #[doc(hidden)] + fn from_mob_owned(mob: Mob) -> Option; +} + +macro_rules! define_mob_wrapper { + ( + $(#[$meta:meta])* + $name:ident, $variant:ident, $data_ty:ident + ) => { + $(#[$meta])* + pub struct $name<'a> { + mob: &'a Mob, + _owned: Option, + } + + impl<'a> $name<'a> { + /// Wraps a borrowed [`Mob`] reference if this mob is of the matching type. + #[must_use] + pub fn from_mob(mob: &'a Mob) -> Option { + if matches!(mob.get_mob_data(), MobData::$variant(_)) { + Some(Self { mob, _owned: None }) + } else { + None + } + } + + /// Wraps a borrowed [`Entity`] reference if it is an AI mob of the matching type. + #[must_use] + pub fn from_entity(entity: &'a Entity) -> Option { + let mob = entity.as_mob()?; + Self::from_mob_owned(mob) + } + + /// Wraps a borrowed [`LivingEntity`] reference if it is an AI mob of the matching type. + #[must_use] + pub fn from_living(living: &'a LivingEntity) -> Option { + let mob = living.as_mob()?; + Self::from_mob_owned(mob) + } + + fn from_mob_owned(mob: Mob) -> Option { + if matches!(mob.get_mob_data(), MobData::$variant(_)) { + // Safe reference extension to the owned Mob + let mob_ref = unsafe { &*(&mob as *const Mob) }; + Some(Self { + mob: mob_ref, + _owned: Some(mob), + }) + } else { + None + } + } + + /// Retrieves the underlying data record for this mob. + #[must_use] + pub fn get_data(&self) -> Option<$data_ty> { + match self.mob.get_mob_data() { + MobData::$variant(data) => Some(data), + _ => None, + } + } + + /// Updates the underlying data record for this mob. + pub fn set_data(&self, data: $data_ty) -> bool { + self.mob.set_mob_data(MobData::$variant(data)) + } + } + + impl<'a> Deref for $name<'a> { + type Target = Mob; + + fn deref(&self) -> &Self::Target { + self.mob + } + } + + impl<'a> TryFrom<&'a Mob> for $name<'a> { + type Error = (); + + fn try_from(mob: &'a Mob) -> Result { + Self::from_mob(mob).ok_or(()) + } + } + + impl<'a> TryFrom<&'a Entity> for $name<'a> { + type Error = (); + + fn try_from(entity: &'a Entity) -> Result { + Self::from_entity(entity).ok_or(()) + } + } + + impl<'a> TryFrom<&'a LivingEntity> for $name<'a> { + type Error = (); + + fn try_from(living: &'a LivingEntity) -> Result { + Self::from_living(living).ok_or(()) + } + } + + impl<'a> MobCast<'a> for $name<'a> { + fn from_mob(mob: &'a Mob) -> Option { + Self::from_mob(mob) + } + + fn from_living(living: &'a LivingEntity) -> Option { + Self::from_living(living) + } + + fn from_mob_owned(mob: Mob) -> Option { + Self::from_mob_owned(mob) + } + } + }; +} + +define_mob_wrapper!( + /// Specialized wrapper for Sheep entities. + Sheep, Sheep, SheepData +); + +impl<'a> Sheep<'a> { + /// Gets the fleece dye color of this sheep. + #[must_use] + pub fn get_color(&self) -> DyeColor { + self.get_data().map_or(DyeColor::White, |d| d.color) + } + + /// Sets the fleece dye color of this sheep. + pub fn set_color(&self, color: DyeColor) { + if let Some(mut data) = self.get_data() { + data.color = color; + self.set_data(data); + } + } + + /// Returns whether this sheep has been sheared. + #[must_use] + pub fn is_sheared(&self) -> bool { + self.get_data().is_some_and(|d| d.is_sheared) + } + + /// Sets whether this sheep is sheared. + pub fn set_sheared(&self, sheared: bool) { + if let Some(mut data) = self.get_data() { + data.is_sheared = sheared; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Wolf entities. + Wolf, Wolf, WolfData +); + +impl<'a> Wolf<'a> { + /// Returns whether this wolf is tamed. + #[must_use] + pub fn is_tamed(&self) -> bool { + self.get_data().is_some_and(|d| d.is_tamed) + } + + /// Sets whether this wolf is tamed. + pub fn set_tamed(&self, tamed: bool) { + if let Some(mut data) = self.get_data() { + data.is_tamed = tamed; + self.set_data(data); + } + } + + /// Gets the UUID of the player who owns this wolf, if any. + #[must_use] + pub fn get_owner(&self) -> Option { + self.get_data().and_then(|d| d.owner) + } + + /// Sets the owner of this wolf by UUID. + pub fn set_owner(&self, owner: Option) { + if let Some(mut data) = self.get_data() { + data.owner = owner; + self.set_data(data); + } + } + + /// Returns whether this wolf is currently in a sitting pose. + #[must_use] + pub fn is_sitting(&self) -> bool { + self.get_data().is_some_and(|d| d.is_sitting) + } + + /// Orders this wolf to sit or stand. + pub fn set_sitting(&self, sitting: bool) { + if let Some(mut data) = self.get_data() { + data.is_sitting = sitting; + self.set_data(data); + } + } + + /// Gets the collar dye color of this wolf. + #[must_use] + pub fn get_collar_color(&self) -> DyeColor { + self.get_data().map_or(DyeColor::Red, |d| d.collar_color) + } + + /// Sets the collar dye color of this wolf. + pub fn set_collar_color(&self, color: DyeColor) { + if let Some(mut data) = self.get_data() { + data.collar_color = color; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Cat entities. + Cat, Cat, CatData +); + +impl<'a> Cat<'a> { + /// Returns whether this cat is tamed. + #[must_use] + pub fn is_tamed(&self) -> bool { + self.get_data().is_some_and(|d| d.is_tamed) + } + + /// Sets whether this cat is tamed. + pub fn set_tamed(&self, tamed: bool) { + if let Some(mut data) = self.get_data() { + data.is_tamed = tamed; + self.set_data(data); + } + } + + /// Gets the owner UUID of this cat. + #[must_use] + pub fn get_owner(&self) -> Option { + self.get_data().and_then(|d| d.owner) + } + + /// Sets the owner UUID of this cat. + pub fn set_owner(&self, owner: Option) { + if let Some(mut data) = self.get_data() { + data.owner = owner; + self.set_data(data); + } + } + + /// Returns whether this cat is in a sitting pose. + #[must_use] + pub fn is_sitting(&self) -> bool { + self.get_data().is_some_and(|d| d.is_sitting) + } + + /// Orders this cat to sit or stand. + pub fn set_sitting(&self, sitting: bool) { + if let Some(mut data) = self.get_data() { + data.is_sitting = sitting; + self.set_data(data); + } + } + + /// Gets the collar dye color of this cat. + #[must_use] + pub fn get_collar_color(&self) -> DyeColor { + self.get_data().map_or(DyeColor::Red, |d| d.collar_color) + } + + /// Sets the collar dye color of this cat. + pub fn set_collar_color(&self, color: DyeColor) { + if let Some(mut data) = self.get_data() { + data.collar_color = color; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Villager entities. + Villager, Villager, VillagerData +); + +impl<'a> Villager<'a> { + /// Gets the profession of this villager. + #[must_use] + pub fn get_profession(&self) -> VillagerProfession { + self.get_data() + .map_or(VillagerProfession::None, |d| d.profession) + } + + /// Sets the profession of this villager. + pub fn set_profession(&self, profession: VillagerProfession) { + if let Some(mut data) = self.get_data() { + data.profession = profession; + self.set_data(data); + } + } + + /// Gets the trading level of this villager (1-5). + #[must_use] + pub fn get_level(&self) -> u8 { + self.get_data().map_or(1, |d| d.level) + } + + /// Sets the trading level of this villager (1-5). + pub fn set_level(&self, level: u8) { + if let Some(mut data) = self.get_data() { + data.level = level; + self.set_data(data); + } + } + + /// Gets the trading experience points of this villager. + #[must_use] + pub fn get_experience(&self) -> u32 { + self.get_data().map_or(0, |d| d.experience) + } + + /// Sets the trading experience points of this villager. + pub fn set_experience(&self, experience: u32) { + if let Some(mut data) = self.get_data() { + data.experience = experience; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Creeper entities. + Creeper, Creeper, CreeperData +); + +impl<'a> Creeper<'a> { + /// Returns whether this creeper is charged (struck by lightning). + #[must_use] + pub fn is_powered(&self) -> bool { + self.get_data().is_some_and(|d| d.is_powered) + } + + /// Sets whether this creeper is powered/charged. + pub fn set_powered(&self, powered: bool) { + if let Some(mut data) = self.get_data() { + data.is_powered = powered; + self.set_data(data); + } + } + + /// Gets the fuse duration of this creeper in ticks. + #[must_use] + pub fn get_fuse(&self) -> i32 { + self.get_data().map_or(30, |d| d.fuse) + } + + /// Sets the fuse duration of this creeper in ticks. + pub fn set_fuse(&self, fuse: i32) { + if let Some(mut data) = self.get_data() { + data.fuse = fuse; + self.set_data(data); + } + } + + /// Returns whether this creeper has been manually ignited with flint and steel. + #[must_use] + pub fn is_ignited(&self) -> bool { + self.get_data().is_some_and(|d| d.is_ignited) + } + + /// Sets whether this creeper is ignited. + pub fn set_ignited(&self, ignited: bool) { + if let Some(mut data) = self.get_data() { + data.is_ignited = ignited; + self.set_data(data); + } + } + + /// Gets the explosion radius of this creeper. + #[must_use] + pub fn get_explosion_radius(&self) -> u8 { + self.get_data().map_or(3, |d| d.explosion_radius) + } + + /// Sets the explosion radius of this creeper. + pub fn set_explosion_radius(&self, radius: u8) { + if let Some(mut data) = self.get_data() { + data.explosion_radius = radius; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Slime and Magma Cube entities. + Slime, Slime, SlimeData +); + +impl<'a> Slime<'a> { + /// Gets the size scale of this slime. + #[must_use] + pub fn get_size(&self) -> i32 { + self.get_data().map_or(1, |d| d.size) + } + + /// Sets the size scale of this slime. + pub fn set_size(&self, size: i32) { + if let Some(mut data) = self.get_data() { + data.size = size; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Enderman entities. + Enderman, Enderman, EndermanData +); + +impl<'a> Enderman<'a> { + /// Gets the numerical block state ID carried by this enderman, if any. + #[must_use] + pub fn get_carried_block(&self) -> Option { + self.get_data().and_then(|d| d.carried_block_state) + } + + /// Sets the carried block state ID for this enderman. + pub fn set_carried_block(&self, block_state: Option) { + if let Some(mut data) = self.get_data() { + data.carried_block_state = block_state; + self.set_data(data); + } + } + + /// Returns whether this enderman is currently screaming (angry). + #[must_use] + pub fn is_screaming(&self) -> bool { + self.get_data().is_some_and(|d| d.is_screaming) + } + + /// Returns whether this enderman is staring at a player. + #[must_use] + pub fn is_staring(&self) -> bool { + self.get_data().is_some_and(|d| d.is_staring) + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Iron Golem entities. + IronGolem, IronGolem, IronGolemData +); + +impl<'a> IronGolem<'a> { + /// Returns whether this iron golem was created by a player. + #[must_use] + pub fn is_player_created(&self) -> bool { + self.get_data().is_some_and(|d| d.is_player_created) + } + + /// Sets whether this iron golem is considered player-created. + pub fn set_player_created(&self, created: bool) { + if let Some(mut data) = self.get_data() { + data.is_player_created = created; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Fox entities. + Fox, Fox, FoxData +); + +impl<'a> Fox<'a> { + /// Returns whether this fox is sitting. + #[must_use] + pub fn is_sitting(&self) -> bool { + self.get_data().is_some_and(|d| d.is_sitting) + } + + /// Sets whether this fox is sitting. + pub fn set_sitting(&self, sitting: bool) { + if let Some(mut data) = self.get_data() { + data.is_sitting = sitting; + self.set_data(data); + } + } + + /// Returns whether this fox is sleeping. + #[must_use] + pub fn is_sleeping(&self) -> bool { + self.get_data().is_some_and(|d| d.is_sleeping) + } + + /// Sets whether this fox is sleeping. + pub fn set_sleeping(&self, sleeping: bool) { + if let Some(mut data) = self.get_data() { + data.is_sleeping = sleeping; + self.set_data(data); + } + } + + /// Returns whether this fox is crouching. + #[must_use] + pub fn is_crouching(&self) -> bool { + self.get_data().is_some_and(|d| d.is_crouching) + } + + /// Sets whether this fox is crouching. + pub fn set_crouching(&self, crouching: bool) { + if let Some(mut data) = self.get_data() { + data.is_crouching = crouching; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Shulker entities. + Shulker, Shulker, ShulkerData +); + +impl<'a> Shulker<'a> { + /// Gets the attached block direction of this shulker. + #[must_use] + pub fn get_attached_face(&self) -> BlockDirection { + self.get_data() + .map_or(BlockDirection::Down, |d| d.attached_face) + } + + /// Sets the attached block direction of this shulker. + pub fn set_attached_face(&self, face: BlockDirection) { + if let Some(mut data) = self.get_data() { + data.attached_face = face; + self.set_data(data); + } + } + + /// Gets the raw peek amount of this shulker (0-100). + #[must_use] + pub fn get_peek_amount(&self) -> u8 { + self.get_data().map_or(0, |d| d.peek_amount) + } + + /// Sets the raw peek amount of this shulker (0-100). + pub fn set_peek_amount(&self, amount: u8) { + if let Some(mut data) = self.get_data() { + data.peek_amount = amount; + self.set_data(data); + } + } + + /// Gets the custom dye color of this shulker, if dyed. + #[must_use] + pub fn get_color(&self) -> Option { + self.get_data().and_then(|d| d.color) + } + + /// Sets or removes the custom dye color of this shulker. + pub fn set_color(&self, color: Option) { + if let Some(mut data) = self.get_data() { + data.color = color; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Zombie entities. + Zombie, Zombie, ZombieData +); + +impl<'a> Zombie<'a> { + /// Returns whether this zombie is a baby. + #[must_use] + pub fn is_baby(&self) -> bool { + self.get_data().is_some_and(|d| d.is_baby) + } + + /// Sets whether this zombie is a baby. + pub fn set_baby(&self, baby: bool) { + if let Some(mut data) = self.get_data() { + data.is_baby = baby; + self.set_data(data); + } + } + + /// Returns whether this zombie is capable of breaking doors. + #[must_use] + pub fn can_break_doors(&self) -> bool { + self.get_data().is_some_and(|d| d.can_break_doors) + } + + /// Sets whether this zombie can break doors. + pub fn set_can_break_doors(&self, can_break: bool) { + if let Some(mut data) = self.get_data() { + data.can_break_doors = can_break; + self.set_data(data); + } + } +} + +define_mob_wrapper!( + /// Specialized wrapper for Ageable animal mobs (cows, pigs, chickens, rabbits, etc.). + Ageable, Ageable, AgeableData +); + +impl<'a> Ageable<'a> { + /// Returns whether this animal is a baby. + #[must_use] + pub fn is_baby(&self) -> bool { + self.get_data().is_some_and(|d| d.is_baby) + } + + /// Sets whether this animal is a baby. + pub fn set_baby(&self, baby: bool) { + if let Some(mut data) = self.get_data() { + data.is_baby = baby; + self.set_data(data); + } + } + + /// Gets the age of this animal in ticks (negative for babies). + #[must_use] + pub fn get_age(&self) -> i32 { + self.get_data().map_or(0, |d| d.age) + } + + /// Sets the age of this animal in ticks. + pub fn set_age(&self, age: i32) { + if let Some(mut data) = self.get_data() { + data.age = age; + self.set_data(data); + } + } +} + +/// Extension trait providing generic `.cast::()` downcasting on [`Entity`] and [`Mob`]. +pub trait EntityCastExt { + /// Attempts to cast this entity or mob reference to a specialized mob wrapper type. + fn cast<'a, T: MobCast<'a>>(&'a self) -> Option; +} + +impl EntityCastExt for Mob { + fn cast<'a, T: MobCast<'a>>(&'a self) -> Option { + T::from_mob(self) + } +} + +impl EntityCastExt for Entity { + fn cast<'a, T: MobCast<'a>>(&'a self) -> Option { + T::from_entity(self) + } +} + +impl EntityCastExt for LivingEntity { + fn cast<'a, T: MobCast<'a>>(&'a self) -> Option { + T::from_living(self) + } +} diff --git a/crates/pumpkin-plugin-wit b/crates/pumpkin-plugin-wit index 10f15637d..a1ba8b3db 160000 --- a/crates/pumpkin-plugin-wit +++ b/crates/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit 10f15637dcfd79371c3e40796f7e2575b26876cd +Subproject commit a1ba8b3db2d39b69cd2fc00b6517d54476d64e51 diff --git a/crates/pumpkin-world/Cargo.toml b/crates/pumpkin-world/Cargo.toml index 7c11cd934..f1675a8f2 100644 --- a/crates/pumpkin-world/Cargo.toml +++ b/crates/pumpkin-world/Cargo.toml @@ -55,7 +55,6 @@ tokio-util = { workspace = true, features = ["rt"] } rand.workspace = true rustc-hash.workspace = true slotmap.workspace = true -crossfire.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/pumpkin-world/benches/chunk_io.rs b/crates/pumpkin-world/benches/chunk_io.rs index 9cdc50198..1ad5c8a38 100644 --- a/crates/pumpkin-world/benches/chunk_io.rs +++ b/crates/pumpkin-world/benches/chunk_io.rs @@ -73,11 +73,8 @@ fn bench_chunk_deserialization(c: &mut Criterion) { let Chunk::Level(chunk) = chunk else { panic!("full generation must return a level chunk"); }; - let runtime = tokio::runtime::Builder::new_current_thread() - .build() - .expect("failed to create benchmark runtime"); - let bytes = runtime - .block_on(chunk.to_bytes()) + let bytes = chunk + .to_bytes() .expect("failed to serialize benchmark chunk"); let position = Vector2::new(chunk.x, chunk.z); diff --git a/crates/pumpkin-world/src/chunk/format/anvil.rs b/crates/pumpkin-world/src/chunk/format/anvil.rs index 9441587a6..b792e66ae 100644 --- a/crates/pumpkin-world/src/chunk/format/anvil.rs +++ b/crates/pumpkin-world/src/chunk/format/anvil.rs @@ -8,7 +8,6 @@ use std::{ io::{Read, SeekFrom, Write}, marker::PhantomData, path::{Path, PathBuf}, - pin::Pin, time::{SystemTime, UNIX_EPOCH}, }; use tokio::{ @@ -308,7 +307,7 @@ impl AnvilChunkData { } } - async fn from_chunk( + fn from_chunk( chunk: &S, compression: Option, chunk_config: &AnvilChunkConfig, @@ -318,7 +317,6 @@ impl AnvilChunkData { { let raw_bytes = chunk .to_bytes() - .await .map_err(|err| ChunkWritingError::ChunkSerializingError(err.to_string()))?; let compression = compression.unwrap_or_else(|| chunk_config.compression.algorithm.into()); @@ -498,9 +496,7 @@ impl Default for AnvilChunkFile { } pub trait SingleChunkDataSerializer: Send + Sync + Sized + Dirtiable + 'static { - fn to_bytes( - &self, - ) -> Pin> + Send + '_>>; + fn to_bytes(&self) -> Result; fn from_bytes(bytes: &Bytes, pos: Vector2) -> Result; fn position(&self) -> (i32, i32); } @@ -620,8 +616,7 @@ impl ChunkSerializer for AnvilChunkFile< let compression_type = self.chunks_data[index] .as_ref() .and_then(|chunk_data| chunk_data.serialized_data.compression); - let new_chunk_data = - AnvilChunkData::from_chunk(chunk, compression_type, chunk_config).await?; + let new_chunk_data = AnvilChunkData::from_chunk(chunk, compression_type, chunk_config)?; let mut write_action = self.write_action.lock().await; if !chunk_config.write_in_place { diff --git a/crates/pumpkin-world/src/chunk/format/linear.rs b/crates/pumpkin-world/src/chunk/format/linear.rs index fd7b4943f..8136ab3ce 100644 --- a/crates/pumpkin-world/src/chunk/format/linear.rs +++ b/crates/pumpkin-world/src/chunk/format/linear.rs @@ -572,7 +572,6 @@ impl ChunkSerializer for LinearV2File let index = Self::get_chunk_index(chunk.position().0, chunk.position().1); let chunk_raw: Bytes = chunk .to_bytes() - .await .map_err(|err| ChunkWritingError::ChunkSerializingError(err.to_string()))?; self.timestamps[index] = SystemTime::now() diff --git a/crates/pumpkin-world/src/chunk/format/mod.rs b/crates/pumpkin-world/src/chunk/format/mod.rs index c2f86ac09..87e09abe3 100644 --- a/crates/pumpkin-world/src/chunk/format/mod.rs +++ b/crates/pumpkin-world/src/chunk/format/mod.rs @@ -1,6 +1,5 @@ use std::{ path::PathBuf, - pin::Pin, str::FromStr, sync::{ RwLock, @@ -13,7 +12,6 @@ use pumpkin_data::{Block, BlockStateId, chunk::ChunkStatus, fluid::Fluid}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::resource_location::{FromResourceLocation, ResourceLocation, ToResourceLocation}; use rustc_hash::FxHashMap; -use tokio::sync::Mutex; use crate::{ chunk::{ @@ -43,10 +41,8 @@ impl SingleChunkDataSerializer for ChunkData { } #[inline] - fn to_bytes( - &self, - ) -> Pin> + Send + '_>> { - Box::pin(async move { Ok(self.internal_to_bytes()) }) + fn to_bytes(&self) -> Result { + Ok(self.internal_to_bytes()) } #[inline] @@ -707,10 +703,8 @@ impl SingleChunkDataSerializer for ChunkEntityData { } #[inline] - fn to_bytes( - &self, - ) -> Pin> + Send + '_>> { - Box::pin(async move { self.internal_to_bytes().await }) + fn to_bytes(&self) -> Result { + Ok(self.internal_to_bytes()) } #[inline] @@ -775,12 +769,12 @@ impl ChunkEntityData { Ok(Self { x: position.x, z: position.y, - data: Mutex::new(entities), + data: std::sync::Mutex::new(entities), dirty: AtomicBool::new(false), }) } - async fn internal_to_bytes(&self) -> Result { + fn internal_to_bytes(&self) -> Bytes { let mut root = NbtCompound::new(); root.put_int("DataVersion", WORLD_DATA_VERSION); root.put( @@ -790,14 +784,14 @@ impl ChunkEntityData { let entities_tag: Vec = self .data .lock() - .await + .unwrap_or_else(std::sync::PoisonError::into_inner) .iter() .map(|c| pumpkin_nbt::tag::NbtTag::Compound(c.clone())) .collect(); root.put_list("Entities", entities_tag); let nbt = pumpkin_nbt::Nbt::from(root); - Ok(nbt.write()) + nbt.write() } } diff --git a/crates/pumpkin-world/src/chunk/format/pump.rs b/crates/pumpkin-world/src/chunk/format/pump.rs index 617dd177c..e3e8d8fd9 100644 --- a/crates/pumpkin-world/src/chunk/format/pump.rs +++ b/crates/pumpkin-world/src/chunk/format/pump.rs @@ -108,7 +108,6 @@ where let bytes = chunk_data .to_bytes() - .await .map_err(|e| ChunkWritingError::ChunkSerializingError(e.to_string()))?; let compressed = compress_to_vec(&bytes[..], CompressionLevel::Fastest); @@ -165,9 +164,6 @@ mod tests { use crate::chunk::io::{ChunkSerializer, LoadedData}; use bytes::Bytes; use pumpkin_util::math::vector2::Vector2; - use serde::{Deserialize, Serialize}; - use std::future::Future; - use std::pin::Pin; use tempfile::TempDir; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -185,17 +181,14 @@ mod tests { } impl SingleChunkDataSerializer for MockChunk { - fn to_bytes( - &self, - ) -> Pin> + Send + '_>> - { + fn to_bytes(&self) -> Result { let mut root = pumpkin_nbt::compound::NbtCompound::new(); root.put_int("x", self.x); root.put_int("z", self.z); let i8_vec: Vec = self.data.iter().map(|&b| b as i8).collect(); root.put("data", pumpkin_nbt::tag::NbtTag::ByteArray(i8_vec.into())); let bytes = pumpkin_nbt::Nbt::from(root).write_unnamed(); - Box::pin(async move { Ok(bytes) }) + Ok(bytes) } fn from_bytes(bytes: &Bytes, pos: Vector2) -> Result { let mut cursor = std::io::Cursor::new(bytes); diff --git a/crates/pumpkin-world/src/chunk/io/file_manager.rs b/crates/pumpkin-world/src/chunk/io/file_manager.rs index 4d2acdc0b..019bbb873 100644 --- a/crates/pumpkin-world/src/chunk/io/file_manager.rs +++ b/crates/pumpkin-world/src/chunk/io/file_manager.rs @@ -13,10 +13,7 @@ use tokio::{ use tracing::{debug, error, trace}; use crate::{ - chunk::{ - ChunkReadingError, ChunkWritingError, - io::{BoxFuture, Dirtiable}, - }, + chunk::{ChunkReadingError, ChunkWritingError, io::Dirtiable}, level::LevelFolder, }; @@ -201,230 +198,209 @@ where { type Data = Arc; - fn watch_chunks<'a>( - &'a self, - folder: &'a LevelFolder, - chunks: &'a [Vector2], - ) -> BoxFuture<'a, ()> { - Box::pin(async move { - let paths: Vec<_> = chunks - .iter() - .map(|c| P::file_path(folder, &S::get_chunk_key(c))) - .collect(); + async fn watch_chunks<'a>(&'a self, folder: &'a LevelFolder, chunks: &'a [Vector2]) { + let paths: Vec<_> = chunks + .iter() + .map(|c| P::file_path(folder, &S::get_chunk_key(c))) + .collect(); - let mut watchers = self.watchers.write().await; - for path in paths { - *watchers.entry(path).or_insert(0) += 1; - } - }) + let mut watchers = self.watchers.write().await; + for path in paths { + *watchers.entry(path).or_insert(0) += 1; + } } - fn unwatch_chunks<'a>( - &'a self, - folder: &'a LevelFolder, - chunks: &'a [Vector2], - ) -> BoxFuture<'a, ()> { - Box::pin(async move { - let paths: Vec<_> = chunks - .iter() - .map(|c| P::file_path(folder, &S::get_chunk_key(c))) - .collect(); + async fn unwatch_chunks<'a>(&'a self, folder: &'a LevelFolder, chunks: &'a [Vector2]) { + let paths: Vec<_> = chunks + .iter() + .map(|c| P::file_path(folder, &S::get_chunk_key(c))) + .collect(); - let mut paths_to_evict = Vec::new(); - { - let mut watchers = self.watchers.write().await; - for path in paths { - if let std::collections::btree_map::Entry::Occupied(mut e) = - watchers.entry(path) - { - let count = e.get_mut(); - *count = count.saturating_sub(1); - if *count == 0 { - let (path, _) = e.remove_entry(); - paths_to_evict.push(path); - } + let mut paths_to_evict = Vec::new(); + { + let mut watchers = self.watchers.write().await; + for path in paths { + if let std::collections::btree_map::Entry::Occupied(mut e) = watchers.entry(path) { + let count = e.get_mut(); + *count = count.saturating_sub(1); + if *count == 0 { + let (path, _) = e.remove_entry(); + paths_to_evict.push(path); } } } + } - for path in paths_to_evict { - self.maybe_evict(&path).await; - } - }) + for path in paths_to_evict { + self.maybe_evict(&path).await; + } } - fn clear_watched_chunks(&self) -> BoxFuture<'_, ()> { - Box::pin(async move { - let paths: Vec = { - let mut watchers = self.watchers.write().await; - let keys: Vec<_> = watchers.keys().cloned().collect(); - watchers.clear(); - keys - }; - for path in paths { - self.maybe_evict(&path).await; - } - }) + async fn clear_watched_chunks(&self) { + let paths: Vec = { + let mut watchers = self.watchers.write().await; + let keys: Vec<_> = watchers.keys().cloned().collect(); + watchers.clear(); + keys + }; + for path in paths { + self.maybe_evict(&path).await; + } } - fn fetch_chunks<'a>( + async fn fetch_chunks<'a>( &'a self, folder: &'a LevelFolder, chunk_coords: &'a [Vector2], stream: mpsc::Sender>, - ) -> BoxFuture<'a, ()> { - Box::pin(async move { - // Group requested chunk coords by their region file. - let mut regions_chunks: BTreeMap>> = BTreeMap::new(); - for at in chunk_coords { - regions_chunks - .entry(S::get_chunk_key(at)) - .or_default() - .push(*at); + ) { + // Group requested chunk coords by their region file. + let mut regions_chunks: BTreeMap>> = BTreeMap::new(); + for at in chunk_coords { + regions_chunks + .entry(S::get_chunk_key(at)) + .or_default() + .push(*at); + } + + let region_tasks = regions_chunks.into_iter().map(|(file_name, chunks)| { + let task_stream = stream.clone(); + async move { + let path = P::file_path(folder, &file_name); + + let chunk_serializer = match self.get_serializer(&path).await { + Ok(s) => s, + Err(ChunkReadingError::ChunkNotExist) => { + return; + } + Err(err) => { + // Best-effort: report the error for the first coord in the batch. + let _ = task_stream.send(LoadedData::Error((chunks[0], err))).await; + return; + } + }; + + // A bounded channel of 1 keeps backpressure between the + // serializer and the caller without unbounded buffering. + let (send, mut recv) = mpsc::channel::>(1); + + // Forward received chunks, wrapping them in `Arc`. + // Captured move is intentional — `task_stream` is consumed here. + let forward = async move { + while let Some(data) = recv.recv().await { + let wrapped = data.map_loaded(Arc::new); + if task_stream.send(wrapped).await.is_err() { + // Receiver dropped; abort early to avoid wasted work. + return; + } + } + }; + + // Hold the read lock only for the duration of `get_chunks`. + let read = async move { + let serializer = chunk_serializer.read().await; + serializer.get_chunks(chunks, send).await; + }; + + join!(forward, read); + + // Evict if not watched and references are dropped + self.maybe_evict(&path).await; } + }); - let region_tasks = regions_chunks.into_iter().map(|(file_name, chunks)| { - let task_stream = stream.clone(); - async move { - let path = P::file_path(folder, &file_name); - - let chunk_serializer = match self.get_serializer(&path).await { - Ok(s) => s, - Err(ChunkReadingError::ChunkNotExist) => { - return; - } - Err(err) => { - // Best-effort: report the error for the first coord in the batch. - let _ = task_stream.send(LoadedData::Error((chunks[0], err))).await; - return; - } - }; - - // A bounded channel of 1 keeps backpressure between the - // serializer and the caller without unbounded buffering. - let (send, mut recv) = - mpsc::channel::>(1); - - // Forward received chunks, wrapping them in `Arc`. - // Captured move is intentional — `task_stream` is consumed here. - let forward = async move { - while let Some(data) = recv.recv().await { - let wrapped = data.map_loaded(Arc::new); - if task_stream.send(wrapped).await.is_err() { - // Receiver dropped; abort early to avoid wasted work. - return; - } - } - }; - - // Hold the read lock only for the duration of `get_chunks`. - let read = async move { - let serializer = chunk_serializer.read().await; - serializer.get_chunks(chunks, send).await; - }; - - join!(forward, read); - - // Evict if not watched and references are dropped - self.maybe_evict(&path).await; - } - }); - - join_all(region_tasks).await; - }) + join_all(region_tasks).await; } - fn save_chunks<'a>( + async fn save_chunks<'a>( &'a self, folder: &'a LevelFolder, chunks_data: Vec<(Vector2, Self::Data)>, - ) -> BoxFuture<'a, Result<(), ChunkWritingError>> { - Box::pin(async move { - // Group chunks by region file. - let mut regions_chunks: BTreeMap> = BTreeMap::new(); - for (at, chunk) in chunks_data { - regions_chunks - .entry(S::get_chunk_key(&at)) - .or_default() - .push(chunk); - } + ) -> Result<(), ChunkWritingError> { + // Group chunks by region file. + let mut regions_chunks: BTreeMap> = BTreeMap::new(); + for (at, chunk) in chunks_data { + regions_chunks + .entry(S::get_chunk_key(&at)) + .or_default() + .push(chunk); + } - let tasks = regions_chunks - .into_iter() - .map(|(file_name, chunk_locks)| async move { - let path = P::file_path(folder, &file_name); - trace!("Saving chunks into {}", path.display()); + let tasks = regions_chunks + .into_iter() + .map(|(file_name, chunk_locks)| async move { + let path = P::file_path(folder, &file_name); + trace!("Saving chunks into {}", path.display()); - let chunk_serializer = match self.get_serializer(&path).await { - Ok(s) => s, - Err(ChunkReadingError::ChunkNotExist) => { - return Err(ChunkWritingError::IoError(std::io::Error::other( - "get_serializer returned ChunkNotExist", - ))); - } - Err(ChunkReadingError::IoError(err)) => { - error!("I/O error reading region before write: {err}"); - return Err(ChunkWritingError::IoError(err)); - } - Err(err) => { - return Err(ChunkWritingError::IoError(std::io::Error::other( - err.to_string(), - ))); - } - }; + let chunk_serializer = match self.get_serializer(&path).await { + Ok(s) => s, + Err(ChunkReadingError::ChunkNotExist) => { + return Err(ChunkWritingError::IoError(std::io::Error::other( + "get_serializer returned ChunkNotExist", + ))); + } + Err(ChunkReadingError::IoError(err)) => { + error!("I/O error reading region before write: {err}"); + return Err(ChunkWritingError::IoError(err)); + } + Err(err) => { + return Err(ChunkWritingError::IoError(std::io::Error::other( + err.to_string(), + ))); + } + }; + { + let mut writer = chunk_serializer.write().await; + for chunk in &chunk_locks { + // Atomically snapshot and clear the dirty flag before we + // write so that any mutation that races in *during* this + // serialisation round will mark dirty again correctly. + let was_dirty = chunk.is_dirty(); + chunk.mark_dirty(false); + + if was_dirty { + writer.update_chunk(&**chunk, &self.chunk_config).await?; + } + } + // Write-lock released here — flush can proceed under a read-lock. + } + + trace!("Chunk data updated for {}", path.display()); + + // We check watchers *after* releasing the write-lock to honour + // lock ordering (serializer lock → watchers, never the reverse). + let is_watched = { + let watchers = self.watchers.read().await; + watchers.get(&path).is_some_and(|&c| c > 0) + }; + + if !is_watched { + // A read-lock suffices for `write()` since we have already + // applied all mutations above. { - let mut writer = chunk_serializer.write().await; - for chunk in &chunk_locks { - // Atomically snapshot and clear the dirty flag before we - // write so that any mutation that races in *during* this - // serialisation round will mark dirty again correctly. - let was_dirty = chunk.is_dirty(); - chunk.mark_dirty(false); - - if was_dirty { - writer.update_chunk(&**chunk, &self.chunk_config).await?; - } - } - // Write-lock released here — flush can proceed under a read-lock. - } - - trace!("Chunk data updated for {}", path.display()); - - // We check watchers *after* releasing the write-lock to honour - // lock ordering (serializer lock → watchers, never the reverse). - let is_watched = { - let watchers = self.watchers.read().await; - watchers.get(&path).is_some_and(|&c| c > 0) + let serializer = chunk_serializer.read().await; + debug!("Flushing {} to disk", path.display()); + serializer + .write(&path) + .await + .map_err(ChunkWritingError::IoError)?; + // Read-lock released here. }; - if !is_watched { - // A read-lock suffices for `write()` since we have already - // applied all mutations above. - { - let serializer = chunk_serializer.read().await; - debug!("Flushing {} to disk", path.display()); - serializer - .write(&path) - .await - .map_err(ChunkWritingError::IoError)?; - // Read-lock released here. - }; + // Drop our handle so `can_remove` may succeed. + drop(chunk_serializer); - // Drop our handle so `can_remove` may succeed. - drop(chunk_serializer); + // Evict the cache entry when no longer needed. + self.maybe_evict(&path).await; + } - // Evict the cache entry when no longer needed. - self.maybe_evict(&path).await; - } + Ok(()) + }); - Ok(()) - }); - - // Collect all region results; surface the first error encountered. - let results: Vec> = join_all(tasks).await; - results.into_iter().find(Result::is_err).unwrap_or(Ok(())) - }) + // Collect all region results; surface the first error encountered. + let results: Vec> = join_all(tasks).await; + results.into_iter().find(Result::is_err).unwrap_or(Ok(())) } /// Blocks until all in-flight serialiser operations have completed by @@ -433,27 +409,25 @@ where /// /// This is a linearisation point: after this future resolves no mutation /// started before the call is still running. - fn block_and_await_ongoing_tasks(&self) -> BoxFuture<'_, ()> { - Box::pin(async move { - // Snapshot the current set of loaders under a read-lock so we do - // not block new insertions longer than necessary. - let loaders: Vec>> = - { self.file_locks.read().await.values().cloned().collect() }; + async fn block_and_await_ongoing_tasks(&self) { + // Snapshot the current set of loaders under a read-lock so we do + // not block new insertions longer than necessary. + let loaders: Vec>> = + { self.file_locks.read().await.values().cloned().collect() }; - // For each loader that has been initialised, acquire a write-lock - // and release it immediately. This guarantees that any concurrent - // read or write operation that was in progress has finished. - let drain_tasks = loaders.into_iter().map(|loader| async move { - if let Some(serializer_arc) = loader.internal.get() { - // Acquiring + immediately dropping the write-lock acts as a - // barrier: it can only succeed once all current lock holders - // have released their guards. - let _guard = serializer_arc.write().await; - } - }); + // For each loader that has been initialised, acquire a write-lock + // and release it immediately. This guarantees that any concurrent + // read or write operation that was in progress has finished. + let drain_tasks = loaders.into_iter().map(|loader| async move { + if let Some(serializer_arc) = loader.internal.get() { + // Acquiring + immediately dropping the write-lock acts as a + // barrier: it can only succeed once all current lock holders + // have released their guards. + let _guard = serializer_arc.write().await; + } + }); - join_all(drain_tasks).await; - }) + join_all(drain_tasks).await; } } @@ -480,68 +454,60 @@ where { type Data = Arc

; - fn fetch_chunks<'a>( + async fn fetch_chunks<'a>( &'a self, folder: &'a LevelFolder, chunk_coords: &'a [Vector2], stream: tokio::sync::mpsc::Sender>, - ) -> BoxFuture<'a, ()> { + ) { match self { - Self::Linear(io) => io.fetch_chunks(folder, chunk_coords, stream), - Self::Anvil(io) => io.fetch_chunks(folder, chunk_coords, stream), - Self::Pump(io) => io.fetch_chunks(folder, chunk_coords, stream), + Self::Linear(io) => io.fetch_chunks(folder, chunk_coords, stream).await, + Self::Anvil(io) => io.fetch_chunks(folder, chunk_coords, stream).await, + Self::Pump(io) => io.fetch_chunks(folder, chunk_coords, stream).await, } } - fn save_chunks<'a>( + async fn save_chunks<'a>( &'a self, folder: &'a LevelFolder, chunks_data: Vec<(Vector2, Self::Data)>, - ) -> BoxFuture<'a, Result<(), ChunkWritingError>> { + ) -> Result<(), ChunkWritingError> { match self { - Self::Linear(io) => io.save_chunks(folder, chunks_data), - Self::Anvil(io) => io.save_chunks(folder, chunks_data), - Self::Pump(io) => io.save_chunks(folder, chunks_data), + Self::Linear(io) => io.save_chunks(folder, chunks_data).await, + Self::Anvil(io) => io.save_chunks(folder, chunks_data).await, + Self::Pump(io) => io.save_chunks(folder, chunks_data).await, } } - fn watch_chunks<'a>( - &'a self, - folder: &'a LevelFolder, - chunks: &'a [Vector2], - ) -> BoxFuture<'a, ()> { + async fn watch_chunks<'a>(&'a self, folder: &'a LevelFolder, chunks: &'a [Vector2]) { match self { - Self::Linear(io) => io.watch_chunks(folder, chunks), - Self::Anvil(io) => io.watch_chunks(folder, chunks), - Self::Pump(io) => io.watch_chunks(folder, chunks), + Self::Linear(io) => io.watch_chunks(folder, chunks).await, + Self::Anvil(io) => io.watch_chunks(folder, chunks).await, + Self::Pump(io) => io.watch_chunks(folder, chunks).await, } } - fn unwatch_chunks<'a>( - &'a self, - folder: &'a LevelFolder, - chunks: &'a [Vector2], - ) -> BoxFuture<'a, ()> { + async fn unwatch_chunks<'a>(&'a self, folder: &'a LevelFolder, chunks: &'a [Vector2]) { match self { - Self::Linear(io) => io.unwatch_chunks(folder, chunks), - Self::Anvil(io) => io.unwatch_chunks(folder, chunks), - Self::Pump(io) => io.unwatch_chunks(folder, chunks), + Self::Linear(io) => io.unwatch_chunks(folder, chunks).await, + Self::Anvil(io) => io.unwatch_chunks(folder, chunks).await, + Self::Pump(io) => io.unwatch_chunks(folder, chunks).await, } } - fn clear_watched_chunks(&self) -> BoxFuture<'_, ()> { + async fn clear_watched_chunks(&self) { match self { - Self::Linear(io) => io.clear_watched_chunks(), - Self::Anvil(io) => io.clear_watched_chunks(), - Self::Pump(io) => io.clear_watched_chunks(), + Self::Linear(io) => io.clear_watched_chunks().await, + Self::Anvil(io) => io.clear_watched_chunks().await, + Self::Pump(io) => io.clear_watched_chunks().await, } } - fn block_and_await_ongoing_tasks(&self) -> BoxFuture<'_, ()> { + async fn block_and_await_ongoing_tasks(&self) { match self { - Self::Linear(io) => io.block_and_await_ongoing_tasks(), - Self::Anvil(io) => io.block_and_await_ongoing_tasks(), - Self::Pump(io) => io.block_and_await_ongoing_tasks(), + Self::Linear(io) => io.block_and_await_ongoing_tasks().await, + Self::Anvil(io) => io.block_and_await_ongoing_tasks().await, + Self::Pump(io) => io.block_and_await_ongoing_tasks().await, } } } diff --git a/crates/pumpkin-world/src/chunk/io/mod.rs b/crates/pumpkin-world/src/chunk/io/mod.rs index 743bd25b8..593e7ba8f 100644 --- a/crates/pumpkin-world/src/chunk/io/mod.rs +++ b/crates/pumpkin-world/src/chunk/io/mod.rs @@ -1,4 +1,4 @@ -use std::{error, pin::Pin}; +use std::error; use bytes::Bytes; use pumpkin_util::math::vector2::Vector2; @@ -38,8 +38,6 @@ pub trait Dirtiable { fn mark_dirty(&self, flag: bool); } -type BoxFuture<'a, T> = Pin + Send + 'a>>; - /// Trait to handle the IO of chunks /// for loading and saving chunks data /// can be implemented for different types of IO @@ -59,34 +57,34 @@ where folder: &'a LevelFolder, chunk_coords: &'a [Vector2], stream: tokio::sync::mpsc::Sender>, - ) -> BoxFuture<'a, ()>; // Returns BoxFuture<()> + ) -> impl Future + Send + 'a; /// Persist the chunks data fn save_chunks<'a>( &'a self, folder: &'a LevelFolder, chunks_data: Vec<(Vector2, Self::Data)>, - ) -> BoxFuture<'a, Result<(), ChunkWritingError>>; // Returns BoxFuture + ) -> impl Future> + Send + 'a; /// Tells the `ChunkIO` that these chunks are currently loaded in memory fn watch_chunks<'a>( &'a self, folder: &'a LevelFolder, chunks: &'a [Vector2], - ) -> BoxFuture<'a, ()>; + ) -> impl Future + Send + 'a; /// Tells the `ChunkIO` that these chunks are no longer loaded in memory fn unwatch_chunks<'a>( &'a self, folder: &'a LevelFolder, chunks: &'a [Vector2], - ) -> BoxFuture<'a, ()>; + ) -> impl Future + Send + 'a; /// Tells the `ChunkIO` that no more chunks are loaded in memory - fn clear_watched_chunks(&self) -> BoxFuture<'_, ()>; + fn clear_watched_chunks(&self) -> impl Future + Send + '_; /// Ensure that all ongoing operations are finished - fn block_and_await_ongoing_tasks(&self) -> BoxFuture<'_, ()>; + fn block_and_await_ongoing_tasks(&self) -> impl Future + Send + '_; } /// Trait to serialize and deserialize the chunk data to and from bytes. diff --git a/crates/pumpkin-world/src/chunk/mod.rs b/crates/pumpkin-world/src/chunk/mod.rs index 8ffda88e2..dd67fd542 100644 --- a/crates/pumpkin-world/src/chunk/mod.rs +++ b/crates/pumpkin-world/src/chunk/mod.rs @@ -14,7 +14,6 @@ use std::sync::RwLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; use thiserror::Error; -use tokio::sync::Mutex; pub mod format; pub mod io; @@ -90,7 +89,7 @@ pub struct ChunkEntityData { pub x: i32, /// Chunk Z pub z: i32, - pub data: Mutex>, + pub data: std::sync::Mutex>, pub dirty: AtomicBool, } diff --git a/crates/pumpkin-world/src/chunk_system/schedule.rs b/crates/pumpkin-world/src/chunk_system/schedule.rs index 162ed19e1..63c16737a 100644 --- a/crates/pumpkin-world/src/chunk_system/schedule.rs +++ b/crates/pumpkin-world/src/chunk_system/schedule.rs @@ -3,7 +3,7 @@ use super::chunk_holder::ChunkHolder; use super::chunk_state::{Chunk, StagedChunkEnum}; use super::dag::{DAG, EdgeKey, Node, NodeKey}; use super::generation_cache::Cache; -use super::worker_logic::{RecvChunk, generation_work, io_read_work, io_write_work}; +use super::worker_logic::{RecvChunk, io_read_work, io_write_work}; use super::{ ChunkLevel, ChunkListener, ChunkLoading, ChunkPos, HashMapType, HashSetType, IOLock, LevelChannel, @@ -69,12 +69,10 @@ pub struct GenerationSchedule { running_task_count: u16, max_in_flight: u16, queue_dirty: bool, - recv_chunk: crossfire::compat::MRx<(ChunkPos, RecvChunk)>, - io_read: crossfire::compat::MTx>, - io_write: crossfire::compat::Tx>, - generate: crossfire::compat::MTx<(ChunkPos, Cache, StagedChunkEnum)>, - send_chunk: crossfire::compat::MTx<(ChunkPos, RecvChunk)>, - gen_pool: Option>, + recv_chunk: crossbeam::channel::Receiver<(ChunkPos, RecvChunk)>, + io_read: tokio::sync::mpsc::Sender>, + io_write: tokio::sync::mpsc::Sender>, + send_chunk: crossbeam::channel::Sender<(ChunkPos, RecvChunk)>, listener: Arc, lighting_config: LightingEngineConfig, last_unload: std::time::Instant, @@ -83,22 +81,17 @@ pub struct GenerationSchedule { impl GenerationSchedule { pub fn create( io_read_thread_count: usize, - gen_thread_count: usize, level: Arc, level_channel: Arc, listener: Arc, thread_tracker: &mut Vec>, - gen_pool: Option>, ) { - let (send_chunk, recv_chunk) = crossfire::compat::mpmc::unbounded_blocking(); + let (send_chunk, recv_chunk) = crossbeam::channel::unbounded(); - let (send_read_io, recv_read_io) = - crossfire::compat::mpmc::bounded_tx_blocking_rx_async(io_read_thread_count + 5); + let (send_read_io, recv_read_io) = tokio::sync::mpsc::channel(io_read_thread_count + 5); + let recv_read_io = Arc::new(tokio::sync::Mutex::new(recv_read_io)); - let (send_write_io, recv_write_io) = - crossfire::compat::spsc::bounded_tx_blocking_rx_async(500); - - let (send_gen, recv_gen) = crossfire::compat::mpmc::bounded_blocking(gen_thread_count + 5); + let (send_write_io, recv_write_io) = tokio::sync::mpsc::channel(500); let io_lock = Arc::new(( Mutex::new(HashMapType::default()), @@ -120,28 +113,8 @@ impl GenerationSchedule { io_lock.clone(), )); - if gen_pool.is_none() { - for i in 0..gen_thread_count { - let recv_gen = recv_gen.clone(); - let send_chunk = send_chunk.clone(); - let level_clone = level.clone(); - - let handle = thread::Builder::new() - .name(format!("Gen-{i}")) - .spawn(move || { - generation_work(&recv_gen, &send_chunk, &level_clone); - }) - .expect("Failed to spawn Generation Thread"); - - thread_tracker.push(handle); - } - } - - let max_in_flight = if gen_pool.is_some() { - (thread::available_parallelism().map_or(1, std::num::NonZero::get) * 4) as u16 - } else { - gen_thread_count as u16 - }; + let max_in_flight = + (thread::available_parallelism().map_or(1, std::num::NonZero::get) * 4) as u16; let level_sched = level; let lighting_config = level_sched.lighting_config; @@ -164,9 +137,7 @@ impl GenerationSchedule { recv_chunk, io_read: send_read_io, io_write: send_write_io, - generate: send_gen, send_chunk, - gen_pool, listener, chunk_map: HashMap::default(), lighting_config, @@ -446,8 +417,12 @@ impl GenerationSchedule { }) }); if all_ready { - now_ready.push(node_key); - false + if node.in_degree == 0 { + now_ready.push(node_key); + false + } else { + true + } } else { true } @@ -463,7 +438,6 @@ impl GenerationSchedule { Self::calc_priority(&self.last_level, &self.last_high_priority, n.pos, n.stage); self.queue.push(TaskHeapNode(priority, node_key)); } - // If in_degree > 0, drop_node will re-queue when unblocked } } @@ -796,7 +770,7 @@ impl GenerationSchedule { *data.entry(*pos).or_insert(0) += 1; } drop(data); - if let Err(e) = self.io_write.send(chunks) { + if let Err(e) = self.io_write.blocking_send(chunks) { error!( "Failed to send chunks to io write thread during save (may have shut down): {:?}", e @@ -851,7 +825,7 @@ impl GenerationSchedule { } drop(data); - if let Err(e) = self.io_write.send(chunks) { + if let Err(e) = self.io_write.blocking_send(chunks) { error!("Failed to send chunks to io write thread: {:?}", e); } } @@ -1311,7 +1285,10 @@ impl GenerationSchedule { io_batch.push(node.pos); if io_batch.len() >= 16 - && self.io_read.send(std::mem::take(&mut io_batch)).is_err() + && self + .io_read + .blocking_send(std::mem::take(&mut io_batch)) + .is_err() { info!("IO read thread closed, saving remaining chunks..."); self.save_all_chunk(true); @@ -1320,7 +1297,10 @@ impl GenerationSchedule { } else { // Send any pending IO batch before starting generation if !io_batch.is_empty() - && self.io_read.send(std::mem::take(&mut io_batch)).is_err() + && self + .io_read + .blocking_send(std::mem::take(&mut io_batch)) + .is_err() { info!("IO read thread closed, saving remaining chunks..."); self.save_all_chunk(true); @@ -1432,41 +1412,37 @@ impl GenerationSchedule { } self.running_task_count += 1; - if let Some(pool) = &self.gen_pool { - let pos = node.pos; - let stage = node.stage; - let send_chunk = self.send_chunk.clone(); - let level = level.clone(); - let settings = GenerationSettings::from_dimension( - level.world_gen.load().dimension(), - ); + let pos = node.pos; + let stage = node.stage; + let send_chunk = self.send_chunk.clone(); + let level = level.clone(); + let settings = + GenerationSettings::from_dimension(level.world_gen.load().dimension()); - pool.spawn(move || { - let result = crate::chunk_system::worker_logic::run_generation( - pos, cache, stage, &level, settings, - ); - let _ = send_chunk.send((pos, result)); - }); - } else if self.generate.send((node.pos, cache, node.stage)).is_err() { - self.running_task_count = self.running_task_count.saturating_sub(1); - info!("Generation thread closed, saving remaining chunks..."); - self.save_all_chunk(true); - break 'out2; - } + rayon::spawn(move || { + let result = crate::chunk_system::worker_logic::run_generation( + pos, cache, stage, &level, settings, + ); + let _ = send_chunk.send((pos, result)); + }); } } } // Flush any remaining IO batch - if !io_batch.is_empty() && self.io_read.send(std::mem::take(&mut io_batch)).is_err() { + if !io_batch.is_empty() + && self + .io_read + .blocking_send(std::mem::take(&mut io_batch)) + .is_err() + { info!("IO read thread closed, saving remaining chunks..."); self.save_all_chunk(true); } - // 3. If queue is empty, wait for work or results + // 5. Wait for work or results if self.queue.is_empty() { - // If we have tasks in flight, wait for them with timeout - if self.running_task_count > 0 || !self.waiting_for_chunks.is_empty() { + if self.running_task_count > 0 { match self.recv_chunk.recv_timeout(Duration::from_millis(5)) { Ok((pos, data)) => { self.receive_chunk(pos, data); @@ -1474,16 +1450,17 @@ impl GenerationSchedule { self.garbage_collect_dependencies(); } } - Err(crossfire::compat::RecvTimeoutError::Timeout) => { + Err(crossbeam::channel::RecvTimeoutError::Timeout) => { // Periodically check LevelChannel for new requests if self.resort_work(self.send_level.get()) { self.garbage_collect_dependencies(); } } - Err(crossfire::compat::RecvTimeoutError::Disconnected) => break, + Err(crossbeam::channel::RecvTimeoutError::Disconnected) => break, } } else { - // No tasks in flight, wait indefinitely for LevelChannel changes + // No tasks in flight, check for any unblocked waiting tasks or stranded ready tasks + self.check_waiting_tasks(); let restored = Self::restore_ready_tasks( &mut self.graph, &mut self.queue, @@ -1492,10 +1469,14 @@ impl GenerationSchedule { &self.last_high_priority, &self.waiting_for_chunks, ); - if restored > 0 { + if restored > 0 || !self.queue.is_empty() { debug!( "Restored {restored} stranded ready chunk tasks to generation queue" ); + if self.queue_dirty { + self.sort_queue(); + self.queue_dirty = false; + } continue; } debug_assert!(self.debug_check()); @@ -1508,6 +1489,27 @@ impl GenerationSchedule { self.sort_queue(); self.queue_dirty = false; } + } else if self.running_task_count >= self.max_in_flight { + // Queue has tasks, but we are at maximum in-flight capacity. + // Wait for an in-flight worker to finish instead of busy-spinning. + match self.recv_chunk.recv_timeout(Duration::from_millis(5)) { + Ok((pos, data)) => { + self.receive_chunk(pos, data); + if self.resort_work(self.send_level.get()) { + self.garbage_collect_dependencies(); + } + } + Err(crossbeam::channel::RecvTimeoutError::Timeout) => { + if self.resort_work(self.send_level.get()) { + self.garbage_collect_dependencies(); + } + } + Err(crossbeam::channel::RecvTimeoutError::Disconnected) => break, + } + if self.queue_dirty { + self.sort_queue(); + self.queue_dirty = false; + } } } info!( diff --git a/crates/pumpkin-world/src/chunk_system/worker_logic.rs b/crates/pumpkin-world/src/chunk_system/worker_logic.rs index bc95c8066..55f37e47b 100644 --- a/crates/pumpkin-world/src/chunk_system/worker_logic.rs +++ b/crates/pumpkin-world/src/chunk_system/worker_logic.rs @@ -6,7 +6,6 @@ use crate::chunk::format::LightContainer; use crate::chunk::io::LoadedData::Loaded; use crate::chunk::io::{FileIO, LoadedData}; use crate::level::Level; -use crossfire::compat::AsyncRx; use pumpkin_config::lighting::LightingEngineConfig; use pumpkin_data::chunk::ChunkStatus; use pumpkin_data::chunk_gen_settings::GenerationSettings; @@ -57,19 +56,15 @@ fn needs_relighting(chunk: &crate::chunk::ChunkData, config: LightingEngineConfi } async fn load_proto_chunk(chunk: &Arc, level: &Level) -> ProtoChunk { - if let Some(pool) = &level.gen_pool { - let (tx, rx) = tokio::sync::oneshot::channel(); - let world_gen = level.world_gen.load(); - let chunk_clone = chunk.clone(); - pool.spawn(move || { - let p = ProtoChunk::from_chunk_data(&chunk_clone, &world_gen); - let _ = tx.send(p); - }); - rx.await - .unwrap_or_else(|_| ProtoChunk::from_chunk_data(chunk, &level.world_gen.load())) - } else { - ProtoChunk::from_chunk_data(chunk, &level.world_gen.load()) - } + let (tx, rx) = tokio::sync::oneshot::channel(); + let world_gen = level.world_gen.load(); + let chunk_clone = chunk.clone(); + rayon::spawn(move || { + let p = ProtoChunk::from_chunk_data(&chunk_clone, &world_gen); + let _ = tx.send(p); + }); + rx.await + .unwrap_or_else(|_| ProtoChunk::from_chunk_data(chunk, &level.world_gen.load())) } async fn process_loaded_chunk(chunk: Arc, level: &Level) -> Chunk { @@ -91,10 +86,7 @@ async fn process_loaded_chunk(chunk: Arc, level: &Level proto.light.block_light = (0..section_count) .map(|_| LightContainer::new_empty(0)) .collect(); - - // Set stage to Features proto.stage = StagedChunkEnum::Features; - Chunk::Proto(Box::new(proto)) } else { Chunk::Level(chunk) @@ -106,15 +98,22 @@ async fn process_loaded_chunk(chunk: Arc, level: &Level } pub async fn io_read_work( - recv: crossfire::compat::MAsyncRx>, - send: crossfire::compat::MTx<(ChunkPos, RecvChunk)>, + recv: Arc>>>, + send: crossbeam::channel::Sender<(ChunkPos, RecvChunk)>, level: Arc, lock: IOLock, ) { debug!("io read thread start"); // Cleaner loop and async recv - while let Ok(batch) = recv.recv().await { + loop { + let batch = { + let mut lock_rx = recv.lock().await; + lock_rx.recv().await + }; + let Some(batch) = batch else { + break; + }; for pos in &batch { // Lock handling loop { @@ -178,10 +177,14 @@ pub async fn io_read_work( debug!("io read thread stop"); } -pub async fn io_write_work(recv: AsyncRx>, level: Arc, lock: IOLock) { +pub async fn io_write_work( + mut recv: tokio::sync::mpsc::Receiver>, + level: Arc, + lock: IOLock, +) { loop { // Don't check cancel_token here (keep saving chunks) - let Ok(data) = recv.recv().await else { break }; + let Some(data) = recv.recv().await else { break }; // debug!("io write thread receive chunks size {}", data.len()); let mut vec = Vec::with_capacity(data.len()); let mut positions = Vec::with_capacity(data.len()); @@ -287,23 +290,3 @@ pub fn run_generation( } } } - -pub fn generation_work( - recv: &crossfire::compat::MRx<(ChunkPos, Cache, StagedChunkEnum)>, - send: &crossfire::compat::MTx<(ChunkPos, RecvChunk)>, - level: &Arc, -) { - let settings = GenerationSettings::from_dimension(level.world_gen.load().dimension()); - - loop { - let Ok((pos, cache, stage)) = recv.recv() else { - debug!("generation channel closed, exiting"); - break; - }; - - let result = run_generation(pos, cache, stage, level, settings); - if send.send((pos, result)).is_err() { - break; - } - } -} diff --git a/crates/pumpkin-world/src/dimension.rs b/crates/pumpkin-world/src/dimension.rs index 8a122aff1..26537a37a 100644 --- a/crates/pumpkin-world/src/dimension.rs +++ b/crates/pumpkin-world/src/dimension.rs @@ -11,7 +11,6 @@ pub fn into_level( level_config: &LevelConfig, base_directory: PathBuf, seed: i64, - gen_pool: Option>, ) -> Arc { - Level::from_root_folder(level_config, base_directory, seed, dimension, gen_pool) + Level::from_root_folder(level_config, base_directory, seed, dimension) } diff --git a/crates/pumpkin-world/src/level.rs b/crates/pumpkin-world/src/level.rs index 032c14d44..b5f8ff456 100644 --- a/crates/pumpkin-world/src/level.rs +++ b/crates/pumpkin-world/src/level.rs @@ -114,7 +114,6 @@ pub struct Level { pub level_channel: Arc, pub thread_tracker: Mutex>>, pub chunk_listener: Arc, - pub gen_pool: Option>, } pub struct TickData { @@ -146,7 +145,6 @@ impl Level { root_folder: PathBuf, seed: i64, dimension: Dimension, - gen_pool: Option>, ) -> Arc { let (namespace, name) = match dimension.minecraft_name.split_once(':') { Some((ns, n)) => (ns, n), @@ -281,19 +279,10 @@ impl Level { level_channel: level_channel.clone(), thread_tracker, chunk_listener: listener.clone(), - gen_pool: gen_pool.clone(), }); - // TODO - let total_cores = thread::available_parallelism() - .map_or(1, std::num::NonZero::get) - .saturating_sub(2) - .max(1); - let threads_per_dimension = (total_cores / 2).max(1); - GenerationSchedule::create( 4, - threads_per_dimension, level_ref.clone(), level_channel, listener, @@ -302,7 +291,6 @@ impl Level { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .as_mut(), - gen_pool, ); level_ref @@ -319,48 +307,22 @@ impl Level { pub fn spawn_entity_generation(self: &Arc, pos: Vector2) { let level = self.clone(); - if let Some(pool) = &self.gen_pool { - pool.spawn(move || { - let arc_chunk = Arc::new(ChunkEntityData { - x: pos.x, - z: pos.y, - data: tokio::sync::Mutex::new(Vec::new()), - dirty: AtomicBool::new(false), - }); - - level.loaded_entity_chunks.insert(pos, arc_chunk.clone()); - - if let Some((_, waiters)) = level.pending_entity_generations.remove(&pos) { - for tx in waiters { - let _ = tx.send(arc_chunk.clone()); - } - } + rayon::spawn(move || { + let arc_chunk = Arc::new(ChunkEntityData { + x: pos.x, + z: pos.y, + data: std::sync::Mutex::new(Vec::new()), + dirty: AtomicBool::new(false), }); - } else { - // Fallback to spawning a new thread if no pool is available (should not happen in production) - let level_clone = level; - let _ = thread::Builder::new() - .name(format!("Entity Gen {pos:?}")) - .spawn(move || { - let arc_chunk = Arc::new(ChunkEntityData { - x: pos.x, - z: pos.y, - data: tokio::sync::Mutex::new(Vec::new()), - dirty: AtomicBool::new(false), - }); - level_clone - .loaded_entity_chunks - .insert(pos, arc_chunk.clone()); + level.loaded_entity_chunks.insert(pos, arc_chunk.clone()); - if let Some((_, waiters)) = level_clone.pending_entity_generations.remove(&pos) - { - for tx in waiters { - let _ = tx.send(arc_chunk.clone()); - } - } - }); - } + if let Some((_, waiters)) = level.pending_entity_generations.remove(&pos) { + for tx in waiters { + let _ = tx.send(arc_chunk.clone()); + } + } + }); } /// Spawns a task associated with this world. All tasks spawned with this method are awaited @@ -750,20 +712,18 @@ impl Level { let _ = sender.send((Arc::downgrade(&chunk), true)).await; } LoadedData::Missing(pos) | LoadedData::Error((pos, _)) => { - let sender_clone = sender.clone(); - let level_clone = level.clone(); - - tokio::spawn(async move { - let (tx, rx) = oneshot::channel(); - match level_clone.pending_entity_generations.entry(pos) { - dashmap::mapref::entry::Entry::Occupied(mut entry) => { - entry.get_mut().push(tx); - } - dashmap::mapref::entry::Entry::Vacant(entry) => { - entry.insert(vec![tx]); - level_clone.spawn_entity_generation(pos); - } + let (tx, rx) = oneshot::channel(); + match level.pending_entity_generations.entry(pos) { + dashmap::mapref::entry::Entry::Occupied(mut entry) => { + entry.get_mut().push(tx); } + dashmap::mapref::entry::Entry::Vacant(entry) => { + entry.insert(vec![tx]); + level.spawn_entity_generation(pos); + } + } + let sender_clone = sender.clone(); + tokio::spawn(async move { if let Ok(chunk) = rx.await { let _ = sender_clone.send((Arc::downgrade(&chunk), true)).await; @@ -807,7 +767,7 @@ impl Level { Arc::new(ChunkEntityData { x: pos.x, z: pos.y, - data: tokio::sync::Mutex::new(Vec::new()), + data: std::sync::Mutex::new(Vec::new()), dirty: AtomicBool::new(false), }) }) @@ -1025,7 +985,7 @@ mod tests { let config = LevelConfig::default(); let overworld_level = - Level::from_root_folder(&config, root.clone(), 0, Dimension::OVERWORLD, None); + Level::from_root_folder(&config, root.clone(), 0, Dimension::OVERWORLD); assert_eq!( overworld_level.level_folder.dim_folder, root.join("dimensions").join("minecraft").join("overworld") @@ -1038,14 +998,13 @@ mod tests { .join("region") ); - let nether_level = - Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_NETHER, None); + let nether_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_NETHER); assert_eq!( nether_level.level_folder.dim_folder, root.join("dimensions").join("minecraft").join("the_nether") ); - let end_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_END, None); + let end_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_END); assert_eq!( end_level.level_folder.dim_folder, root.join("dimensions").join("minecraft").join("the_end") @@ -1064,14 +1023,13 @@ mod tests { std::fs::create_dir_all(root.join("DIM1").join("region")).unwrap(); let overworld_level = - Level::from_root_folder(&config, root.clone(), 0, Dimension::OVERWORLD, None); + Level::from_root_folder(&config, root.clone(), 0, Dimension::OVERWORLD); assert_eq!(overworld_level.level_folder.dim_folder, root); - let nether_level = - Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_NETHER, None); + let nether_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_NETHER); assert_eq!(nether_level.level_folder.dim_folder, root.join("DIM-1")); - let end_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_END, None); + let end_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_END); assert_eq!(end_level.level_folder.dim_folder, root.join("DIM1")); } } diff --git a/crates/pumpkin/src/block/entities/mod.rs b/crates/pumpkin/src/block/entities/mod.rs index 3d1068f11..d1c9f5a3c 100644 --- a/crates/pumpkin/src/block/entities/mod.rs +++ b/crates/pumpkin/src/block/entities/mod.rs @@ -1,4 +1,3 @@ -use std::pin::Pin; use std::{any::Any, sync::Arc}; use pumpkin_data::{Block, block_properties::BLOCK_ENTITY_TYPES}; @@ -126,19 +125,10 @@ pub trait BlockEntity: Any + Send + Sync { None } fn set_block_state(&mut self, _block_state: BlockStateId) {} - fn on_block_replaced<'a>( - self: Arc, - world: Arc, - position: BlockPos, - ) -> Pin + Send + 'a>> - where - Self: 'a, - { - Box::pin(async move { - if let Some(inventory) = self.get_inventory() { - world.scatter_inventory(&position, &inventory); - } - }) + fn on_block_replaced(self: Arc, world: &Arc, position: &BlockPos) { + if let Some(inventory) = self.get_inventory() { + world.scatter_inventory(position, &inventory); + } } fn is_dirty(&self) -> bool { false diff --git a/crates/pumpkin/src/block/entities/shulker_box.rs b/crates/pumpkin/src/block/entities/shulker_box.rs index f951e6dcf..d527c1c49 100644 --- a/crates/pumpkin/src/block/entities/shulker_box.rs +++ b/crates/pumpkin/src/block/entities/shulker_box.rs @@ -3,8 +3,6 @@ use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::position::BlockPos; use std::any::Any; -use std::future::Future; -use std::pin::Pin; use std::sync::RwLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::{array::from_fn, sync::Arc}; @@ -63,17 +61,8 @@ impl BlockEntity for ShulkerBoxBlockEntity { .update_viewer_count::(self, world, &self.position); } - fn on_block_replaced<'a>( - self: Arc, - _world: Arc, - _position: BlockPos, - ) -> Pin + Send + 'a>> - where - Self: 'a, - { - Box::pin(async move { - // Do nothing - }) + fn on_block_replaced(self: Arc, _world: &Arc, _position: &BlockPos) { + // Shulker boxes retain items when broken } fn get_inventory(self: Arc) -> Option> { diff --git a/crates/pumpkin/src/command/commands/datapack.rs b/crates/pumpkin/src/command/commands/datapack.rs index 67f012cd7..92f341d4f 100644 --- a/crates/pumpkin/src/command/commands/datapack.rs +++ b/crates/pumpkin/src/command/commands/datapack.rs @@ -50,84 +50,16 @@ static ERROR_CREATE_IO_FAILURE: CommandErrorType<1> = CommandErrorType::new( translation::java::COMMANDS_DATAPACK_CREATE_IO_FAILURE, ); -fn get_all_known_packs(server: &Server) -> Vec { - let mut packs = Vec::new(); - packs.push("vanilla".to_string()); - - // Bundled feature packs - for bundled in [ - "trade_rebalance", - "minecart_improvements", - "redstone_experiments", - ] { - if !packs.iter().any(|p| p == bundled) { - packs.push(bundled.to_string()); - } - } - - // World datapacks directory - let datapacks_dir = server.basic_config.get_world_path().join("datapacks"); - if let Ok(entries) = fs::read_dir(datapacks_dir) { - for entry in entries.flatten() { - let path = entry.path(); - let file_name = entry.file_name().to_string_lossy().to_string(); - if file_name.starts_with('.') { - continue; - } - if path.is_dir() - || path - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("zip")) - { - let pack_name = format!("file/{file_name}"); - if !packs.iter().any(|p| p == &pack_name) { - packs.push(pack_name); - } - } - } - } - - let level_info = server.level_info.load(); - for pack in &level_info.data_packs.enabled { - if !packs.iter().any(|p| p == pack) { - packs.push(pack.clone()); - } - } - for pack in &level_info.data_packs.disabled { - if !packs.iter().any(|p| p == pack) { - packs.push(pack.clone()); - } - } - - packs -} - fn get_enabled_packs(server: &Server) -> Vec { - server.level_info.load().data_packs.enabled.clone() + crate::data::datapack::DatapackManager::get_enabled_packs(server) } fn get_available_packs(server: &Server) -> Vec { - let enabled = get_enabled_packs(server); - let all = get_all_known_packs(server); - all.into_iter().filter(|p| !enabled.contains(p)).collect() + crate::data::datapack::DatapackManager::get_available_packs(server) } fn find_pack_name(server: &Server, input: &str) -> Option { - let known = get_all_known_packs(server); - if let Some(p) = known.iter().find(|p| *p == input) { - return Some(p.clone()); - } - let file_input = format!("file/{input}"); - if let Some(p) = known.iter().find(|p| **p == file_input) { - return Some(p.clone()); - } - if let Some(p) = known - .iter() - .find(|p| p.strip_prefix("file/") == Some(input)) - { - return Some(p.clone()); - } - None + crate::data::datapack::DatapackManager::find_pack_name(server, input) } fn format_pack(name: &str) -> TextComponent { diff --git a/crates/pumpkin/src/command/commands/fetchprofile.rs b/crates/pumpkin/src/command/commands/fetchprofile.rs index 28a5bf43b..6bbb4bde5 100644 --- a/crates/pumpkin/src/command/commands/fetchprofile.rs +++ b/crates/pumpkin/src/command/commands/fetchprofile.rs @@ -267,28 +267,32 @@ struct ResolveNameExecutor; impl CommandExecutor for ResolveNameExecutor { fn execute(&self, context: &CommandContext) -> CommandExecutorResult { let name = StringArgumentType::get(context, ARG_NAME)?; - let server = context.server(); + let server = context.server().clone(); + let source = context.source.clone(); let name_owned = name.to_string(); let name_component = TextComponent::text(name_owned.clone()); - let result = futures::executor::block_on(fetch_profile_by_name_helper(server, &name_owned)); - match result { - Some(profile) => { - report_resolved_profile( - &context.source, - &profile, - translation::java::COMMANDS_FETCHPROFILE_NAME_SUCCESS, - name_component, - ); + let runtime = server.runtime.clone(); + runtime.spawn(async move { + let result = fetch_profile_by_name_helper(&server, &name_owned).await; + match result { + Some(profile) => { + report_resolved_profile( + &source, + &profile, + translation::java::COMMANDS_FETCHPROFILE_NAME_SUCCESS, + name_component, + ); + } + None => { + source.send_error(TextComponent::translate_cross( + translation::java::COMMANDS_FETCHPROFILE_NAME_FAILURE, + translation::java::COMMANDS_FETCHPROFILE_NAME_FAILURE, + [name_component], + )); + } } - None => { - context.source.send_error(TextComponent::translate_cross( - translation::java::COMMANDS_FETCHPROFILE_NAME_FAILURE, - translation::java::COMMANDS_FETCHPROFILE_NAME_FAILURE, - [name_component], - )); - } - } + }); Ok(1) } @@ -299,27 +303,31 @@ struct ResolveIdExecutor; impl CommandExecutor for ResolveIdExecutor { fn execute(&self, context: &CommandContext) -> CommandExecutorResult { let id = UuidArgumentType::get(context, ARG_ID)?; - let server = context.server(); + let server = context.server().clone(); + let source = context.source.clone(); let id_component = TextComponent::text(id.to_string()); - let result = futures::executor::block_on(fetch_profile_by_id_helper(server, id)); - match result { - Some(profile) => { - report_resolved_profile( - &context.source, - &profile, - translation::java::COMMANDS_FETCHPROFILE_ID_SUCCESS, - id_component, - ); + let runtime = server.runtime.clone(); + runtime.spawn(async move { + let result = fetch_profile_by_id_helper(&server, id).await; + match result { + Some(profile) => { + report_resolved_profile( + &source, + &profile, + translation::java::COMMANDS_FETCHPROFILE_ID_SUCCESS, + id_component, + ); + } + None => { + source.send_error(TextComponent::translate_cross( + translation::java::COMMANDS_FETCHPROFILE_ID_FAILURE, + translation::java::COMMANDS_FETCHPROFILE_ID_FAILURE, + [id_component], + )); + } } - None => { - context.source.send_error(TextComponent::translate_cross( - translation::java::COMMANDS_FETCHPROFILE_ID_FAILURE, - translation::java::COMMANDS_FETCHPROFILE_ID_FAILURE, - [id_component], - )); - } - } + }); Ok(1) } diff --git a/crates/pumpkin/src/command/commands/fillbiome.rs b/crates/pumpkin/src/command/commands/fillbiome.rs index b76633638..a55742604 100644 --- a/crates/pumpkin/src/command/commands/fillbiome.rs +++ b/crates/pumpkin/src/command/commands/fillbiome.rs @@ -30,7 +30,6 @@ struct FillBiomeExecutor { } impl CommandExecutor for FillBiomeExecutor { - #[expect(clippy::too_many_lines)] fn execute(&self, context: &CommandContext) -> CommandExecutorResult { let from_pos = BlockPosArgumentType::get_block_pos(context, "from")?; let to_pos = BlockPosArgumentType::get_block_pos(context, "to")?; @@ -96,51 +95,30 @@ impl CommandExecutor for FillBiomeExecutor { let mut changed_count = 0; for (chunk_pos, mods) in chunk_modifications { - let (has_replaced, count) = - futures::executor::block_on(world.level.get_or_fetch_chunk(chunk_pos, |chunk| { - let mut local_count = 0; - let mut modified = false; - for &(rel_x, rel_y, rel_z) in &mods { - let section_index = rel_y / 4; - let scale_y = rel_y % 4; - if let Some(current_id) = - chunk - .section - .get_noise_biome(section_index, rel_x, scale_y, rel_z) - { - if let Some(replace_id) = replace_biome_id { - if current_id == replace_id { - chunk.section.set_relative_biome( - rel_x, - rel_y, - rel_z, - target_biome_id, - ); - local_count += 1; - modified = true; - } - } else { - chunk.section.set_relative_biome( - rel_x, - rel_y, - rel_z, - target_biome_id, - ); - local_count += 1; - modified = true; - } - } + let result = world.level.read_chunk_sync(&chunk_pos, |chunk| { + let mut local_count = 0; + let mut modified = false; + for &(rel_x, rel_y, rel_z) in &mods { + let section_index = rel_y / 4; + let scale_y = rel_y % 4; + if let Some(current_id) = + chunk + .section + .get_noise_biome(section_index, rel_x, scale_y, rel_z) + && replace_biome_id.is_none_or(|rep| current_id == rep) + { + chunk + .section + .set_relative_biome(rel_x, rel_y, rel_z, target_biome_id); + local_count += 1; + modified = true; } - (modified, local_count) - })); + } + (local_count, modified.then(|| chunk.clone())) + }); - if has_replaced { + if let Some((count, Some(chunk))) = result { changed_count += count; - let chunk = futures::executor::block_on( - world - .level - .get_or_fetch_chunk(chunk_pos, std::clone::Clone::clone), - ); world.broadcast_to_chunk_except(chunk_pos, &[], &CChunkData(&chunk)); } } diff --git a/crates/pumpkin/src/command/commands/plugin.rs b/crates/pumpkin/src/command/commands/plugin.rs index 05f20763e..4b4de6b36 100644 --- a/crates/pumpkin/src/command/commands/plugin.rs +++ b/crates/pumpkin/src/command/commands/plugin.rs @@ -34,9 +34,8 @@ impl CommandExecutor for ListExecutor { ))); }; - let plugins = futures::executor::block_on(server_arc.plugin_manager.active_plugins()); - let loaded_plugins = - futures::executor::block_on(server_arc.plugin_manager.loaded_plugins()); + let plugins = server_arc.plugin_manager.active_plugins(); + let loaded_plugins = server_arc.plugin_manager.loaded_plugins(); let mut message = TextComponent::text(format!("Plugins ({}):", loaded_plugins.len())) .color_named(NamedColor::Gold) @@ -59,11 +58,18 @@ impl CommandExecutor for ListExecutor { metadata.authors.join(", "), metadata.description ); - let component = TextComponent::text(line) + let mut plugin_component = TextComponent::text(line) .color_named(NamedColor::Green) .hover_event(HoverEvent::show_text(TextComponent::text(hover_text))); - message = message.add_child(component); + if !metadata.permissions.is_empty() { + plugin_component = plugin_component.add_child( + TextComponent::text(format!(" (Permissions: {:?})", metadata.permissions)) + .color_named(NamedColor::Gray), + ); + } + + message = message.add_child(plugin_component); } sender.send_message(message); @@ -95,32 +101,38 @@ impl CommandExecutor for LoadExecutor { }; let plugin_name = plugin_name.to_string(); - if futures::executor::block_on(server_arc.plugin_manager.is_plugin_active(&plugin_name)) { + if server_arc.plugin_manager.is_plugin_active(&plugin_name) { sender.send_message(TextComponent::text(format!( "Plugin {plugin_name} is already loaded" ))); return Ok(1); } - let result = futures::executor::block_on( - server_arc + let sender_clone = sender.clone(); + let plugin_name_clone = plugin_name; + let server_clone = server_arc.clone(); + server_arc.runtime.spawn(async move { + let result = server_clone .plugin_manager - .try_load_plugin(&server_arc, Path::new(&plugin_name)), - ); + .try_load_plugin(&server_clone, Path::new(&plugin_name_clone)) + .await; - match result { - Ok(()) => { - sender.send_message( - TextComponent::text(format!("Plugin {plugin_name} loaded successfully")) + match result { + Ok(()) => { + sender_clone.send_message( + TextComponent::text(format!( + "Plugin {plugin_name_clone} loaded successfully" + )) .color_named(NamedColor::Green), - ); + ); + } + Err(e) => { + sender_clone.send_message(TextComponent::text(format!( + "Failed to load plugin {plugin_name_clone}: {e}" + ))); + } } - Err(e) => { - sender.send_message(TextComponent::text(format!( - "Failed to load plugin {plugin_name}: {e}" - ))); - } - } + }); Ok(1) } @@ -149,29 +161,38 @@ impl CommandExecutor for UnloadExecutor { }; let plugin_name = plugin_name.to_string(); - if !futures::executor::block_on(server_arc.plugin_manager.is_plugin_active(&plugin_name)) { + if !server_arc.plugin_manager.is_plugin_active(&plugin_name) { sender.send_message(TextComponent::text(format!( "Plugin {plugin_name} is not loaded" ))); return Ok(1); } - let result = - futures::executor::block_on(server_arc.plugin_manager.unload_plugin(&plugin_name)); + let sender_clone = sender.clone(); + let plugin_name_clone = plugin_name; + let server_clone = server_arc.clone(); + server_arc.runtime.spawn(async move { + let result = server_clone + .plugin_manager + .unload_plugin(&plugin_name_clone) + .await; - match result { - Ok(()) => { - sender.send_message( - TextComponent::text(format!("Plugin {plugin_name} unloaded successfully")) + match result { + Ok(()) => { + sender_clone.send_message( + TextComponent::text(format!( + "Plugin {plugin_name_clone} unloaded successfully" + )) .color_named(NamedColor::Green), - ); + ); + } + Err(e) => { + sender_clone.send_message(TextComponent::text(format!( + "Failed to unload plugin {plugin_name_clone}: {e}" + ))); + } } - Err(e) => { - sender.send_message(TextComponent::text(format!( - "Failed to unload plugin {plugin_name}: {e}" - ))); - } - } + }); Ok(1) } @@ -197,32 +218,36 @@ impl CommandExecutor for HotReloadExecutor { ))); }; + let sender_clone = sender.clone(); + let server_clone = server_arc.clone(); if enabled { - if let Err(e) = - futures::executor::block_on(server_arc.plugin_manager.start_watcher(&server_arc)) - { - sender.send_message(TextComponent::text(format!( - "Failed to start plugin watcher: {e}" - ))); - return Ok(1); - } + server_arc.runtime.spawn(async move { + if let Err(e) = server_clone.plugin_manager.start_watcher(&server_clone).await { + sender_clone.send_message(TextComponent::text(format!( + "Failed to start plugin watcher: {e}" + ))); + return; + } - sender.send_message( - TextComponent::text("Hot reloading has been enabled.") - .color_named(NamedColor::Green), - ); - sender.send_message( - TextComponent::text( - "WARNING: Hot reloading can impact performance and should only be enabled during plugin development.", - ) - .color_named(NamedColor::Red), - ); + sender_clone.send_message( + TextComponent::text("Hot reloading has been enabled.") + .color_named(NamedColor::Green), + ); + sender_clone.send_message( + TextComponent::text( + "WARNING: Hot reloading can impact performance and should only be enabled during plugin development.", + ) + .color_named(NamedColor::Red), + ); + }); } else { - futures::executor::block_on(server_arc.plugin_manager.stop_watcher()); - sender.send_message( - TextComponent::text("Hot reloading has been disabled.") - .color_named(NamedColor::Yellow), - ); + server_arc.runtime.spawn(async move { + server_clone.plugin_manager.stop_watcher().await; + sender_clone.send_message( + TextComponent::text("Hot reloading has been disabled.") + .color_named(NamedColor::Yellow), + ); + }); } Ok(1) diff --git a/crates/pumpkin/src/command/commands/plugins.rs b/crates/pumpkin/src/command/commands/plugins.rs index 081d8bbe7..8f431df8c 100644 --- a/crates/pumpkin/src/command/commands/plugins.rs +++ b/crates/pumpkin/src/command/commands/plugins.rs @@ -28,7 +28,7 @@ impl CommandExecutor for PluginsExecutor { ))); }; - let plugins = futures::executor::block_on(server_arc.plugin_manager.active_plugins()); + let plugins = server_arc.plugin_manager.active_plugins(); let message_text = if plugins.is_empty() { TextComponent::text("No plugins are loaded on the server.").color_named(NamedColor::Red) diff --git a/crates/pumpkin/src/command/commands/ride.rs b/crates/pumpkin/src/command/commands/ride.rs index 9d139beb3..afae76106 100644 --- a/crates/pumpkin/src/command/commands/ride.rs +++ b/crates/pumpkin/src/command/commands/ride.rs @@ -108,11 +108,9 @@ impl CommandExecutor for RideMountExecutor { continue; } // Dismount first - futures::executor::block_on( - curr_veh - .get_entity() - .remove_passenger(target.get_entity().entity_id), - ); + curr_veh + .get_entity() + .remove_passenger_sync(target.get_entity().entity_id); } vehicle @@ -160,7 +158,7 @@ impl CommandExecutor for RideDismountExecutor { .clone(); if let Some(vehicle) = current_vehicle { let target_id = target.get_entity().entity_id; - futures::executor::block_on(vehicle.get_entity().remove_passenger(target_id)); + vehicle.get_entity().remove_passenger_sync(target_id); success_count += 1; let msg = TextComponent::translate_cross( diff --git a/crates/pumpkin/src/command/commands/rotate.rs b/crates/pumpkin/src/command/commands/rotate.rs index ee5645a68..3c659ee3b 100644 --- a/crates/pumpkin/src/command/commands/rotate.rs +++ b/crates/pumpkin/src/command/commands/rotate.rs @@ -53,7 +53,7 @@ fn get_anchor_position(entity: &crate::entity::Entity, anchor: EntityAnchor) -> /// If relative flags are true, the values are added to current rotation. /// If relative flags are false, the values are absolute. fn rotate_entity( - target: std::sync::Arc, + target: &std::sync::Arc, yaw: f32, is_yaw_relative: bool, pitch: f32, @@ -80,7 +80,14 @@ fn rotate_entity( // This properly handles both players (sends CPlayerPosition) and other entities let pos = entity.pos.load(); let world = entity.world.load_full(); - futures::executor::block_on(target.teleport(pos, Some(final_yaw), Some(final_pitch), world)); + if let Some(player) = target.get_player() { + let fut = target + .clone() + .teleport(pos, Some(final_yaw), Some(final_pitch), world); + player.spawn_task(fut); + } else { + entity.teleport(pos, Some(final_yaw), Some(final_pitch), world); + } } /// Sends success message for the rotate command. @@ -107,7 +114,7 @@ impl CommandExecutor for RotateToRotationExecutor { let (yaw, yaw_rel, pitch, pitch_rel) = RotationArgumentConsumer::find_arg(args, ARG_ROTATION)?; - rotate_entity(target.clone(), yaw, yaw_rel, pitch, pitch_rel); + rotate_entity(&target, yaw, yaw_rel, pitch, pitch_rel); send_success_message(sender, target.as_ref()); Ok(1) @@ -135,7 +142,7 @@ impl CommandExecutor for RotateFacingLocationExecutor { let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &facing_pos); // Facing uses absolute rotation - rotate_entity(target.clone(), yaw, false, pitch, false); + rotate_entity(&target, yaw, false, pitch, false); send_success_message(sender, target.as_ref()); Ok(1) @@ -165,7 +172,7 @@ impl CommandExecutor for RotateFacingEntityExecutor { let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &looking_towards); // Facing uses absolute rotation - rotate_entity(target.clone(), yaw, false, pitch, false); + rotate_entity(&target, yaw, false, pitch, false); send_success_message(sender, target.as_ref()); Ok(1) @@ -196,7 +203,7 @@ impl CommandExecutor for RotateFacingEntityNoAnchorExecutor { let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &looking_towards); // Facing uses absolute rotation - rotate_entity(target.clone(), yaw, false, pitch, false); + rotate_entity(&target, yaw, false, pitch, false); send_success_message(sender, target.as_ref()); Ok(1) diff --git a/crates/pumpkin/src/command/commands/saveall.rs b/crates/pumpkin/src/command/commands/saveall.rs index 67278bd0d..d1d8aaf7f 100644 --- a/crates/pumpkin/src/command/commands/saveall.rs +++ b/crates/pumpkin/src/command/commands/saveall.rs @@ -26,24 +26,28 @@ impl CommandExecutor for SaveAllExecutor { false, ); - let server = context.server(); - if let Err(err) = futures::executor::block_on(server.save_all()) { - error!("Failed to save server data: {err}"); - context.source.send_error(TextComponent::translate_cross( - translation::java::COMMANDS_SAVE_FAILED, - translation::bedrock::COMMANDS_SAVE_FAILED, - [], - )); - } else { - context.source.send_feedback( - TextComponent::translate_cross( - translation::java::COMMANDS_SAVE_SUCCESS, - translation::bedrock::COMMANDS_SAVE_SUCCESS, + let server_arc = context.server().clone(); + let source = context.source.clone(); + let runtime = server_arc.runtime.clone(); + runtime.spawn(async move { + if let Err(err) = server_arc.save_all().await { + error!("Failed to save server data: {err}"); + source.send_error(TextComponent::translate_cross( + translation::java::COMMANDS_SAVE_FAILED, + translation::bedrock::COMMANDS_SAVE_FAILED, [], - ), - true, - ); - } + )); + } else { + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SAVE_SUCCESS, + translation::bedrock::COMMANDS_SAVE_SUCCESS, + [], + ), + true, + ); + } + }); Ok(1) } diff --git a/crates/pumpkin/src/command/commands/spectate.rs b/crates/pumpkin/src/command/commands/spectate.rs index 071ac5cb9..33e0003fc 100644 --- a/crates/pumpkin/src/command/commands/spectate.rs +++ b/crates/pumpkin/src/command/commands/spectate.rs @@ -105,7 +105,11 @@ impl CommandExecutor for SpectateTargetSelfExecutor { let yaw = target_entity.yaw.load(); let pitch = target_entity.pitch.load(); player.try_send_client_packet(&CSetCamera::new(target_id.into())); - futures::executor::block_on(player.teleport(pos, Some(yaw), Some(pitch), player_world)); + player.spawn_task( + player + .clone() + .teleport(pos, Some(yaw), Some(pitch), player_world), + ); let target_name = target.get_display_name(); sender.send_message(TextComponent::translate_cross( @@ -174,12 +178,11 @@ impl CommandExecutor for SpectateTargetOtherExecutor { let pitch = target_entity.pitch.load(); let player_world = player.world(); player.try_send_client_packet(&CSetCamera::new(target_id.into())); - futures::executor::block_on(player.clone().teleport( - pos, - Some(yaw), - Some(pitch), - player_world, - )); + player.spawn_task( + player + .clone() + .teleport(pos, Some(yaw), Some(pitch), player_world), + ); succeeded += 1; } diff --git a/crates/pumpkin/src/command/commands/spreadplayers.rs b/crates/pumpkin/src/command/commands/spreadplayers.rs index 7133cae29..821136d22 100644 --- a/crates/pumpkin/src/command/commands/spreadplayers.rs +++ b/crates/pumpkin/src/command/commands/spreadplayers.rs @@ -269,7 +269,7 @@ impl CommandExecutor for SpreadPlayersExecutor { for (index, target) in targets.iter().enumerate() { let pile = piles[index % pile_count]; let y = surface_ys[index % pile_count]; - futures::executor::block_on(target.clone().teleport( + context.server().runtime.spawn(target.clone().teleport( Vector3::new(pile.x.floor() + 0.5, f64::from(y), pile.z.floor() + 0.5), None, None, diff --git a/crates/pumpkin/src/command/commands/teleport.rs b/crates/pumpkin/src/command/commands/teleport.rs index a45793d99..2f093e0d3 100644 --- a/crates/pumpkin/src/command/commands/teleport.rs +++ b/crates/pumpkin/src/command/commands/teleport.rs @@ -82,7 +82,7 @@ impl CommandExecutor for EntitiesToEntityExecutor { fn execute( &self, sender: &CommandSender, - _server: &crate::server::Server, + server: &crate::server::Server, args: &ConsumedArgs, ) -> CommandResult { let targets = EntitiesArgumentConsumer::find_arg(args, ARG_TARGETS)?; @@ -101,7 +101,7 @@ impl CommandExecutor for EntitiesToEntityExecutor { ))); } for target in targets { - futures::executor::block_on(target.clone().teleport( + server.runtime.spawn(target.clone().teleport( pos, Some(yaw), Some(pitch), @@ -148,7 +148,7 @@ impl CommandExecutor for EntitiesToPosFacingPosExecutor { let world = resolve_sender_world(sender, server)?; for target in targets { - futures::executor::block_on(target.clone().teleport( + server.runtime.spawn(target.clone().teleport( pos, Some(yaw), Some(pitch), @@ -200,7 +200,7 @@ impl CommandExecutor for EntitiesToPosFacingEntityExecutor { let world = resolve_sender_world(sender, server)?; for target in targets { - futures::executor::block_on(target.clone().teleport( + server.runtime.spawn(target.clone().teleport( pos, Some(yaw), Some(pitch), @@ -253,7 +253,7 @@ impl CommandExecutor for EntitiesToPosWithRotationExecutor { let world = resolve_sender_world(sender, server)?; for target in targets { - futures::executor::block_on(target.clone().teleport( + server.runtime.spawn(target.clone().teleport( pos, Some(yaw), Some(pitch), @@ -304,7 +304,7 @@ impl CommandExecutor for EntitiesToPosExecutor { for target in targets { let yaw = target.get_entity().yaw.load(); let pitch = target.get_entity().pitch.load(); - futures::executor::block_on(target.clone().teleport( + server.runtime.spawn(target.clone().teleport( pos, Some(yaw), Some(pitch), @@ -338,7 +338,7 @@ impl CommandExecutor for SelfToEntityExecutor { fn execute( &self, sender: &CommandSender, - _server: &crate::server::Server, + server: &crate::server::Server, args: &ConsumedArgs, ) -> CommandResult { let destination = EntityArgumentConsumer::find_arg(args, ARG_DESTINATION)?; @@ -357,12 +357,9 @@ impl CommandExecutor for SelfToEntityExecutor { [], ))); } - futures::executor::block_on(player.clone().teleport( - pos, - Some(yaw), - Some(pitch), - world, - )); + server + .runtime + .spawn(player.clone().teleport(pos, Some(yaw), Some(pitch), world)); sender.send_message(TextComponent::translate_cross( translation::java::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE, @@ -390,7 +387,7 @@ impl CommandExecutor for SelfToPosExecutor { fn execute( &self, sender: &CommandSender, - _server: &crate::server::Server, + server: &crate::server::Server, args: &ConsumedArgs, ) -> CommandResult { match sender { @@ -406,7 +403,7 @@ impl CommandExecutor for SelfToPosExecutor { ))); } let player_world = player.world(); - futures::executor::block_on(player.clone().teleport( + server.runtime.spawn(player.clone().teleport( pos, Some(yaw), Some(pitch), diff --git a/crates/pumpkin/src/data/datapack/mod.rs b/crates/pumpkin/src/data/datapack/mod.rs index 71b974780..b4e7b2a21 100644 --- a/crates/pumpkin/src/data/datapack/mod.rs +++ b/crates/pumpkin/src/data/datapack/mod.rs @@ -24,6 +24,25 @@ pub struct LoadedDatapack { pub function_count: usize, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DatapackInfo { + pub id: String, + pub name: String, + pub description: String, + pub pack_format: u32, + pub is_enabled: bool, + pub recipe_count: usize, + pub function_count: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DatapackEnablePosition { + First, + Last, + Before(String), + After(String), +} + pub struct DatapackManager { loaded_packs: RwLock>, functions: RwLock>>, @@ -244,9 +263,314 @@ impl DatapackManager { Ok(total_executed) } + + pub fn get_all_known_packs(server: &Server) -> Vec { + let mut packs = Vec::new(); + packs.push("vanilla".to_string()); + + // Bundled feature packs + for bundled in [ + "trade_rebalance", + "minecart_improvements", + "redstone_experiments", + ] { + if !packs.iter().any(|p| p == bundled) { + packs.push(bundled.to_string()); + } + } + + // World datapacks directory + let datapacks_dir = server.basic_config.get_world_path().join("datapacks"); + if let Ok(entries) = fs::read_dir(datapacks_dir) { + for entry in entries.flatten() { + let path = entry.path(); + let file_name = entry.file_name().to_string_lossy().to_string(); + if file_name.starts_with('.') { + continue; + } + if path.is_dir() + || path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("zip")) + { + let pack_name = format!("file/{file_name}"); + if !packs.iter().any(|p| p == &pack_name) { + packs.push(pack_name); + } + } + } + } + + let level_info = server.level_info.load(); + for pack in &level_info.data_packs.enabled { + if !packs.iter().any(|p| p == pack) { + packs.push(pack.clone()); + } + } + for pack in &level_info.data_packs.disabled { + if !packs.iter().any(|p| p == pack) { + packs.push(pack.clone()); + } + } + + packs + } + + pub fn get_enabled_packs(server: &Server) -> Vec { + server.level_info.load().data_packs.enabled.clone() + } + + pub fn get_available_packs(server: &Server) -> Vec { + let enabled = Self::get_enabled_packs(server); + let all = Self::get_all_known_packs(server); + all.into_iter().filter(|p| !enabled.contains(p)).collect() + } + + pub fn find_pack_name(server: &Server, input: &str) -> Option { + let known = Self::get_all_known_packs(server); + if let Some(p) = known.iter().find(|p| *p == input) { + return Some(p.clone()); + } + let file_input = format!("file/{input}"); + if let Some(p) = known.iter().find(|p| **p == file_input) { + return Some(p.clone()); + } + if let Some(p) = known + .iter() + .find(|p| p.strip_prefix("file/") == Some(input)) + { + return Some(p.clone()); + } + None + } + + pub fn get_pack_info(server: &Server, name_or_id: &str) -> Option { + let resolved_name = Self::find_pack_name(server, name_or_id)?; + let enabled_packs = Self::get_enabled_packs(server); + let is_enabled = enabled_packs.contains(&resolved_name); + + let loaded = server.datapack_manager.get_loaded_packs(); + if let Some(pack) = loaded + .iter() + .find(|p| p.id == resolved_name || p.name == resolved_name) + { + return Some(DatapackInfo { + id: pack.id.clone(), + name: pack.name.clone(), + description: pack.description.clone(), + pack_format: pack.pack_format, + is_enabled, + recipe_count: pack.recipe_count, + function_count: pack.function_count, + }); + } + + let (id, name, description, pack_format) = if resolved_name == "vanilla" { + ( + "vanilla".to_string(), + "vanilla".to_string(), + "The default data pack".to_string(), + 61, + ) + } else if let Some(stripped) = resolved_name.strip_prefix("file/") { + let pack_path = server + .basic_config + .get_world_path() + .join("datapacks") + .join(stripped); + let (desc, format) = read_pack_mcmeta(&pack_path); + (resolved_name.clone(), stripped.to_string(), desc, format) + } else { + ( + resolved_name.clone(), + resolved_name.clone(), + format!("Bundled datapack: {resolved_name}"), + 61, + ) + }; + + Some(DatapackInfo { + id, + name, + description, + pack_format, + is_enabled, + recipe_count: 0, + function_count: 0, + }) + } + + pub fn list_all_packs(server: &Server) -> Vec { + let all = Self::get_all_known_packs(server); + all.into_iter() + .filter_map(|p| Self::get_pack_info(server, &p)) + .collect() + } + + pub fn list_enabled_packs(server: &Server) -> Vec { + let enabled = Self::get_enabled_packs(server); + enabled + .into_iter() + .filter_map(|p| Self::get_pack_info(server, &p)) + .collect() + } + + pub fn list_available_packs(server: &Server) -> Vec { + let available = Self::get_available_packs(server); + available + .into_iter() + .filter_map(|p| Self::get_pack_info(server, &p)) + .collect() + } + + pub fn is_pack_enabled(server: &Server, name: &str) -> bool { + let Some(resolved) = Self::find_pack_name(server, name) else { + return false; + }; + Self::get_enabled_packs(server).contains(&resolved) + } + + pub fn enable_pack( + server: &Arc, + name: &str, + position: DatapackEnablePosition, + ) -> Result<(), String> { + let Some(resolved_name) = Self::find_pack_name(server, name) else { + return Err(format!("Unknown datapack '{name}'")); + }; + + let enabled = Self::get_enabled_packs(server); + if enabled.contains(&resolved_name) { + return Err(format!("Datapack '{resolved_name}' is already enabled")); + } + + let target = resolved_name; + match position { + DatapackEnablePosition::First => { + server.level_info.rcu(|level_info| { + let mut new_info = (**level_info).clone(); + new_info.data_packs.disabled.retain(|p| p != &target); + new_info.data_packs.enabled.retain(|p| p != &target); + new_info.data_packs.enabled.insert(0, target.clone()); + new_info + }); + } + DatapackEnablePosition::Last => { + server.level_info.rcu(|level_info| { + let mut new_info = (**level_info).clone(); + new_info.data_packs.disabled.retain(|p| p != &target); + new_info.data_packs.enabled.retain(|p| p != &target); + new_info.data_packs.enabled.push(target.clone()); + new_info + }); + } + DatapackEnablePosition::Before(existing_name) => { + let Some(existing_pack) = Self::find_pack_name(server, &existing_name) else { + return Err(format!("Unknown existing datapack '{existing_name}'")); + }; + if !enabled.contains(&existing_pack) { + return Err(format!("Datapack '{existing_pack}' is not enabled")); + } + server.level_info.rcu(|level_info| { + let mut new_info = (**level_info).clone(); + new_info.data_packs.disabled.retain(|p| p != &target); + new_info.data_packs.enabled.retain(|p| p != &target); + if let Some(idx) = new_info + .data_packs + .enabled + .iter() + .position(|p| p == &existing_pack) + { + new_info.data_packs.enabled.insert(idx, target.clone()); + } else { + new_info.data_packs.enabled.push(target.clone()); + } + new_info + }); + } + DatapackEnablePosition::After(existing_name) => { + let Some(existing_pack) = Self::find_pack_name(server, &existing_name) else { + return Err(format!("Unknown existing datapack '{existing_name}'")); + }; + if !enabled.contains(&existing_pack) { + return Err(format!("Datapack '{existing_pack}' is not enabled")); + } + server.level_info.rcu(|level_info| { + let mut new_info = (**level_info).clone(); + new_info.data_packs.disabled.retain(|p| p != &target); + new_info.data_packs.enabled.retain(|p| p != &target); + if let Some(idx) = new_info + .data_packs + .enabled + .iter() + .position(|p| p == &existing_pack) + { + new_info.data_packs.enabled.insert(idx + 1, target.clone()); + } else { + new_info.data_packs.enabled.push(target.clone()); + } + new_info + }); + } + } + + if let Err(err) = server.save_world_info() { + tracing::error!("Failed to save world info: {err}"); + } + + server.reload_datapacks(server); + Ok(()) + } + + pub fn disable_pack(server: &Arc, name: &str) -> Result<(), String> { + let Some(target_pack) = Self::find_pack_name(server, name) else { + return Err(format!("Unknown datapack '{name}'")); + }; + + let enabled = Self::get_enabled_packs(server); + if !enabled.contains(&target_pack) { + return Err(format!("Datapack '{target_pack}' is not enabled")); + } + + if target_pack == "vanilla" { + return Err("Cannot disable the default vanilla datapack".to_string()); + } + + let target = target_pack; + server.level_info.rcu(|level_info| { + let mut new_info = (**level_info).clone(); + new_info.data_packs.enabled.retain(|p| p != &target); + if !new_info.data_packs.disabled.contains(&target) { + new_info.data_packs.disabled.push(target.clone()); + } + new_info + }); + + if let Err(err) = server.save_world_info() { + tracing::error!("Failed to save world info: {err}"); + } + + server.reload_datapacks(server); + Ok(()) + } + + pub fn reload(server: &Arc) -> Result<(), String> { + server.reload_datapacks(server); + Ok(()) + } + + pub fn execute_function_from_console( + server: &Arc, + name: &str, + ) -> Result { + let source = crate::command::CommandSender::Console.into_source(server); + server + .datapack_manager + .execute_function(server, &source, name) + } } -fn read_pack_mcmeta(pack_path: &Path) -> (String, u32) { +pub fn read_pack_mcmeta(pack_path: &Path) -> (String, u32) { let mcmeta_path = pack_path.join("pack.mcmeta"); if let Ok(content) = fs::read_to_string(mcmeta_path) && let Ok(val) = serde_json::from_str::(&content) diff --git a/crates/pumpkin/src/data/player_server.rs b/crates/pumpkin/src/data/player_server.rs index 70aea5837..4602515d3 100644 --- a/crates/pumpkin/src/data/player_server.rs +++ b/crates/pumpkin/src/data/player_server.rs @@ -94,7 +94,7 @@ impl ServerPlayerData { } let storage = self.storage.clone(); - server.runtime.spawn(async move { + tokio::task::spawn_blocking(move || { for (uuid, nbt) in snapshots { if let Err(e) = storage.save_player_data(&uuid, nbt) { error!("Failed to save player data for {uuid}: {e}"); diff --git a/crates/pumpkin/src/entity/mob/creeper.rs b/crates/pumpkin/src/entity/mob/creeper.rs index 752dae5d5..5a739a0b4 100644 --- a/crates/pumpkin/src/entity/mob/creeper.rs +++ b/crates/pumpkin/src/entity/mob/creeper.rs @@ -247,3 +247,53 @@ impl Mob for CreeperEntity { true } } + +impl CreeperEntity { + pub fn is_charged(&self) -> bool { + self.charged.load(Ordering::Relaxed) + } + + pub fn set_charged(&self, charged: bool) { + self.charged.store(charged, Ordering::Relaxed); + let entity = &self.mob_entity.living_entity.entity; + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::creeper::CHARGED, + charged, + )], + None, + ); + } + + pub fn is_ignited(&self) -> bool { + self.ignited.load(Ordering::Relaxed) + } + + pub fn set_ignited(&self, ignited: bool) { + self.ignited.store(ignited, Ordering::Relaxed); + let entity = &self.mob_entity.living_entity.entity; + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::creeper::IS_IGNITED, + ignited, + )], + None, + ); + } + + pub fn get_fuse(&self) -> i32 { + self.fuse_time.load(Ordering::Relaxed) + } + + pub fn set_fuse(&self, fuse: i32) { + self.fuse_time.store(fuse, Ordering::Relaxed); + } + + pub fn get_explosion_radius(&self) -> i32 { + self.explosion_radius.load(Ordering::Relaxed) + } + + pub fn set_explosion_radius(&self, radius: i32) { + self.explosion_radius.store(radius, Ordering::Relaxed); + } +} diff --git a/crates/pumpkin/src/entity/mob/evoker.rs b/crates/pumpkin/src/entity/mob/evoker.rs index fac168bcf..1ff9f2a0d 100644 --- a/crates/pumpkin/src/entity/mob/evoker.rs +++ b/crates/pumpkin/src/entity/mob/evoker.rs @@ -535,8 +535,9 @@ impl Goal for EvokerWololoSpellGoal { for cand in candidates { if *cand.get_entity().entity_type == EntityType::SHEEP - && let Some(mob) = cand.get_mob() - && let Some(sheep) = mob.get_sheep() + && let Some(sheep) = cand + .cast_any() + .downcast_ref::() { // Blue color is 11 in Minecraft if sheep.get_color() == 11 { @@ -609,8 +610,9 @@ impl Goal for EvokerWololoSpellGoal { for cand in candidates { if cand.get_entity().entity_id == target_id - && let Some(mob) = cand.get_mob() - && let Some(sheep) = mob.get_sheep() + && let Some(sheep) = cand + .cast_any() + .downcast_ref::() { // Convert color to Red (14) sheep.set_color(14); diff --git a/crates/pumpkin/src/entity/mob/mod.rs b/crates/pumpkin/src/entity/mob/mod.rs index 9a559c939..195399577 100644 --- a/crates/pumpkin/src/entity/mob/mod.rs +++ b/crates/pumpkin/src/entity/mob/mod.rs @@ -957,10 +957,6 @@ pub trait Mob: EntityBase + Send + Sync { fn mob_set_variant_name(&self, _name: &str) {} - fn get_sheep(&self) -> Option<&crate::entity::passive::sheep::SheepEntity> { - None - } - fn mob_on_lightning_strike( &self, caller: &dyn EntityBase, diff --git a/crates/pumpkin/src/entity/mob/shulker.rs b/crates/pumpkin/src/entity/mob/shulker.rs index 05a8ae2a1..579e053e2 100644 --- a/crates/pumpkin/src/entity/mob/shulker.rs +++ b/crates/pumpkin/src/entity/mob/shulker.rs @@ -113,7 +113,7 @@ impl ShulkerEntity { .unwrap_or(DEFAULT_ATTACH_FACE) } - fn set_attach_face(&self, face: BlockDirection) { + pub fn set_attach_face(&self, face: BlockDirection) { self.attach_face.store(face as u8, Ordering::Relaxed); let entity = &self.mob_entity.living_entity.entity; entity.send_meta_data( @@ -125,6 +125,24 @@ impl ShulkerEntity { ); } + pub fn get_color(&self) -> Option { + let c = self.color.load(Ordering::Relaxed); + if c == NO_COLOR { None } else { Some(c) } + } + + pub fn set_color(&self, color: Option) { + let val = color.unwrap_or(NO_COLOR); + self.color.store(val, Ordering::Relaxed); + let entity = &self.mob_entity.living_entity.entity; + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::shulker::COLOR, + val as i8, + )], + None, + ); + } + pub fn get_raw_peek(&self) -> u8 { self.peek_amount.load(Ordering::Relaxed) } diff --git a/crates/pumpkin/src/entity/mob/zombie/zombie.rs b/crates/pumpkin/src/entity/mob/zombie/zombie.rs index 412038a54..721d59fe3 100644 --- a/crates/pumpkin/src/entity/mob/zombie/zombie.rs +++ b/crates/pumpkin/src/entity/mob/zombie/zombie.rs @@ -52,3 +52,46 @@ impl Mob for ZombieEntity { self.entity.mob_read_nbt(nbt); } } + +impl ZombieEntity { + #[must_use] + pub fn can_break_doors(&self) -> bool { + self.entity + .can_break_doors + .load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn set_can_break_doors(&self, can_break: bool) { + self.entity + .can_break_doors + .store(can_break, std::sync::atomic::Ordering::Relaxed); + } + + #[must_use] + pub fn is_baby(&self) -> bool { + self.entity + .mob_entity + .living_entity + .entity + .age + .load(std::sync::atomic::Ordering::Relaxed) + < 0 + } + + pub fn set_baby(&self, baby: bool) { + let age = if baby { -24000 } else { 0 }; + self.entity + .mob_entity + .living_entity + .entity + .age + .store(age, std::sync::atomic::Ordering::Relaxed); + self.entity.mob_entity.living_entity.entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( + pumpkin_data::tracked_data::zombie::BABY, + baby, + )], + None, + ); + } +} diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 218a9a7ef..06406c226 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -2351,29 +2351,18 @@ impl Entity { tokio::spawn(async move { let world_for_dest = world_clone.clone(); let caller_for_dest = caller_clone.clone(); - let gen_pool = world_for_dest.level.gen_pool.clone(); - let transition = if let Some(pool) = gen_pool { - let (tx, rx) = tokio::sync::oneshot::channel(); - pool.spawn(move || { - let dest = portal_type.get_portal_destination( - &world_for_dest, - dest_world_opt, - &caller_for_dest, - entry_pos, - src_portal.as_ref(), - ); - let _ = tx.send(dest); - }); - rx.await.ok().flatten() - } else { - portal_type.get_portal_destination( + let (tx, rx) = tokio::sync::oneshot::channel(); + rayon::spawn(move || { + let dest = portal_type.get_portal_destination( &world_for_dest, dest_world_opt, &caller_for_dest, entry_pos, src_portal.as_ref(), - ) - }; + ); + let _ = tx.send(dest); + }); + let transition = rx.await.ok().flatten(); if let Some(transition) = transition { let dest_world = transition.new_world.clone(); @@ -3164,7 +3153,7 @@ impl Entity { } } - fn teleport( + pub fn teleport( &self, position: Vector3, yaw: Option, @@ -3485,6 +3474,10 @@ impl Entity { ); } + pub fn remove_passenger_sync(&self, passenger_id: i32) { + self.remove_passenger_on_disconnect(passenger_id); + } + pub async fn remove_passenger(&self, passenger_id: i32) { self.remove_passenger_internal(passenger_id, true).await; } diff --git a/crates/pumpkin/src/entity/passive/iron_golem.rs b/crates/pumpkin/src/entity/passive/iron_golem.rs index 86cd7b81a..01812e857 100644 --- a/crates/pumpkin/src/entity/passive/iron_golem.rs +++ b/crates/pumpkin/src/entity/passive/iron_golem.rs @@ -119,9 +119,6 @@ impl IronGolemEntity { } impl Mob for IronGolemEntity { - fn as_iron_golem(&self) -> Option<&IronGolemEntity> { - Some(self) - } fn mob_write_nbt(&self, nbt: &mut NbtCompound) { nbt.put_bool("PlayerCreated", self.is_player_created()); } diff --git a/crates/pumpkin/src/entity/passive/sheep.rs b/crates/pumpkin/src/entity/passive/sheep.rs index 727c0a820..d3a6b7469 100644 --- a/crates/pumpkin/src/entity/passive/sheep.rs +++ b/crates/pumpkin/src/entity/passive/sheep.rs @@ -156,10 +156,6 @@ impl Mob for SheepEntity { self.set_sheared(false); } - fn get_sheep(&self) -> Option<&SheepEntity> { - Some(self) - } - fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { use super::animal::Animal; self.animal_interact(player, item_stack, Sound::EntitySheepAmbient) diff --git a/crates/pumpkin/src/entity/passive/wolf.rs b/crates/pumpkin/src/entity/passive/wolf.rs index 74b8f7332..e5adbf286 100644 --- a/crates/pumpkin/src/entity/passive/wolf.rs +++ b/crates/pumpkin/src/entity/passive/wolf.rs @@ -328,3 +328,21 @@ impl Mob for WolfEntity { ); } } + +impl WolfEntity { + pub fn get_collar_color(&self) -> u8 { + self.collar_color.load(Ordering::Relaxed) + } + + pub fn set_collar_color(&self, color: u8) { + self.collar_color.store(color, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::wolf::COLLAR_COLOR, + VarInt(color as i32), + )], + None, + ); + } +} diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 886ad1950..96cd9c389 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -411,8 +411,8 @@ pub struct ChunkManager { } impl ChunkManager { - pub const NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE: u8 = 10; - const ACK_STALL_FALLBACK_DELAY: Duration = Duration::from_millis(250); + pub const NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE: u8 = 16; + const ACK_STALL_FALLBACK_DELAY: Duration = Duration::from_millis(100); #[must_use] pub fn new( diff --git a/crates/pumpkin/src/item/items/shears.rs b/crates/pumpkin/src/item/items/shears.rs index 3b0009638..51262d42d 100644 --- a/crates/pumpkin/src/item/items/shears.rs +++ b/crates/pumpkin/src/item/items/shears.rs @@ -76,7 +76,9 @@ impl ItemBehaviour for ShearsItem { } fn use_on_entity(&self, _item: &mut ItemStack, player: &Player, entity: Arc) { - if let Some(sheep) = entity.get_mob().and_then(|m| m.get_sheep()) + if let Some(sheep) = entity + .cast_any() + .downcast_ref::() && !sheep.is_sheared() { sheep.set_sheared(true); diff --git a/crates/pumpkin/src/main.rs b/crates/pumpkin/src/main.rs index bc299a15d..1f0cebd4b 100644 --- a/crates/pumpkin/src/main.rs +++ b/crates/pumpkin/src/main.rs @@ -49,6 +49,11 @@ static MAIN_THREAD: OnceLock = OnceLock::new(); async fn main() { let _ = MAIN_THREAD.set(thread::current().id()); + // Initialize global Rayon thread pool with named worker threads + let _ = rayon::ThreadPoolBuilder::new() + .thread_name(|i| format!("Rayon-Worker-{i}")) + .build_global(); + // Set the panic handler. std::panic::set_hook(Box::new(handle_panic)); diff --git a/crates/pumpkin/src/net/java/config/known_packs.rs b/crates/pumpkin/src/net/java/config/known_packs.rs index a9f4de3e3..c347f569a 100644 --- a/crates/pumpkin/src/net/java/config/known_packs.rs +++ b/crates/pumpkin/src/net/java/config/known_packs.rs @@ -13,45 +13,60 @@ impl JavaClient { if version.supports_configuration_state() { self.send_packet(&CFeatureFlags::new(&["minecraft:vanilla".to_string()])) .await; - let registry = Registry::get_synced(version); - let mut sent_dimension_type = false; - for reg in ®istry { - if reg.registry_id == "minecraft:dimension_type" { - sent_dimension_type = true; + let registry_packets = tokio::task::spawn_blocking(move || { + let registry = Registry::get_synced(version); + let mut packets = Vec::new(); + let mut sent_dimension_type = false; + for reg in ®istry { + if reg.registry_id == "minecraft:dimension_type" { + sent_dimension_type = true; + } + let packet = CRegistryData::new(®.registry_id, ®.registry_entries); + if let Ok(data) = Self::serialize_packet_for_version(&packet, version) { + packets.push(data); + } } - self.send_packet(&CRegistryData::new(®.registry_id, ®.registry_entries)) - .await; - } - if !sent_dimension_type { - let dims = [ - &pumpkin_data::dimension::Dimension::OVERWORLD, - &pumpkin_data::dimension::Dimension::OVERWORLD_CAVES, - &pumpkin_data::dimension::Dimension::THE_END, - &pumpkin_data::dimension::Dimension::THE_NETHER, - ]; - let dim_entries: Vec = dims - .iter() - .map(|dim| pumpkin_data::registry::RegistryEntryData { - entry_id: dim.minecraft_name.to_string(), - data: Some(build_dimension_nbt(dim).into_boxed_slice()), - }) - .collect(); - self.send_packet(&CRegistryData::new( - &"minecraft:dimension_type".to_string(), - &dim_entries, - )) - .await; + if !sent_dimension_type { + let dims = [ + &pumpkin_data::dimension::Dimension::OVERWORLD, + &pumpkin_data::dimension::Dimension::OVERWORLD_CAVES, + &pumpkin_data::dimension::Dimension::THE_END, + &pumpkin_data::dimension::Dimension::THE_NETHER, + ]; + let dim_entries: Vec = dims + .iter() + .map(|dim| pumpkin_data::registry::RegistryEntryData { + entry_id: dim.minecraft_name.to_string(), + data: Some(build_dimension_nbt(dim).into_boxed_slice()), + }) + .collect(); + let dim_type = "minecraft:dimension_type".to_string(); + let packet = CRegistryData::new(&dim_type, &dim_entries); + if let Ok(data) = Self::serialize_packet_for_version(&packet, version) { + packets.push(data); + } + } + let mut tags = Vec::new(); + for &key in pumpkin_data::tag::RegistryKey::NETWORK_KEYS { + if pumpkin_data::tag::get_registry_key_tags(version, key) + .is_some_and(|map| !map.is_empty()) + { + tags.push(key); + } + } + let packet = CUpdateTags::new(&tags); + if let Ok(data) = Self::serialize_packet_for_version(&packet, version) { + packets.push(data); + } + packets + }) + .await + .unwrap_or_default(); + + for packet_data in registry_packets { + self.send_packet_now(packet_data).await; } } - let mut tags = Vec::new(); - for &key in pumpkin_data::tag::RegistryKey::NETWORK_KEYS { - if pumpkin_data::tag::get_registry_key_tags(version, key) - .is_some_and(|map| !map.is_empty()) - { - tags.push(key); - } - } - self.send_packet(&CUpdateTags::new(&tags)).await; // We are done with configuring self.send_packet(&CFinishConfig).await; diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs index d11f84d43..f14d9395e 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs @@ -344,51 +344,84 @@ impl JavaClient { return; }; - if self.version.load() >= JavaMinecraftVersion::V_1_20_2 { - self.send_packet(&CChunkBatchStart).await; - } + let mut valid_chunks = Vec::with_capacity(chunks.len()); for chunk in chunks { let mut event = ChunkSend::new(player.world(), chunk.clone()); server.plugin_manager.fire(&server, &mut event).await; - if event.cancelled { - continue; + if !event.cancelled { + valid_chunks.push(chunk.clone()); } + } - let mut buf = Vec::new(); - let version = self.version.load(); - if let Err(err) = buf.write_var_int(&VarInt(CChunkData::to_id(version))) { - error!("Failed to write chunk data id: {err:?}"); - continue; - } - if let Err(err) = CChunkData(chunk).write_packet_data(&mut buf, &version) { - error!("Failed to write chunk data: {err:?}"); - continue; - } - self.send_packet_now_data(buf.into()).await; + if valid_chunks.is_empty() { + return; + } - if version >= JavaMinecraftVersion::V_1_14 && version < JavaMinecraftVersion::V_1_18 { - match CLightUpdate::from_chunk(chunk, version) { - Ok(light_packet) => { - let mut light_buf = Vec::new(); - if let Err(err) = - light_buf.write_var_int(&VarInt(CLightUpdate::to_id(version))) - { - error!("Failed to write light update id: {err:?}"); - } else if let Err(err) = - light_packet.write_packet_data(&mut light_buf, &version) - { - error!("Failed to write light update data: {err:?}"); - } else { - self.send_packet_now_data(light_buf.into()).await; + let version = self.version.load(); + // Offload CPU-heavy packet serialization (palette packing, lighting, NBT heightmaps) + // off Tokio worker threads onto blocking/Rayon worker threads + let serialize_tasks: Vec<_> = valid_chunks + .into_iter() + .map(|chunk| { + tokio::task::spawn_blocking(move || { + let mut buf = Vec::with_capacity(32 * 1024); + if let Err(err) = buf.write_var_int(&VarInt(CChunkData::to_id(version))) { + error!("Failed to write chunk data id: {err:?}"); + return None; + } + if let Err(err) = CChunkData(&chunk).write_packet_data(&mut buf, &version) { + error!("Failed to write chunk data: {err:?}"); + return None; + } + + let light_buf = if version >= JavaMinecraftVersion::V_1_14 + && version < JavaMinecraftVersion::V_1_18 + { + match CLightUpdate::from_chunk(&chunk, version) { + Ok(light_packet) => { + let mut light_buf = Vec::new(); + if let Err(err) = + light_buf.write_var_int(&VarInt(CLightUpdate::to_id(version))) + { + error!("Failed to write light update id: {err:?}"); + None + } else if let Err(err) = + light_packet.write_packet_data(&mut light_buf, &version) + { + error!("Failed to write light update data: {err:?}"); + None + } else { + Some(Bytes::from(light_buf)) + } + } + Err(err) => { + error!("Failed to create light update packet: {err:?}"); + None + } } - } - Err(err) => { - error!("Failed to create light update packet: {err:?}"); - } + } else { + None + }; + + Some((Bytes::from(buf), light_buf)) + }) + }) + .collect(); + + if version >= JavaMinecraftVersion::V_1_20_2 { + self.send_packet(&CChunkBatchStart).await; + } + + for task in serialize_tasks { + if let Ok(Some((chunk_data, light_data))) = task.await { + self.send_packet_now_data(chunk_data).await; + if let Some(light_data) = light_data { + self.send_packet_now_data(light_data).await; } } } - if self.version.load() >= JavaMinecraftVersion::V_1_20_2 { + + if version >= JavaMinecraftVersion::V_1_20_2 { self.send_packet(&CChunkBatchEnd::new(chunks.len() as u16)) .await; } diff --git a/crates/pumpkin/src/net/query.rs b/crates/pumpkin/src/net/query.rs index 3ce4fd08c..2c86cba41 100644 --- a/crates/pumpkin/src/net/query.rs +++ b/crates/pumpkin/src/net/query.rs @@ -145,7 +145,6 @@ async fn handle_packet( let plugins = server .plugin_manager .active_plugins() - .await .into_iter() .map(|meta| meta.name) .reduce(|acc, name| format!("{acc}, {name}")) diff --git a/crates/pumpkin/src/plugin/api/context.rs b/crates/pumpkin/src/plugin/api/context.rs index 88faccda9..887255461 100644 --- a/crates/pumpkin/src/plugin/api/context.rs +++ b/crates/pumpkin/src/plugin/api/context.rs @@ -316,9 +316,9 @@ impl Context { &self, loader: Arc, ) -> bool { - let before_count = self.plugin_manager.loaded_plugins().await.len(); + let before_count = self.plugin_manager.loaded_plugins().len(); self.plugin_manager.add_loader(&self.server, loader).await; - let after_count = self.plugin_manager.loaded_plugins().await.len(); + let after_count = self.plugin_manager.loaded_plugins().len(); // Return true if any new plugins were loaded after_count > before_count diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs index a76ea7f26..4457c16aa 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs @@ -70,8 +70,22 @@ pub type EnchantmentManagerResource = pub type OpManagerResource = WasmResource>; pub type BanManagerResource = WasmResource>; pub type WhitelistManagerResource = WasmResource>; +pub type DatapackManagerResource = WasmResource>; pub type BlockEntityResource = WasmResource>; +#[derive(Clone)] +pub enum InventoryProvider { + Generic(Arc), + PlayerMain(Arc), + PlayerEnderChest(Arc), +} + +pub type InventoryResource = WasmResource; +pub type PlayerInventoryResource = WasmResource>; + +pub type LivingEntityResource = WasmResource>; +pub type MobResource = WasmResource>; + #[derive(Clone)] pub struct ContainerBlockEntity { pub provider: Arc, @@ -195,6 +209,24 @@ impl PluginHostState { Ok(wasmtime::component::Resource::new_own(resource.rep())) } + pub fn add_living_entity( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self + .resource_table + .push(LivingEntityResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + + pub fn add_mob( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self.resource_table.push(MobResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + pub fn add_world( &mut self, provider: Arc, @@ -362,6 +394,34 @@ impl PluginHostState { Ok(wasmtime::component::Resource::new_own(resource.rep())) } + pub fn add_datapack_manager( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self + .resource_table + .push(DatapackManagerResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + + pub fn add_inventory( + &mut self, + provider: InventoryProvider, + ) -> wasmtime::Result> { + let resource = self.resource_table.push(InventoryResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + + pub fn add_player_inventory( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self + .resource_table + .push(PlayerInventoryResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + pub fn add_block_entity( &mut self, provider: Arc, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/block_entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/block_entity.rs index fbbab43d5..a25c8e6e3 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/block_entity.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/block_entity.rs @@ -319,6 +319,24 @@ impl HostContainerBlockEntity for PluginHostState { self.add_block_entity(provider) } + async fn get_inventory( + &mut self, + res: Resource, + ) -> wasmtime::Result< + Resource< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::inventory::Inventory, + >, + >{ + let container = self + .resource_table + .get::(&Resource::new_own(res.rep())) + .map_err(|_| wasmtime::Error::msg("invalid container block entity resource handle"))?; + let inventory = container.provider.inventory.clone(); + self.add_inventory( + crate::plugin::loader::wasm::wasm_host::state::InventoryProvider::Generic(inventory), + ) + } + async fn get_size(&mut self, res: Resource) -> wasmtime::Result { let container = self .resource_table diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/datapack.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/datapack.rs new file mode 100644 index 000000000..50db7492c --- /dev/null +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/datapack.rs @@ -0,0 +1,154 @@ +use crate::data::datapack::DatapackManager; +use crate::plugin::loader::wasm::wasm_host::state::PluginHostState; +use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::datapack::{ + DatapackInfo as WitDatapackInfo, DatapackManager as WitDatapackManager, + EnablePosition as WitEnablePosition, Host as DatapackHost, HostDatapackManager, +}; +use wasmtime::component::Resource; + +impl DatapackHost for PluginHostState {} + +impl HostDatapackManager for PluginHostState { + async fn list_all_packs( + &mut self, + _res: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let packs = DatapackManager::list_all_packs(server); + Ok(packs.into_iter().map(to_wit_datapack_info).collect()) + } + + async fn list_enabled_packs( + &mut self, + _res: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let packs = DatapackManager::list_enabled_packs(server); + Ok(packs.into_iter().map(to_wit_datapack_info).collect()) + } + + async fn list_available_packs( + &mut self, + _res: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let packs = DatapackManager::list_available_packs(server); + Ok(packs.into_iter().map(to_wit_datapack_info).collect()) + } + + async fn get_pack( + &mut self, + _res: Resource, + name: String, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let pack = DatapackManager::get_pack_info(server, &name); + Ok(pack.map(to_wit_datapack_info)) + } + + async fn is_enabled( + &mut self, + _res: Resource, + name: String, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + Ok(DatapackManager::is_pack_enabled(server, &name)) + } + + async fn enable_pack( + &mut self, + _res: Resource, + name: String, + position: WitEnablePosition, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let pos = to_data_enable_position(position); + Ok(DatapackManager::enable_pack(server, &name, pos)) + } + + async fn disable_pack( + &mut self, + _res: Resource, + name: String, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + Ok(DatapackManager::disable_pack(server, &name)) + } + + async fn reload( + &mut self, + _res: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + Ok(DatapackManager::reload(server)) + } + + async fn execute_function( + &mut self, + _res: Resource, + name: String, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let result = DatapackManager::execute_function_from_console(server, &name); + Ok(result.map(|count| count as u32)) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::( + Resource::new_own(rep.rep()), + ); + Ok(()) + } +} + +fn to_wit_datapack_info(info: crate::data::datapack::DatapackInfo) -> WitDatapackInfo { + WitDatapackInfo { + id: info.id, + name: info.name, + description: info.description, + pack_format: info.pack_format, + is_enabled: info.is_enabled, + recipe_count: info.recipe_count as u32, + function_count: info.function_count as u32, + } +} + +fn to_data_enable_position( + pos: WitEnablePosition, +) -> crate::data::datapack::DatapackEnablePosition { + match pos { + WitEnablePosition::First => crate::data::datapack::DatapackEnablePosition::First, + WitEnablePosition::Last => crate::data::datapack::DatapackEnablePosition::Last, + WitEnablePosition::Before(s) => crate::data::datapack::DatapackEnablePosition::Before(s), + WitEnablePosition::After(s) => crate::data::datapack::DatapackEnablePosition::After(s), + } +} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs index c78a5809a..b971533e1 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs @@ -3,27 +3,18 @@ use wasmtime::component::Resource; use pumpkin_util::math::vector3::Vector3; -use crate::entity::ai::goal::Goal; -use crate::entity::mob::Mob; -use crate::plugin::loader::wasm::wasm_host::{PluginInstance, WasmPlugin}; use crate::plugin::loader::wasm::wasm_host::{ state::{EntityResource, PluginHostState}, wit::v0_1::events::to_wasm_position, wit::v0_1::pumpkin::plugin::{ - attributes::{ - Attribute, AttributeModifier as WitAttributeModifier, - ModifierOperation as WitModifierOperation, - }, common::{EntityPose, NbtTree as WitNbtTree, Position}, - damage_types::DamageType as WitDamageType, entity::Host, entity_types, - item_stack::ItemStack as WitHostItemStack, text::TextComponent, uuid::Uuid, world::{ - BlockPos as WitBlockPos, BoundingBox as WitBoundingBox, Entity, - EquipmentSlot as WitEquipmentSlot, HostEntity, + BlockPos as WitBlockPos, BoundingBox as WitBoundingBox, Entity, HostEntity, + LivingEntity as WitLivingEntity, Mob as WitMob, RayTraceBlockResult as WitRayTraceBlockResult, RayTraceEntityResult as WitRayTraceEntityResult, RaycastResult as WitRaycastResult, World, @@ -37,7 +28,7 @@ use pumpkin_data::entity::EntityPose as InternalEntityPose; impl Host for PluginHostState {} impl entity_types::Host for PluginHostState {} -fn entity_from_resource( +pub fn entity_from_resource( state: &PluginHostState, entity: &Resource, ) -> wasmtime::Result> { @@ -71,112 +62,6 @@ const fn map_entity_pose(pose: InternalEntityPose) -> EntityPose { } } -#[must_use] -pub const fn from_wit_attribute(attr: Attribute) -> &'static pumpkin_data::attributes::Attributes { - use pumpkin_data::attributes::Attributes; - match attr { - Attribute::AirDragModifier => &Attributes::AIR_DRAG_MODIFIER, - Attribute::Armor => &Attributes::ARMOR, - Attribute::ArmorToughness => &Attributes::ARMOR_TOUGHNESS, - Attribute::AttackDamage => &Attributes::ATTACK_DAMAGE, - Attribute::AttackKnockback => &Attributes::ATTACK_KNOCKBACK, - Attribute::AttackSpeed => &Attributes::ATTACK_SPEED, - Attribute::BelowNameDistance => &Attributes::BELOW_NAME_DISTANCE, - Attribute::BlockBreakSpeed => &Attributes::BLOCK_BREAK_SPEED, - Attribute::BlockInteractionRange => &Attributes::BLOCK_INTERACTION_RANGE, - Attribute::Bounciness => &Attributes::BOUNCINESS, - Attribute::BurningTime => &Attributes::BURNING_TIME, - Attribute::CameraDistance => &Attributes::CAMERA_DISTANCE, - Attribute::ExplosionKnockbackResistance => &Attributes::EXPLOSION_KNOCKBACK_RESISTANCE, - Attribute::EntityInteractionRange => &Attributes::ENTITY_INTERACTION_RANGE, - Attribute::FallDamageMultiplier => &Attributes::FALL_DAMAGE_MULTIPLIER, - Attribute::FlyingSpeed => &Attributes::FLYING_SPEED, - Attribute::FollowRange => &Attributes::FOLLOW_RANGE, - Attribute::FrictionModifier => &Attributes::FRICTION_MODIFIER, - Attribute::Gravity => &Attributes::GRAVITY, - Attribute::JumpStrength => &Attributes::JUMP_STRENGTH, - Attribute::KnockbackResistance => &Attributes::KNOCKBACK_RESISTANCE, - Attribute::Luck => &Attributes::LUCK, - Attribute::MaxAbsorption => &Attributes::MAX_ABSORPTION, - Attribute::MaxHealth => &Attributes::MAX_HEALTH, - Attribute::MiningEfficiency => &Attributes::MINING_EFFICIENCY, - Attribute::MovementEfficiency => &Attributes::MOVEMENT_EFFICIENCY, - Attribute::MovementSpeed => &Attributes::MOVEMENT_SPEED, - Attribute::NameTagDistance => &Attributes::NAME_TAG_DISTANCE, - Attribute::OxygenBonus => &Attributes::OXYGEN_BONUS, - Attribute::SafeFallDistance => &Attributes::SAFE_FALL_DISTANCE, - Attribute::Scale => &Attributes::SCALE, - Attribute::SneakingSpeed => &Attributes::SNEAKING_SPEED, - Attribute::SpawnReinforcements => &Attributes::SPAWN_REINFORCEMENTS, - Attribute::StepHeight => &Attributes::STEP_HEIGHT, - Attribute::SubmergedMiningSpeed => &Attributes::SUBMERGED_MINING_SPEED, - Attribute::SweepingDamageRatio => &Attributes::SWEEPING_DAMAGE_RATIO, - Attribute::TemptRange => &Attributes::TEMPT_RANGE, - Attribute::WaterMovementEfficiency => &Attributes::WATER_MOVEMENT_EFFICIENCY, - Attribute::WaypointTransmitRange => &Attributes::WAYPOINT_TRANSMIT_RANGE, - Attribute::WaypointReceiveRange => &Attributes::WAYPOINT_RECEIVE_RANGE, - } -} - -#[must_use] -pub const fn from_wit_modifier_op( - op: WitModifierOperation, -) -> crate::entity::attributes::ModifierOperation { - match op { - WitModifierOperation::Add => crate::entity::attributes::ModifierOperation::Add, - WitModifierOperation::MultiplyBase => { - crate::entity::attributes::ModifierOperation::MultiplyBase - } - WitModifierOperation::MultiplyTotal => { - crate::entity::attributes::ModifierOperation::MultiplyTotal - } - } -} - -#[must_use] -pub const fn to_wit_modifier_op( - op: crate::entity::attributes::ModifierOperation, -) -> WitModifierOperation { - match op { - crate::entity::attributes::ModifierOperation::Add => WitModifierOperation::Add, - crate::entity::attributes::ModifierOperation::MultiplyBase => { - WitModifierOperation::MultiplyBase - } - crate::entity::attributes::ModifierOperation::MultiplyTotal => { - WitModifierOperation::MultiplyTotal - } - } -} - -#[must_use] -pub const fn from_wit_equipment_slot( - slot: WitEquipmentSlot, -) -> pumpkin_data::data_component_impl::EquipmentSlot { - use pumpkin_data::data_component_impl::EquipmentSlot; - match slot { - WitEquipmentSlot::MainHand => EquipmentSlot::MAIN_HAND, - WitEquipmentSlot::OffHand => EquipmentSlot::OFF_HAND, - WitEquipmentSlot::Feet => EquipmentSlot::FEET, - WitEquipmentSlot::Legs => EquipmentSlot::LEGS, - WitEquipmentSlot::Chest => EquipmentSlot::CHEST, - WitEquipmentSlot::Head => EquipmentSlot::HEAD, - WitEquipmentSlot::Body => EquipmentSlot::BODY, - WitEquipmentSlot::Saddle => EquipmentSlot::SADDLE, - } -} - -#[must_use] -pub const fn to_wit_damage_type(damage_type: &pumpkin_data::damage::DamageType) -> WitDamageType { - // SAFETY: WIT enum is generated in the same order as the internal enum / id - unsafe { std::mem::transmute(damage_type.id) } -} - -#[must_use] -pub fn from_wit_damage_type(wit: WitDamageType) -> pumpkin_data::damage::DamageType { - pumpkin_data::damage::DamageType::from_id(wit as u8) - .unwrap_or(pumpkin_data::damage::DamageType::GENERIC) -} - impl HostEntity for PluginHostState { async fn get_id(&mut self, entity: Resource) -> wasmtime::Result { let entity = entity_from_resource(self, &entity)?; @@ -526,300 +411,6 @@ impl HostEntity for PluginHostState { Ok(()) } - async fn get_health(&mut self, entity: Resource) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - Ok(entity - .get_living_entity() - .map_or(0.0, |living| living.health.load())) - } - - async fn set_health(&mut self, entity: Resource, health: f32) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(living) = entity.get_living_entity() { - living.health.store(health); - } - Ok(()) - } - - async fn get_max_health(&mut self, entity: Resource) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - Ok(entity - .get_living_entity() - .map_or(0.0, crate::entity::living::LivingEntity::get_max_health)) - } - - async fn damage( - &mut self, - entity: Resource, - amount: f32, - damage_type: WitDamageType, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - entity.damage(&*entity, amount, from_wit_damage_type(damage_type)); - Ok(()) - } - - async fn is_dead(&mut self, entity: Resource) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - Ok(entity.get_living_entity().map_or_else( - || entity.get_entity().removal_reason.load().is_some(), - |living| living.dead.load(std::sync::atomic::Ordering::Relaxed), - )) - } - - async fn get_absorption(&mut self, entity: Resource) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - Ok(entity - .get_living_entity() - .map_or(0.0, |living| living.absorption.load())) - } - - async fn set_absorption( - &mut self, - entity: Resource, - amount: f32, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(living) = entity.get_living_entity() { - living.absorption.store(amount); - } - Ok(()) - } - - async fn get_attribute_value( - &mut self, - entity: Resource, - attr: Attribute, - ) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - let attribute = from_wit_attribute(attr); - Ok(entity - .get_living_entity() - .map_or(attribute.default_value, |living| { - living.get_attribute_value(attribute) - })) - } - - async fn get_attribute_base( - &mut self, - entity: Resource, - attr: Attribute, - ) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - let attribute = from_wit_attribute(attr); - Ok(entity - .get_living_entity() - .map_or(attribute.default_value, |living| { - living.get_attribute_base(attribute) - })) - } - - async fn set_attribute_base( - &mut self, - entity: Resource, - attr: Attribute, - value: f64, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - let attribute = from_wit_attribute(attr); - if let Some(living) = entity.get_living_entity() { - living.set_attribute_base(attribute, value); - crate::entity::attributes::send_attribute_updates_for_living( - living, - vec![attribute.clone()], - ); - } - Ok(()) - } - - async fn add_attribute_modifier( - &mut self, - entity: Resource, - attr: Attribute, - modifier: WitAttributeModifier, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - let attribute = from_wit_attribute(attr); - if let Some(living) = entity.get_living_entity() { - let internal_mod = crate::entity::attributes::Modifier { - id: modifier.id, - amount: modifier.amount, - operation: from_wit_modifier_op(modifier.operation), - }; - living.update_attribute(attribute, |inst| inst.add_or_replace_modifier(internal_mod)); - crate::entity::attributes::send_attribute_updates_for_living( - living, - vec![attribute.clone()], - ); - } - Ok(()) - } - - async fn remove_attribute_modifier( - &mut self, - entity: Resource, - attr: Attribute, - id: String, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - let attribute = from_wit_attribute(attr); - if let Some(living) = entity.get_living_entity() { - living.update_attribute(attribute, |inst| inst.remove_modifier(&id)); - crate::entity::attributes::send_attribute_updates_for_living( - living, - vec![attribute.clone()], - ); - } - Ok(()) - } - - async fn get_attribute_modifiers( - &mut self, - entity: Resource, - attr: Attribute, - ) -> wasmtime::Result> { - let entity = entity_from_resource(self, &entity)?; - let attribute = from_wit_attribute(attr); - if let Some(living) = entity.get_living_entity() { - let map = living - .attributes - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(inst) = map.get(&attribute.id) { - return Ok(inst - .modifiers - .iter() - .map(|m| WitAttributeModifier { - id: m.id.clone(), - amount: m.amount, - operation: to_wit_modifier_op(m.operation), - }) - .collect()); - } - } - Ok(Vec::new()) - } - - async fn reset_attribute( - &mut self, - entity: Resource, - attr: Attribute, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - let attribute = from_wit_attribute(attr); - if let Some(living) = entity.get_living_entity() { - { - let mut map = living - .attributes - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner); - map.remove(&attribute.id); - }; - crate::entity::attributes::send_attribute_updates_for_living( - living, - vec![attribute.clone()], - ); - } - Ok(()) - } - - async fn reset_all_attributes(&mut self, entity: Resource) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(living) = entity.get_living_entity() { - living.reset_effects_and_attributes(); - } - Ok(()) - } - - async fn get_equipment( - &mut self, - entity: Resource, - slot: WitEquipmentSlot, - ) -> wasmtime::Result>> { - let entity = entity_from_resource(self, &entity)?; - if let Some(living) = entity.get_living_entity() { - let slot = from_wit_equipment_slot(slot); - let equipment = living - .entity_equipment - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let stack = equipment.get(&slot); - if !stack.is_empty() { - return Ok(Some( - self.add_item_stack(Arc::new(tokio::sync::Mutex::new(stack)))?, - )); - } - } - Ok(None) - } - - async fn set_equipment( - &mut self, - entity: Resource, - slot: WitEquipmentSlot, - stack: Option>, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(living) = entity.get_living_entity() { - let slot = from_wit_equipment_slot(slot); - let item_stack = if let Some(stack_res) = stack { - self.get_item_stack(&stack_res)?.lock().await.clone() - } else { - pumpkin_data::item_stack::ItemStack::EMPTY.clone() - }; - - { - let mut equipment = living - .entity_equipment - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - equipment.put(&slot, item_stack.clone()); - }; - - living.send_equipment_changes(&[(slot, item_stack)]); - } - Ok(()) - } - - async fn clear_equipment(&mut self, entity: Resource) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(living) = entity.get_living_entity() { - let mut equipment = living - .entity_equipment - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let slots_to_clear: Vec<( - pumpkin_data::data_component_impl::EquipmentSlot, - pumpkin_data::item_stack::ItemStack, - )> = equipment - .equipment - .drain() - .map(|(slot, _)| (slot, pumpkin_data::item_stack::ItemStack::EMPTY.clone())) - .collect(); - drop(equipment); - - living.send_equipment_changes(&slots_to_clear); - } - Ok(()) - } - - async fn get_age(&mut self, entity: Resource) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - Ok(entity - .get_entity() - .age - .load(std::sync::atomic::Ordering::Relaxed)) - } - - async fn set_age(&mut self, entity: Resource, age: i32) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - entity - .get_entity() - .age - .store(age, std::sync::atomic::Ordering::Relaxed); - Ok(()) - } - async fn get_fall_distance(&mut self, entity: Resource) -> wasmtime::Result { let entity = entity_from_resource(self, &entity)?; Ok(entity @@ -1155,144 +746,12 @@ impl HostEntity for PluginHostState { Ok(crate::entity::breath::MAX_AIR) } - async fn send_system_message( - &mut self, - entity: Resource, - message: Resource, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(player) = entity.get_player() { - let text_res = self - .resource_table - .get::( - &Resource::new_own(message.rep()), - ) - .map_err(|_| wasmtime::Error::msg("invalid text component resource handle"))?; - player.send_system_message(&text_res.provider); - } - Ok(()) - } - async fn remove(&mut self, entity: Resource) -> wasmtime::Result<()> { let entity = entity_from_resource(self, &entity)?; entity.get_entity().remove(); Ok(()) } - async fn add_ai_goal( - &mut self, - entity: Resource, - priority: u8, - goal: crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(mob) = entity.get_mob() { - let mob_entity = mob.get_mob_entity(); - match goal { - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::Swim => { - mob_entity.add_goal(priority, crate::entity::ai::goal::swim::SwimGoal::default()); - } - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::WanderAround(speed) => { - mob_entity.add_goal(priority, crate::entity::ai::goal::wander_around::WanderAroundGoal::new(f64::from(speed))); - } - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::MeleeAttack(speed) => { - mob_entity.add_goal(priority, crate::entity::ai::goal::melee_attack::MeleeAttackGoal::new(f64::from(speed), false)); - } - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::LookAtPlayer(range) => { - mob_entity.add_goal(priority, crate::entity::ai::goal::look_at_entity::LookAtEntityGoal::new( - std::sync::Weak::::new() as std::sync::Weak, - &pumpkin_data::entity::EntityType::PLAYER, - range, - 0.02, - false, - )); - } - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::LookAround => { - mob_entity.add_goal(priority, crate::entity::ai::goal::look_around::RandomLookAroundGoal::default()); - } - crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::EscapeDanger(speed) => { - mob_entity.add_goal(priority, *crate::entity::ai::goal::escape_danger::EscapeDangerGoal::new(f64::from(speed))); - } - _ => {} // Remaining goals - } - } - Ok(()) - } - - async fn add_custom_ai_goal( - &mut self, - entity: Resource, - priority: u8, - goal_id: u32, - ) -> wasmtime::Result<()> { - let Some(plugin) = self.plugin.as_ref().and_then(std::sync::Weak::upgrade) else { - return Err(wasmtime::Error::msg("Plugin not active")); - }; - let entity = entity_from_resource(self, &entity)?; - if let Some(mob) = entity.get_mob() { - let mob_entity = mob.get_mob_entity(); - mob_entity.add_goal(priority, CustomWasmGoal { plugin, goal_id }); - } - Ok(()) - } - - async fn clear_ai_goals(&mut self, entity: Resource) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(mob) = entity.get_mob() { - mob.get_mob_entity().clear_ai_goals(mob); - } - Ok(()) - } - - async fn set_ai_disabled( - &mut self, - entity: Resource, - disabled: bool, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - if let Some(mob) = entity.get_mob() { - mob.get_mob_entity().set_no_ai(disabled); - } - Ok(()) - } - - async fn is_ai_disabled(&mut self, entity: Resource) -> wasmtime::Result { - let entity = entity_from_resource(self, &entity)?; - Ok(entity - .get_mob() - .is_none_or(|mob| mob.get_mob_entity().is_no_ai())) - } - - async fn set_target( - &mut self, - entity: Resource, - target: Option>, - ) -> wasmtime::Result<()> { - let entity = entity_from_resource(self, &entity)?; - let target_entity = if let Some(t) = target { - Some(entity_from_resource(self, &t)?) - } else { - None - }; - if let Some(mob) = entity.get_mob() { - mob.get_mob_entity().set_target(target_entity); - } - Ok(()) - } - - async fn get_target( - &mut self, - entity: Resource, - ) -> wasmtime::Result>> { - let entity = entity_from_resource(self, &entity)?; - if let Some(mob) = entity.get_mob() - && let Some(target) = mob.get_mob_entity().get_target() - { - return Ok(Some(self.add_entity(target)?)); - } - Ok(None) - } - async fn raycast( &mut self, entity: Resource, @@ -1429,6 +888,40 @@ impl HostEntity for PluginHostState { Ok(base_entity.has_custom_data(&namespace, &key)) } + async fn as_living( + &mut self, + this: Resource, + ) -> wasmtime::Result>> { + let entity = entity_from_resource(self, &this)?; + if entity.get_living_entity().is_some() { + Ok(Some(self.add_living_entity(entity)?)) + } else { + Ok(None) + } + } + + async fn as_mob( + &mut self, + this: Resource, + ) -> wasmtime::Result>> { + let entity = entity_from_resource(self, &this)?; + if entity.get_mob().is_some() { + Ok(Some(self.add_mob(entity)?)) + } else { + Ok(None) + } + } + + async fn is_living(&mut self, this: Resource) -> wasmtime::Result { + let entity = entity_from_resource(self, &this)?; + Ok(entity.get_living_entity().is_some()) + } + + async fn is_mob(&mut self, this: Resource) -> wasmtime::Result { + let entity = entity_from_resource(self, &this)?; + Ok(entity.get_mob().is_some()) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { let _ = self .resource_table @@ -1436,161 +929,3 @@ impl HostEntity for PluginHostState { Ok(()) } } - -pub struct CustomWasmGoal { - pub plugin: Arc, - pub goal_id: u32, -} - -fn current_mob_entity(mob: &dyn Mob) -> Option> { - let entity = mob.get_entity(); - entity.world.load().get_entity_by_id(entity.entity_id) -} - -impl Goal for CustomWasmGoal { - fn can_start(&mut self, _mob: &dyn Mob) -> bool { - false - } - - fn should_continue(&self, _mob: &dyn Mob) -> bool { - false - } - - fn start(&mut self, mob: &dyn Mob) { - if let Some(entity_arc) = current_mob_entity(mob) { - let plugin = self.plugin.clone(); - let goal_id = self.goal_id; - tokio::spawn(async move { - let mut store = plugin.store.lock().await; - match plugin.plugin_instance { - PluginInstance::V0_1(ref plugin_inst) => { - let Some(server) = store.data_mut().server.clone() else { - return; - }; - let Ok(server_res) = store.data_mut().add_server(server) else { - return; - }; - let Ok(entity_res) = store.data_mut().add_entity(entity_arc) else { - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(server_res.rep()), - ); - return; - }; - let server_rep = server_res.rep(); - let entity_rep = entity_res.rep(); - let _ = plugin_inst - .call_handle_ai_goal_start(&mut *store, goal_id, server_res, entity_res) - .await; - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(server_rep), - ); - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(entity_rep), - ); - } - } - }); - } - } - - fn tick(&mut self, mob: &dyn Mob) { - if let Some(entity_arc) = current_mob_entity(mob) { - let plugin = self.plugin.clone(); - let goal_id = self.goal_id; - tokio::spawn(async move { - let mut store = plugin.store.lock().await; - match plugin.plugin_instance { - PluginInstance::V0_1(ref plugin_inst) => { - let Some(server) = store.data_mut().server.clone() else { - return; - }; - let Ok(server_res) = store.data_mut().add_server(server) else { - return; - }; - let Ok(entity_res) = store.data_mut().add_entity(entity_arc) else { - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(server_res.rep()), - ); - return; - }; - let server_rep = server_res.rep(); - let entity_rep = entity_res.rep(); - let _ = plugin_inst - .call_handle_ai_goal_tick(&mut *store, goal_id, server_res, entity_res) - .await; - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(server_rep), - ); - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(entity_rep), - ); - } - } - }); - } - } - - fn stop(&mut self, mob: &dyn Mob) { - if let Some(entity_arc) = current_mob_entity(mob) { - let plugin = self.plugin.clone(); - let goal_id = self.goal_id; - tokio::spawn(async move { - let mut store = plugin.store.lock().await; - match plugin.plugin_instance { - PluginInstance::V0_1(ref plugin_inst) => { - let Some(server) = store.data_mut().server.clone() else { - return; - }; - let Ok(server_res) = store.data_mut().add_server(server) else { - return; - }; - let Ok(entity_res) = store.data_mut().add_entity(entity_arc) else { - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(server_res.rep()), - ); - return; - }; - let server_rep = server_res.rep(); - let entity_rep = entity_res.rep(); - let _ = plugin_inst - .call_handle_ai_goal_stop(&mut *store, goal_id, server_res, entity_res) - .await; - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(server_rep), - ); - let _ = store - .data_mut() - .resource_table - .delete::( - wasmtime::component::Resource::new_own(entity_rep), - ); - } - } - }); - } - } -} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/entity.rs index 1d6e3be11..5c5954630 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/entity.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/entity.rs @@ -67,12 +67,12 @@ use crate::plugin::{ loader::wasm::wasm_host::{ state::PluginHostState, wit::v0_1::{ - entity::{from_wit_damage_type, to_wit_damage_type}, events::{ ToFromWasmEvent, cleanup_event, consume_player, consume_text_component, consume_world, from_wasm_block_position, from_wasm_position, to_wasm_block_position, to_wasm_position, }, + living_entity::{from_wit_damage_type, to_wit_damage_type}, pumpkin::plugin::event::{ AreaEffectCloudApplyEventData, ArrowBodyCountChangeEventData, BatToggleSleepEventData, CreatureSpawnEventData, CreeperPowerEventData, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs index 4c0407f37..7c06c072a 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs @@ -118,6 +118,23 @@ impl gui::HostGui for PluginHostState { self.add_gui(gui) } + async fn get_inventory( + &mut self, + res: Resource, + ) -> wasmtime::Result< + Resource< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::inventory::Inventory, + >, + >{ + let inv = { + let gui = self.get_gui_res(&res)?.provider.lock().await; + gui.inventory.clone() + }; + self.add_inventory( + crate::plugin::loader::wasm::wasm_host::state::InventoryProvider::Generic(inv), + ) + } + async fn set_item( &mut self, res: Resource, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/inventory.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/inventory.rs new file mode 100644 index 000000000..cade06135 --- /dev/null +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/inventory.rs @@ -0,0 +1,568 @@ +use std::sync::Arc; +use tokio::sync::Mutex; +use wasmtime::component::Resource; + +use crate::entity::player::Player; +use crate::plugin::loader::wasm::wasm_host::{ + state::{InventoryProvider, InventoryResource, PlayerInventoryResource, PluginHostState}, + wit::v0_1::pumpkin::plugin::{ + common::Hand as WitHand, + inventory::{ + Host as InventoryHost, HostInventory, HostPlayerInventory, Inventory as WitInventory, + PlayerInventory as WitPlayerInventory, + }, + item_stack::ItemStack as WitHostItemStack, + }, +}; +use pumpkin_inventory::player::player_inventory::PlayerInventory; +use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; +use pumpkin_protocol::java::client::play::CSetContainerSlot; +use pumpkin_world::inventory::{Clearable, Inventory}; + +const fn from_wasm_hand(hand: WitHand) -> pumpkin_util::Hand { + match hand { + WitHand::Right => pumpkin_util::Hand::Right, + WitHand::Left => pumpkin_util::Hand::Left, + } +} + +impl InventoryHost for PluginHostState {} + +impl PluginHostState { + fn get_inventory_provider( + &self, + res: &Resource, + ) -> wasmtime::Result { + let r = self + .resource_table + .get::(&Resource::new_own(res.rep())) + .map_err(wasmtime::Error::from)?; + Ok(r.provider.clone()) + } + + fn get_player_inventory_player( + &self, + res: &Resource, + ) -> wasmtime::Result> { + let r = self + .resource_table + .get::(&Resource::new_own(res.rep())) + .map_err(wasmtime::Error::from)?; + Ok(r.provider.clone()) + } +} + +impl HostInventory for PluginHostState { + async fn get_size(&mut self, res: Resource) -> wasmtime::Result { + let provider = self.get_inventory_provider(&res)?; + let size = match provider { + InventoryProvider::Generic(inv) => inv.size() as u32, + InventoryProvider::PlayerMain(_) => 36, + InventoryProvider::PlayerEnderChest(_) => 27, + }; + Ok(size) + } + + async fn is_empty(&mut self, res: Resource) -> wasmtime::Result { + let provider = self.get_inventory_provider(&res)?; + let empty = match provider { + InventoryProvider::Generic(inv) => inv.is_empty(), + InventoryProvider::PlayerMain(player) => { + let inv = player.inventory(); + (0..36).all(|slot| inv.get_stack(slot).is_empty()) + } + InventoryProvider::PlayerEnderChest(player) => { + let ec = player.ender_chest_inventory(); + (0..27).all(|slot| ec.get_stack(slot).is_empty()) + } + }; + Ok(empty) + } + + async fn get_item( + &mut self, + res: Resource, + slot: u32, + ) -> wasmtime::Result>> { + let provider = self.get_inventory_provider(&res)?; + let stack = match provider { + InventoryProvider::Generic(inv) => { + let s = inv.get_stack(slot as usize); + if s.is_empty() { None } else { Some(s) } + } + InventoryProvider::PlayerMain(player) => { + if slot < 36 { + let s = player.inventory().get_stack(slot as usize); + if s.is_empty() { None } else { Some(s) } + } else { + None + } + } + InventoryProvider::PlayerEnderChest(player) => { + if slot < 27 { + let s = player.ender_chest_inventory().get_stack(slot as usize); + if s.is_empty() { None } else { Some(s) } + } else { + None + } + } + }; + + if let Some(stack) = stack { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } else { + Ok(None) + } + } + + async fn set_item( + &mut self, + res: Resource, + slot: u32, + item: Option>, + ) -> wasmtime::Result<()> { + let stack = if let Some(stack_res) = item { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + + let provider = self.get_inventory_provider(&res)?; + match provider { + InventoryProvider::Generic(inv) => { + inv.set_stack(slot as usize, stack); + } + InventoryProvider::PlayerMain(player) => { + if slot < 36 { + player.inventory().set_stack(slot as usize, stack.clone()); + let stack_serializer = ItemStackSerializer::from(stack); + let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer); + player.send_client_packet(&packet).await; + } + } + InventoryProvider::PlayerEnderChest(player) => { + if slot < 27 { + player + .ender_chest_inventory() + .set_stack(slot as usize, stack); + } + } + } + Ok(()) + } + + async fn remove_item( + &mut self, + res: Resource, + slot: u32, + ) -> wasmtime::Result>> { + let provider = self.get_inventory_provider(&res)?; + let old_stack = match provider { + InventoryProvider::Generic(inv) => { + let s = inv.remove_stack(slot as usize); + if s.is_empty() { None } else { Some(s) } + } + InventoryProvider::PlayerMain(player) => { + if slot < 36 { + let s = player.inventory().get_stack(slot as usize); + player.inventory().set_stack( + slot as usize, + pumpkin_data::item_stack::ItemStack::EMPTY.clone(), + ); + let empty_serializer = ItemStackSerializer::from( + pumpkin_data::item_stack::ItemStack::EMPTY.clone(), + ); + let packet = CSetContainerSlot::new(0, 0, slot as i16, &empty_serializer); + player.send_client_packet(&packet).await; + if s.is_empty() { None } else { Some(s) } + } else { + None + } + } + InventoryProvider::PlayerEnderChest(player) => { + if slot < 27 { + let s = player.ender_chest_inventory().get_stack(slot as usize); + player.ender_chest_inventory().set_stack( + slot as usize, + pumpkin_data::item_stack::ItemStack::EMPTY.clone(), + ); + if s.is_empty() { None } else { Some(s) } + } else { + None + } + } + }; + + if let Some(stack) = old_stack { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } else { + Ok(None) + } + } + + async fn clear(&mut self, res: Resource) -> wasmtime::Result<()> { + let provider = self.get_inventory_provider(&res)?; + match provider { + InventoryProvider::Generic(inv) => { + inv.clear(); + } + InventoryProvider::PlayerMain(player) => { + for slot in 0..36 { + player + .inventory() + .set_stack(slot, pumpkin_data::item_stack::ItemStack::EMPTY.clone()); + let empty_serializer = ItemStackSerializer::from( + pumpkin_data::item_stack::ItemStack::EMPTY.clone(), + ); + let packet = CSetContainerSlot::new(0, 0, slot as i16, &empty_serializer); + player.send_client_packet(&packet).await; + } + } + InventoryProvider::PlayerEnderChest(player) => { + player.ender_chest_inventory().clear(); + } + } + Ok(()) + } + + async fn get_all_items( + &mut self, + res: Resource, + ) -> wasmtime::Result>>> { + let size = self.get_size(Resource::new_own(res.rep())).await?; + let mut items = Vec::with_capacity(size as usize); + for slot in 0..size { + let item = self.get_item(Resource::new_own(res.rep()), slot).await?; + items.push(item); + } + Ok(items) + } + + async fn set_all_items( + &mut self, + res: Resource, + items: Vec>>, + ) -> wasmtime::Result<()> { + let size = self.get_size(Resource::new_own(res.rep())).await?; + for (slot, item) in items.into_iter().take(size as usize).enumerate() { + self.set_item(Resource::new_own(res.rep()), slot as u32, item) + .await?; + } + Ok(()) + } + + async fn count_item( + &mut self, + res: Resource, + item_id: String, + ) -> wasmtime::Result { + let provider = self.get_inventory_provider(&res)?; + let mut total = 0u32; + let is_matching = + |key: &str| key == item_id || key.strip_prefix("minecraft:") == Some(&item_id); + match provider { + InventoryProvider::Generic(inv) => { + for slot in 0..inv.size() { + let s = inv.get_stack(slot); + if !s.is_empty() && is_matching(s.item.registry_key) { + total += u32::from(s.item_count); + } + } + } + InventoryProvider::PlayerMain(player) => { + let inv = player.inventory(); + for slot in 0..36 { + let s = inv.get_stack(slot); + if !s.is_empty() && is_matching(s.item.registry_key) { + total += u32::from(s.item_count); + } + } + } + InventoryProvider::PlayerEnderChest(player) => { + let ec = player.ender_chest_inventory(); + for slot in 0..27 { + let s = ec.get_stack(slot); + if !s.is_empty() && is_matching(s.item.registry_key) { + total += u32::from(s.item_count); + } + } + } + } + Ok(total) + } + + async fn contains_item( + &mut self, + res: Resource, + item_id: String, + ) -> wasmtime::Result { + let count = self.count_item(res, item_id).await?; + Ok(count > 0) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::(Resource::new_own(rep.rep())); + Ok(()) + } +} + +impl HostPlayerInventory for PluginHostState { + async fn as_inventory( + &mut self, + res: Resource, + ) -> wasmtime::Result> { + let player = self.get_player_inventory_player(&res)?; + self.add_inventory(InventoryProvider::PlayerMain(player)) + } + + async fn get_item_in_hand( + &mut self, + res: Resource, + hand: WitHand, + ) -> wasmtime::Result>> { + let player = self.get_player_inventory_player(&res)?; + let hand = from_wasm_hand(hand); + let stack = player.inventory().get_stack_in_hand(hand); + if stack.is_empty() { + Ok(None) + } else { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } + } + + async fn set_item_in_hand( + &mut self, + res: Resource, + hand: WitHand, + item: Option>, + ) -> wasmtime::Result<()> { + let player = self.get_player_inventory_player(&res)?; + let stack = if let Some(stack_res) = item { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + + let hand = from_wasm_hand(hand); + let slot = match hand { + pumpkin_util::Hand::Right => player.inventory().get_selected_slot() as usize, + pumpkin_util::Hand::Left => PlayerInventory::OFF_HAND_SLOT, + }; + + player.inventory().set_stack(slot, stack.clone()); + + // Sync to client + let stack_serializer = ItemStackSerializer::from(stack); + let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer); + player.send_client_packet(&packet).await; + + Ok(()) + } + + async fn get_selected_slot( + &mut self, + res: Resource, + ) -> wasmtime::Result { + let player = self.get_player_inventory_player(&res)?; + Ok(player.inventory().get_selected_slot()) + } + + async fn set_selected_slot( + &mut self, + res: Resource, + slot: u8, + ) -> wasmtime::Result<()> { + let player = self.get_player_inventory_player(&res)?; + if slot < 9 { + player.inventory().set_selected_slot(slot); + } + Ok(()) + } + + async fn get_helmet( + &mut self, + res: Resource, + ) -> wasmtime::Result>> { + let player = self.get_player_inventory_player(&res)?; + let stack = player.inventory().get_slot(39); + if stack.is_empty() { + Ok(None) + } else { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } + } + + async fn set_helmet( + &mut self, + res: Resource, + item: Option>, + ) -> wasmtime::Result<()> { + let player = self.get_player_inventory_player(&res)?; + let stack = if let Some(stack_res) = item { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + player.inventory().set_slot(39, stack.clone()); + let stack_serializer = ItemStackSerializer::from(stack); + let packet = CSetContainerSlot::new(0, 0, 5, &stack_serializer); + player.send_client_packet(&packet).await; + Ok(()) + } + + async fn get_chestplate( + &mut self, + res: Resource, + ) -> wasmtime::Result>> { + let player = self.get_player_inventory_player(&res)?; + let stack = player.inventory().get_slot(38); + if stack.is_empty() { + Ok(None) + } else { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } + } + + async fn set_chestplate( + &mut self, + res: Resource, + item: Option>, + ) -> wasmtime::Result<()> { + let player = self.get_player_inventory_player(&res)?; + let stack = if let Some(stack_res) = item { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + player.inventory().set_slot(38, stack.clone()); + let stack_serializer = ItemStackSerializer::from(stack); + let packet = CSetContainerSlot::new(0, 0, 6, &stack_serializer); + player.send_client_packet(&packet).await; + Ok(()) + } + + async fn get_leggings( + &mut self, + res: Resource, + ) -> wasmtime::Result>> { + let player = self.get_player_inventory_player(&res)?; + let stack = player.inventory().get_slot(37); + if stack.is_empty() { + Ok(None) + } else { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } + } + + async fn set_leggings( + &mut self, + res: Resource, + item: Option>, + ) -> wasmtime::Result<()> { + let player = self.get_player_inventory_player(&res)?; + let stack = if let Some(stack_res) = item { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + player.inventory().set_slot(37, stack.clone()); + let stack_serializer = ItemStackSerializer::from(stack); + let packet = CSetContainerSlot::new(0, 0, 7, &stack_serializer); + player.send_client_packet(&packet).await; + Ok(()) + } + + async fn get_boots( + &mut self, + res: Resource, + ) -> wasmtime::Result>> { + let player = self.get_player_inventory_player(&res)?; + let stack = player.inventory().get_slot(36); + if stack.is_empty() { + Ok(None) + } else { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } + } + + async fn set_boots( + &mut self, + res: Resource, + item: Option>, + ) -> wasmtime::Result<()> { + let player = self.get_player_inventory_player(&res)?; + let stack = if let Some(stack_res) = item { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + player.inventory().set_slot(36, stack.clone()); + let stack_serializer = ItemStackSerializer::from(stack); + let packet = CSetContainerSlot::new(0, 0, 8, &stack_serializer); + player.send_client_packet(&packet).await; + Ok(()) + } + + async fn get_off_hand( + &mut self, + res: Resource, + ) -> wasmtime::Result>> { + let player = self.get_player_inventory_player(&res)?; + let stack = player.inventory().get_slot(40); + if stack.is_empty() { + Ok(None) + } else { + Ok(Some(self.add_item_stack(Arc::new(Mutex::new(stack)))?)) + } + } + + async fn set_off_hand( + &mut self, + res: Resource, + item: Option>, + ) -> wasmtime::Result<()> { + let player = self.get_player_inventory_player(&res)?; + let stack = if let Some(stack_res) = item { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + player.inventory().set_slot(40, stack.clone()); + let stack_serializer = ItemStackSerializer::from(stack); + let packet = CSetContainerSlot::new(0, 0, 45, &stack_serializer); + player.send_client_packet(&packet).await; + Ok(()) + } + + async fn clear_armor(&mut self, res: Resource) -> wasmtime::Result<()> { + self.set_helmet(Resource::new_own(res.rep()), None).await?; + self.set_chestplate(Resource::new_own(res.rep()), None) + .await?; + self.set_leggings(Resource::new_own(res.rep()), None) + .await?; + self.set_boots(Resource::new_own(res.rep()), None).await?; + Ok(()) + } + + async fn clear_main(&mut self, res: Resource) -> wasmtime::Result<()> { + let inv = self.as_inventory(res).await?; + self.clear(inv).await + } + + async fn clear_all(&mut self, res: Resource) -> wasmtime::Result<()> { + self.clear_main(Resource::new_own(res.rep())).await?; + self.clear_armor(Resource::new_own(res.rep())).await?; + self.set_off_hand(Resource::new_own(res.rep()), None) + .await?; + Ok(()) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::(Resource::new_own(rep.rep())); + Ok(()) + } +} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/living_entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/living_entity.rs new file mode 100644 index 000000000..8d9de947f --- /dev/null +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/living_entity.rs @@ -0,0 +1,502 @@ +use std::sync::Arc; +use wasmtime::component::Resource; + +use crate::plugin::loader::wasm::wasm_host::{ + state::{LivingEntityResource, PluginHostState}, + wit::v0_1::pumpkin::plugin::{ + attributes::{ + Attribute, AttributeModifier as WitAttributeModifier, + ModifierOperation as WitModifierOperation, + }, + damage_types::DamageType as WitDamageType, + item_stack::ItemStack as WitHostItemStack, + text::TextComponent, + world::{ + Entity, EquipmentSlot as WitEquipmentSlot, HostLivingEntity, + LivingEntity as WitLivingEntity, Mob as WitMob, + }, + }, +}; + +pub fn living_entity_from_resource( + state: &PluginHostState, + entity: &Resource, +) -> wasmtime::Result> { + state + .resource_table + .get::(&Resource::new_own(entity.rep())) + .map_err(|_| wasmtime::Error::msg("invalid living entity resource handle")) + .map(|resource| resource.provider.clone()) +} + +#[must_use] +pub const fn from_wit_attribute(attr: Attribute) -> &'static pumpkin_data::attributes::Attributes { + use pumpkin_data::attributes::Attributes; + match attr { + Attribute::AirDragModifier => &Attributes::AIR_DRAG_MODIFIER, + Attribute::Armor => &Attributes::ARMOR, + Attribute::ArmorToughness => &Attributes::ARMOR_TOUGHNESS, + Attribute::AttackDamage => &Attributes::ATTACK_DAMAGE, + Attribute::AttackKnockback => &Attributes::ATTACK_KNOCKBACK, + Attribute::AttackSpeed => &Attributes::ATTACK_SPEED, + Attribute::BelowNameDistance => &Attributes::BELOW_NAME_DISTANCE, + Attribute::BlockBreakSpeed => &Attributes::BLOCK_BREAK_SPEED, + Attribute::BlockInteractionRange => &Attributes::BLOCK_INTERACTION_RANGE, + Attribute::Bounciness => &Attributes::BOUNCINESS, + Attribute::BurningTime => &Attributes::BURNING_TIME, + Attribute::CameraDistance => &Attributes::CAMERA_DISTANCE, + Attribute::ExplosionKnockbackResistance => &Attributes::EXPLOSION_KNOCKBACK_RESISTANCE, + Attribute::EntityInteractionRange => &Attributes::ENTITY_INTERACTION_RANGE, + Attribute::FallDamageMultiplier => &Attributes::FALL_DAMAGE_MULTIPLIER, + Attribute::FlyingSpeed => &Attributes::FLYING_SPEED, + Attribute::FollowRange => &Attributes::FOLLOW_RANGE, + Attribute::FrictionModifier => &Attributes::FRICTION_MODIFIER, + Attribute::Gravity => &Attributes::GRAVITY, + Attribute::JumpStrength => &Attributes::JUMP_STRENGTH, + Attribute::KnockbackResistance => &Attributes::KNOCKBACK_RESISTANCE, + Attribute::Luck => &Attributes::LUCK, + Attribute::MaxAbsorption => &Attributes::MAX_ABSORPTION, + Attribute::MaxHealth => &Attributes::MAX_HEALTH, + Attribute::MiningEfficiency => &Attributes::MINING_EFFICIENCY, + Attribute::MovementEfficiency => &Attributes::MOVEMENT_EFFICIENCY, + Attribute::MovementSpeed => &Attributes::MOVEMENT_SPEED, + Attribute::NameTagDistance => &Attributes::NAME_TAG_DISTANCE, + Attribute::OxygenBonus => &Attributes::OXYGEN_BONUS, + Attribute::SafeFallDistance => &Attributes::SAFE_FALL_DISTANCE, + Attribute::Scale => &Attributes::SCALE, + Attribute::SneakingSpeed => &Attributes::SNEAKING_SPEED, + Attribute::SpawnReinforcements => &Attributes::SPAWN_REINFORCEMENTS, + Attribute::StepHeight => &Attributes::STEP_HEIGHT, + Attribute::SubmergedMiningSpeed => &Attributes::SUBMERGED_MINING_SPEED, + Attribute::SweepingDamageRatio => &Attributes::SWEEPING_DAMAGE_RATIO, + Attribute::TemptRange => &Attributes::TEMPT_RANGE, + Attribute::WaterMovementEfficiency => &Attributes::WATER_MOVEMENT_EFFICIENCY, + Attribute::WaypointTransmitRange => &Attributes::WAYPOINT_TRANSMIT_RANGE, + Attribute::WaypointReceiveRange => &Attributes::WAYPOINT_RECEIVE_RANGE, + } +} + +#[must_use] +pub const fn from_wit_modifier_op( + op: WitModifierOperation, +) -> crate::entity::attributes::ModifierOperation { + match op { + WitModifierOperation::Add => crate::entity::attributes::ModifierOperation::Add, + WitModifierOperation::MultiplyBase => { + crate::entity::attributes::ModifierOperation::MultiplyBase + } + WitModifierOperation::MultiplyTotal => { + crate::entity::attributes::ModifierOperation::MultiplyTotal + } + } +} + +#[must_use] +pub const fn to_wit_modifier_op( + op: crate::entity::attributes::ModifierOperation, +) -> WitModifierOperation { + match op { + crate::entity::attributes::ModifierOperation::Add => WitModifierOperation::Add, + crate::entity::attributes::ModifierOperation::MultiplyBase => { + WitModifierOperation::MultiplyBase + } + crate::entity::attributes::ModifierOperation::MultiplyTotal => { + WitModifierOperation::MultiplyTotal + } + } +} + +#[must_use] +pub const fn from_wit_equipment_slot( + slot: WitEquipmentSlot, +) -> pumpkin_data::data_component_impl::EquipmentSlot { + use pumpkin_data::data_component_impl::EquipmentSlot; + match slot { + WitEquipmentSlot::MainHand => EquipmentSlot::MAIN_HAND, + WitEquipmentSlot::OffHand => EquipmentSlot::OFF_HAND, + WitEquipmentSlot::Feet => EquipmentSlot::FEET, + WitEquipmentSlot::Legs => EquipmentSlot::LEGS, + WitEquipmentSlot::Chest => EquipmentSlot::CHEST, + WitEquipmentSlot::Head => EquipmentSlot::HEAD, + WitEquipmentSlot::Body => EquipmentSlot::BODY, + WitEquipmentSlot::Saddle => EquipmentSlot::SADDLE, + } +} + +#[must_use] +pub const fn to_wit_damage_type(damage_type: &pumpkin_data::damage::DamageType) -> WitDamageType { + // SAFETY: WIT enum is generated in the same order as the internal enum / id + unsafe { std::mem::transmute(damage_type.id) } +} + +#[must_use] +pub fn from_wit_damage_type(wit: WitDamageType) -> pumpkin_data::damage::DamageType { + pumpkin_data::damage::DamageType::from_id(wit as u8) + .unwrap_or(pumpkin_data::damage::DamageType::GENERIC) +} + +impl HostLivingEntity for PluginHostState { + async fn as_entity( + &mut self, + this: Resource, + ) -> wasmtime::Result> { + let entity = living_entity_from_resource(self, &this)?; + self.add_entity(entity) + } + + async fn as_mob( + &mut self, + this: Resource, + ) -> wasmtime::Result>> { + let entity = living_entity_from_resource(self, &this)?; + if entity.get_mob().is_some() { + Ok(Some(self.add_mob(entity)?)) + } else { + Ok(None) + } + } + + async fn is_mob(&mut self, this: Resource) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + Ok(entity.get_mob().is_some()) + } + + async fn get_health(&mut self, this: Resource) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + Ok(entity + .get_living_entity() + .map_or(0.0, |living| living.health.load())) + } + + async fn set_health( + &mut self, + this: Resource, + health: f32, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + living.health.store(health); + } + Ok(()) + } + + async fn get_max_health(&mut self, this: Resource) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + Ok(entity + .get_living_entity() + .map_or(0.0, crate::entity::living::LivingEntity::get_max_health)) + } + + async fn set_max_health( + &mut self, + this: Resource, + max_health: f32, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + living.set_max_health(max_health); + } + Ok(()) + } + + async fn damage( + &mut self, + this: Resource, + amount: f32, + damage_type: WitDamageType, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + entity.damage(&*entity, amount, from_wit_damage_type(damage_type)); + Ok(()) + } + + async fn is_dead(&mut self, this: Resource) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + Ok(entity.get_living_entity().map_or_else( + || entity.get_entity().removal_reason.load().is_some(), + |living| living.dead.load(std::sync::atomic::Ordering::Relaxed), + )) + } + + async fn get_absorption(&mut self, this: Resource) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + Ok(entity + .get_living_entity() + .map_or(0.0, |living| living.absorption.load())) + } + + async fn set_absorption( + &mut self, + this: Resource, + amount: f32, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + living.absorption.store(amount); + } + Ok(()) + } + + async fn get_attribute_value( + &mut self, + this: Resource, + attr: Attribute, + ) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + let attribute = from_wit_attribute(attr); + Ok(entity + .get_living_entity() + .map_or(attribute.default_value, |living| { + living.get_attribute_value(attribute) + })) + } + + async fn get_attribute_base( + &mut self, + this: Resource, + attr: Attribute, + ) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + let attribute = from_wit_attribute(attr); + Ok(entity + .get_living_entity() + .map_or(attribute.default_value, |living| { + living.get_attribute_base(attribute) + })) + } + + async fn set_attribute_base( + &mut self, + this: Resource, + attr: Attribute, + value: f64, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + living.set_attribute_base(attribute, value); + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ); + } + Ok(()) + } + + async fn add_attribute_modifier( + &mut self, + this: Resource, + attr: Attribute, + modifier: WitAttributeModifier, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + let internal_mod = crate::entity::attributes::Modifier { + id: modifier.id, + amount: modifier.amount, + operation: from_wit_modifier_op(modifier.operation), + }; + living.update_attribute(attribute, |inst| inst.add_or_replace_modifier(internal_mod)); + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ); + } + Ok(()) + } + + async fn remove_attribute_modifier( + &mut self, + this: Resource, + attr: Attribute, + id: String, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + living.update_attribute(attribute, |inst| inst.remove_modifier(&id)); + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ); + } + Ok(()) + } + + async fn get_attribute_modifiers( + &mut self, + this: Resource, + attr: Attribute, + ) -> wasmtime::Result> { + let entity = living_entity_from_resource(self, &this)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + let map = living + .attributes + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(inst) = map.get(&attribute.id) { + return Ok(inst + .modifiers + .iter() + .map(|m| WitAttributeModifier { + id: m.id.clone(), + amount: m.amount, + operation: to_wit_modifier_op(m.operation), + }) + .collect()); + } + } + Ok(Vec::new()) + } + + async fn reset_attribute( + &mut self, + this: Resource, + attr: Attribute, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + { + let mut map = living + .attributes + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + map.remove(&attribute.id); + }; + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ); + } + Ok(()) + } + + async fn reset_all_attributes( + &mut self, + this: Resource, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + living.reset_effects_and_attributes(); + } + Ok(()) + } + + async fn get_equipment( + &mut self, + this: Resource, + slot: WitEquipmentSlot, + ) -> wasmtime::Result>> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + let slot = from_wit_equipment_slot(slot); + let equipment = living + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let stack = equipment.get(&slot); + if !stack.is_empty() { + return Ok(Some( + self.add_item_stack(Arc::new(tokio::sync::Mutex::new(stack)))?, + )); + } + } + Ok(None) + } + + async fn set_equipment( + &mut self, + this: Resource, + slot: WitEquipmentSlot, + stack: Option>, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + let slot = from_wit_equipment_slot(slot); + let item_stack = if let Some(stack_res) = stack { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + + { + let mut equipment = living + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + equipment.put(&slot, item_stack.clone()); + }; + + living.send_equipment_changes(&[(slot, item_stack)]); + } + Ok(()) + } + + async fn clear_equipment(&mut self, this: Resource) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + let mut equipment = living + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let slots_to_clear: Vec<( + pumpkin_data::data_component_impl::EquipmentSlot, + pumpkin_data::item_stack::ItemStack, + )> = equipment + .equipment + .drain() + .map(|(slot, _)| (slot, pumpkin_data::item_stack::ItemStack::EMPTY.clone())) + .collect(); + drop(equipment); + + living.send_equipment_changes(&slots_to_clear); + } + Ok(()) + } + + async fn get_age(&mut self, this: Resource) -> wasmtime::Result { + let entity = living_entity_from_resource(self, &this)?; + Ok(entity.get_living_entity().map_or(0, |living| { + living.entity.age.load(std::sync::atomic::Ordering::Relaxed) + })) + } + + async fn set_age(&mut self, this: Resource, age: i32) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(living) = entity.get_living_entity() { + living + .entity + .age + .store(age, std::sync::atomic::Ordering::Relaxed); + } + Ok(()) + } + + async fn send_system_message( + &mut self, + this: Resource, + message: Resource, + ) -> wasmtime::Result<()> { + let entity = living_entity_from_resource(self, &this)?; + if let Some(player) = entity.get_player() { + let text_res = self + .resource_table + .get::( + &Resource::new_own(message.rep()), + ) + .map_err(|_| wasmtime::Error::msg("invalid text component resource handle"))?; + player.send_system_message(&text_res.provider); + } + Ok(()) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::(Resource::new_own(rep.rep())); + Ok(()) + } +} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs new file mode 100644 index 000000000..baadc501b --- /dev/null +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs @@ -0,0 +1,941 @@ +use std::sync::Arc; +use wasmtime::component::Resource; + +use pumpkin_util::math::vector3::Vector3; + +use crate::entity::ai::goal::Goal; +use crate::entity::mob::Mob as InternalMob; +use crate::entity::passive::tamable::TamableAnimal; +use crate::plugin::loader::wasm::wasm_host::{ + PluginInstance, WasmPlugin, + state::{MobResource, PluginHostState}, + wit::v0_1::entity::entity_from_resource, + wit::v0_1::pumpkin::plugin::{ + common::Position, + uuid::Uuid, + world::{ + AgeableData as WitAgeableData, BlockDirection as WitBlockDirection, + CatData as WitCatData, CreeperData as WitCreeperData, DyeColor as WitDyeColor, + EndermanData as WitEndermanData, Entity, FoxData as WitFoxData, HostMob, + IronGolemData as WitIronGolemData, LivingEntity as WitLivingEntity, Mob as WitMob, + MobData as WitMobData, PathNodeType as WitPathNodeType, SheepData as WitSheepData, + ShulkerData as WitShulkerData, SlimeData as WitSlimeData, + VillagerData as WitVillagerData, VillagerProfession as WitVillagerProfession, + WolfData as WitWolfData, ZombieData as WitZombieData, + }, + }, + wit::v0_1::uuid::UuidExt, +}; + +pub fn mob_from_resource( + state: &PluginHostState, + entity: &Resource, +) -> wasmtime::Result> { + state + .resource_table + .get::(&Resource::new_own(entity.rep())) + .map_err(|_| wasmtime::Error::msg("invalid mob resource handle")) + .map(|resource| resource.provider.clone()) +} + +#[must_use] +pub const fn from_wit_path_node_type( + t: WitPathNodeType, +) -> crate::entity::ai::pathfinder::node::PathType { + use crate::entity::ai::pathfinder::node::PathType; + match t { + WitPathNodeType::Blocked => PathType::Blocked, + WitPathNodeType::Open => PathType::Open, + WitPathNodeType::Walkable => PathType::Walkable, + WitPathNodeType::WalkableDoor => PathType::WalkableDoor, + WitPathNodeType::Trapdoor => PathType::Trapdoor, + WitPathNodeType::PowderSnow => PathType::PowderSnow, + WitPathNodeType::DangerPowderSnow => PathType::DangerPowderSnow, + WitPathNodeType::Fence => PathType::Fence, + WitPathNodeType::Lava => PathType::Lava, + WitPathNodeType::Water => PathType::Water, + WitPathNodeType::WaterBorder => PathType::WaterBorder, + WitPathNodeType::Rail => PathType::Rail, + WitPathNodeType::UnpassableRail => PathType::UnpassableRail, + WitPathNodeType::DangerFire => PathType::DangerFire, + WitPathNodeType::DamageFire => PathType::DamageFire, + WitPathNodeType::DangerOther => PathType::DangerOther, + WitPathNodeType::DamageOther => PathType::DamageOther, + WitPathNodeType::DoorOpen => PathType::DoorOpen, + WitPathNodeType::DoorWoodClosed => PathType::DoorWoodClosed, + WitPathNodeType::DoorIronClosed => PathType::DoorIronClosed, + WitPathNodeType::Breach => PathType::Breach, + WitPathNodeType::Leaves => PathType::Leaves, + WitPathNodeType::StickyHoney => PathType::StickyHoney, + WitPathNodeType::Cocoa => PathType::Cocoa, + WitPathNodeType::DamageCautious => PathType::DamageCautious, + WitPathNodeType::DangerTrapdoor => PathType::DangerTrapdoor, + } +} + +#[must_use] +pub const fn to_wit_dye_color(color: u8) -> WitDyeColor { + match color { + 0 => WitDyeColor::White, + 1 => WitDyeColor::Orange, + 2 => WitDyeColor::Magenta, + 3 => WitDyeColor::LightBlue, + 4 => WitDyeColor::Yellow, + 5 => WitDyeColor::Lime, + 6 => WitDyeColor::Pink, + 7 => WitDyeColor::Gray, + 8 => WitDyeColor::LightGray, + 9 => WitDyeColor::Cyan, + 10 => WitDyeColor::Purple, + 11 => WitDyeColor::Blue, + 12 => WitDyeColor::Brown, + 13 => WitDyeColor::Green, + 14 => WitDyeColor::Red, + _ => WitDyeColor::Black, + } +} + +#[must_use] +pub const fn from_wit_dye_color(color: WitDyeColor) -> u8 { + match color { + WitDyeColor::White => 0, + WitDyeColor::Orange => 1, + WitDyeColor::Magenta => 2, + WitDyeColor::LightBlue => 3, + WitDyeColor::Yellow => 4, + WitDyeColor::Lime => 5, + WitDyeColor::Pink => 6, + WitDyeColor::Gray => 7, + WitDyeColor::LightGray => 8, + WitDyeColor::Cyan => 9, + WitDyeColor::Purple => 10, + WitDyeColor::Blue => 11, + WitDyeColor::Brown => 12, + WitDyeColor::Green => 13, + WitDyeColor::Red => 14, + WitDyeColor::Black => 15, + } +} + +#[must_use] +pub const fn to_wit_villager_profession(prof_id: i32) -> WitVillagerProfession { + let prof = match pumpkin_data::villager::VillagerProfession::from_i32(prof_id) { + Some(p) => p, + None => pumpkin_data::villager::VillagerProfession::None, + }; + match prof { + pumpkin_data::villager::VillagerProfession::None => WitVillagerProfession::None, + pumpkin_data::villager::VillagerProfession::Armorer => WitVillagerProfession::Armorer, + pumpkin_data::villager::VillagerProfession::Butcher => WitVillagerProfession::Butcher, + pumpkin_data::villager::VillagerProfession::Cartographer => { + WitVillagerProfession::Cartographer + } + pumpkin_data::villager::VillagerProfession::Cleric => WitVillagerProfession::Cleric, + pumpkin_data::villager::VillagerProfession::Farmer => WitVillagerProfession::Farmer, + pumpkin_data::villager::VillagerProfession::Fisherman => WitVillagerProfession::Fisherman, + pumpkin_data::villager::VillagerProfession::Fletcher => WitVillagerProfession::Fletcher, + pumpkin_data::villager::VillagerProfession::Leatherworker => { + WitVillagerProfession::Leatherworker + } + pumpkin_data::villager::VillagerProfession::Librarian => WitVillagerProfession::Librarian, + pumpkin_data::villager::VillagerProfession::Mason => WitVillagerProfession::Mason, + pumpkin_data::villager::VillagerProfession::Nitwit => WitVillagerProfession::Nitwit, + pumpkin_data::villager::VillagerProfession::Shepherd => WitVillagerProfession::Shepherd, + pumpkin_data::villager::VillagerProfession::Toolsmith => WitVillagerProfession::Toolsmith, + pumpkin_data::villager::VillagerProfession::Weaponsmith => { + WitVillagerProfession::Weaponsmith + } + } +} + +#[must_use] +pub const fn from_wit_villager_profession( + prof: WitVillagerProfession, +) -> pumpkin_data::villager::VillagerProfession { + match prof { + WitVillagerProfession::None => pumpkin_data::villager::VillagerProfession::None, + WitVillagerProfession::Armorer => pumpkin_data::villager::VillagerProfession::Armorer, + WitVillagerProfession::Butcher => pumpkin_data::villager::VillagerProfession::Butcher, + WitVillagerProfession::Cartographer => { + pumpkin_data::villager::VillagerProfession::Cartographer + } + WitVillagerProfession::Cleric => pumpkin_data::villager::VillagerProfession::Cleric, + WitVillagerProfession::Farmer => pumpkin_data::villager::VillagerProfession::Farmer, + WitVillagerProfession::Fisherman => pumpkin_data::villager::VillagerProfession::Fisherman, + WitVillagerProfession::Fletcher => pumpkin_data::villager::VillagerProfession::Fletcher, + WitVillagerProfession::Leatherworker => { + pumpkin_data::villager::VillagerProfession::Leatherworker + } + WitVillagerProfession::Librarian => pumpkin_data::villager::VillagerProfession::Librarian, + WitVillagerProfession::Mason => pumpkin_data::villager::VillagerProfession::Mason, + WitVillagerProfession::Nitwit => pumpkin_data::villager::VillagerProfession::Nitwit, + WitVillagerProfession::Shepherd => pumpkin_data::villager::VillagerProfession::Shepherd, + WitVillagerProfession::Toolsmith => pumpkin_data::villager::VillagerProfession::Toolsmith, + WitVillagerProfession::Weaponsmith => { + pumpkin_data::villager::VillagerProfession::Weaponsmith + } + } +} + +#[must_use] +pub const fn to_wit_block_direction(dir: pumpkin_data::BlockDirection) -> WitBlockDirection { + match dir { + pumpkin_data::BlockDirection::Down => WitBlockDirection::Down, + pumpkin_data::BlockDirection::Up => WitBlockDirection::Up, + pumpkin_data::BlockDirection::North => WitBlockDirection::North, + pumpkin_data::BlockDirection::South => WitBlockDirection::South, + pumpkin_data::BlockDirection::West => WitBlockDirection::West, + pumpkin_data::BlockDirection::East => WitBlockDirection::East, + } +} + +#[must_use] +pub const fn from_wit_block_direction(dir: WitBlockDirection) -> pumpkin_data::BlockDirection { + match dir { + WitBlockDirection::Down => pumpkin_data::BlockDirection::Down, + WitBlockDirection::Up => pumpkin_data::BlockDirection::Up, + WitBlockDirection::North => pumpkin_data::BlockDirection::North, + WitBlockDirection::South => pumpkin_data::BlockDirection::South, + WitBlockDirection::West => pumpkin_data::BlockDirection::West, + WitBlockDirection::East => pumpkin_data::BlockDirection::East, + } +} + +pub struct CustomWasmGoal { + pub plugin: Arc, + pub goal_id: u32, +} + +fn current_mob_entity(mob: &dyn InternalMob) -> Option> { + let entity = mob.get_entity(); + entity.world.load().get_entity_by_id(entity.entity_id) +} + +impl Goal for CustomWasmGoal { + fn can_start(&mut self, _mob: &dyn InternalMob) -> bool { + false + } + + fn should_continue(&self, _mob: &dyn InternalMob) -> bool { + false + } + + fn start(&mut self, mob: &dyn InternalMob) { + if let Some(entity_arc) = current_mob_entity(mob) { + let plugin = self.plugin.clone(); + let goal_id = self.goal_id; + tokio::spawn(async move { + let mut store = plugin.store.lock().await; + match plugin.plugin_instance { + PluginInstance::V0_1(ref plugin_inst) => { + let Some(server) = store.data_mut().server.clone() else { + return; + }; + let Ok(server_res) = store.data_mut().add_server(server) else { + return; + }; + let Ok(entity_res) = store.data_mut().add_entity(entity_arc) else { + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(server_res.rep()), + ); + return; + }; + let server_rep = server_res.rep(); + let entity_rep = entity_res.rep(); + let _ = plugin_inst + .call_handle_ai_goal_start(&mut *store, goal_id, server_res, entity_res) + .await; + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(server_rep), + ); + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(entity_rep), + ); + } + } + }); + } + } + + fn tick(&mut self, mob: &dyn InternalMob) { + if let Some(entity_arc) = current_mob_entity(mob) { + let plugin = self.plugin.clone(); + let goal_id = self.goal_id; + tokio::spawn(async move { + let mut store = plugin.store.lock().await; + match plugin.plugin_instance { + PluginInstance::V0_1(ref plugin_inst) => { + let Some(server) = store.data_mut().server.clone() else { + return; + }; + let Ok(server_res) = store.data_mut().add_server(server) else { + return; + }; + let Ok(entity_res) = store.data_mut().add_entity(entity_arc) else { + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(server_res.rep()), + ); + return; + }; + let server_rep = server_res.rep(); + let entity_rep = entity_res.rep(); + let _ = plugin_inst + .call_handle_ai_goal_tick(&mut *store, goal_id, server_res, entity_res) + .await; + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(server_rep), + ); + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(entity_rep), + ); + } + } + }); + } + } + + fn stop(&mut self, mob: &dyn InternalMob) { + if let Some(entity_arc) = current_mob_entity(mob) { + let plugin = self.plugin.clone(); + let goal_id = self.goal_id; + tokio::spawn(async move { + let mut store = plugin.store.lock().await; + match plugin.plugin_instance { + PluginInstance::V0_1(ref plugin_inst) => { + let Some(server) = store.data_mut().server.clone() else { + return; + }; + let Ok(server_res) = store.data_mut().add_server(server) else { + return; + }; + let Ok(entity_res) = store.data_mut().add_entity(entity_arc) else { + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(server_res.rep()), + ); + return; + }; + let server_rep = server_res.rep(); + let entity_rep = entity_res.rep(); + let _ = plugin_inst + .call_handle_ai_goal_stop(&mut *store, goal_id, server_res, entity_res) + .await; + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(server_rep), + ); + let _ = store + .data_mut() + .resource_table + .delete::( + wasmtime::component::Resource::new_own(entity_rep), + ); + } + } + }); + } + } +} + +impl HostMob for PluginHostState { + async fn as_entity(&mut self, this: Resource) -> wasmtime::Result> { + let entity = mob_from_resource(self, &this)?; + self.add_entity(entity) + } + + async fn as_living( + &mut self, + this: Resource, + ) -> wasmtime::Result> { + let entity = mob_from_resource(self, &this)?; + self.add_living_entity(entity) + } + + async fn add_ai_goal( + &mut self, + this: Resource, + priority: u8, + goal: crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal, + ) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + let mob_entity = mob.get_mob_entity(); + match goal { + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::Swim => { + mob_entity.add_goal(priority, crate::entity::ai::goal::swim::SwimGoal::default()); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::WanderAround(speed) => { + mob_entity.add_goal(priority, crate::entity::ai::goal::wander_around::WanderAroundGoal::new(f64::from(speed))); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::MeleeAttack(speed) => { + mob_entity.add_goal(priority, crate::entity::ai::goal::melee_attack::MeleeAttackGoal::new(f64::from(speed), false)); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::LookAtPlayer(range) => { + mob_entity.add_goal(priority, crate::entity::ai::goal::look_at_entity::LookAtEntityGoal::new( + std::sync::Weak::::new() as std::sync::Weak, + &pumpkin_data::entity::EntityType::PLAYER, + range, + 0.02, + false, + )); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::LookAround => { + mob_entity.add_goal(priority, crate::entity::ai::goal::look_around::RandomLookAroundGoal::default()); + } + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::BuiltinAiGoal::EscapeDanger(speed) => { + mob_entity.add_goal(priority, *crate::entity::ai::goal::escape_danger::EscapeDangerGoal::new(f64::from(speed))); + } + _ => {} + } + } + Ok(()) + } + + async fn add_custom_ai_goal( + &mut self, + this: Resource, + priority: u8, + goal_id: u32, + ) -> wasmtime::Result<()> { + let Some(plugin) = self.plugin.as_ref().and_then(std::sync::Weak::upgrade) else { + return Err(wasmtime::Error::msg("Plugin not active")); + }; + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + let mob_entity = mob.get_mob_entity(); + mob_entity.add_goal(priority, CustomWasmGoal { plugin, goal_id }); + } + Ok(()) + } + + async fn clear_ai_goals(&mut self, this: Resource) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity().clear_ai_goals(mob); + } + Ok(()) + } + + async fn set_ai_disabled( + &mut self, + this: Resource, + disabled: bool, + ) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity().set_no_ai(disabled); + } + Ok(()) + } + + async fn is_ai_disabled(&mut self, this: Resource) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + Ok(entity + .get_mob() + .is_none_or(|mob| mob.get_mob_entity().is_no_ai())) + } + + async fn set_target( + &mut self, + this: Resource, + target: Option>, + ) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + let target_entity = target.map(|t| entity_from_resource(self, &t)).transpose()?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity().set_target(target_entity); + } + Ok(()) + } + + async fn get_target( + &mut self, + this: Resource, + ) -> wasmtime::Result>> { + let entity = mob_from_resource(self, &this)?; + if let Some(target) = entity + .get_mob() + .and_then(|mob| mob.get_mob_entity().get_target()) + { + return Ok(Some(self.add_entity(target)?)); + } + Ok(None) + } + + async fn navigate_to_pos( + &mut self, + this: Resource, + pos: Position, + speed: f64, + ) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + Ok(entity.get_mob().is_some_and(|mob| { + let mob_pos = entity.get_entity().pos.load(); + let dest = Vector3::new(pos.0, pos.1, pos.2); + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .set_progress(crate::entity::ai::pathfinder::NavigatorGoal::new( + mob_pos, dest, speed, + )); + true + })) + } + + async fn navigate_to_entity( + &mut self, + this: Resource, + target: Resource, + speed: f64, + ) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + let target_entity = entity_from_resource(self, &target)?; + Ok(entity.get_mob().is_some_and(|mob| { + let mob_pos = entity.get_entity().pos.load(); + let target_pos = target_entity.get_entity().pos.load(); + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .set_progress(crate::entity::ai::pathfinder::NavigatorGoal::new( + mob_pos, target_pos, speed, + )); + mob.get_mob_entity() + .look_control + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .look_at_entity(mob, &target_entity); + true + })) + } + + async fn stop_navigation(&mut self, this: Resource) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .stop(); + } + Ok(()) + } + + async fn is_navigating(&mut self, this: Resource) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + Ok(entity.get_mob().is_some_and(|mob| { + let is_idle = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_idle + .load(std::sync::atomic::Ordering::Relaxed); + !is_idle + })) + } + + async fn has_reached_destination(&mut self, this: Resource) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + Ok(entity.get_mob().is_none_or(|mob| { + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_idle + .load(std::sync::atomic::Ordering::Relaxed) + })) + } + + async fn set_navigation_speed( + &mut self, + this: Resource, + speed: f64, + ) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .set_speed(speed); + } + Ok(()) + } + + async fn can_reach( + &mut self, + this: Resource, + pos: Position, + max_distance: f32, + ) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + Ok(entity.get_mob().is_some_and(|mob| { + let living = &mob.get_mob_entity().living_entity; + let dest = Vector3::new(pos.0, pos.1, pos.2); + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .can_reach_within(living, dest, max_distance) + })) + } + + async fn set_pathfinding_malus( + &mut self, + this: Resource, + node_type: WitPathNodeType, + malus: f32, + ) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + let internal_type = from_wit_path_node_type(node_type); + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .set_pathfinding_malus(internal_type, malus); + } + Ok(()) + } + + async fn get_pathfinding_malus( + &mut self, + this: Resource, + node_type: WitPathNodeType, + ) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + Ok(entity.get_mob().map_or(0.0, |mob| { + let internal_type = from_wit_path_node_type(node_type); + mob.get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .get_pathfinding_malus(internal_type) + })) + } + + async fn look_at(&mut self, this: Resource, pos: Position) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity() + .look_control + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .look_at(mob, pos.0, pos.1, pos.2); + } + Ok(()) + } + + async fn look_at_entity( + &mut self, + this: Resource, + target: Resource, + ) -> wasmtime::Result<()> { + let entity = mob_from_resource(self, &this)?; + let target_entity = entity_from_resource(self, &target)?; + if let Some(mob) = entity.get_mob() { + mob.get_mob_entity() + .look_control + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .look_at_entity(mob, &target_entity); + } + Ok(()) + } + + #[allow(clippy::too_many_lines)] + async fn get_mob_data(&mut self, this: Resource) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + let any = entity.cast_any(); + + if let Some(sheep) = any.downcast_ref::() { + return Ok(WitMobData::Sheep(WitSheepData { + color: to_wit_dye_color(sheep.get_color()), + is_sheared: sheep.is_sheared(), + })); + } + + if let Some(wolf) = any.downcast_ref::() { + return Ok(WitMobData::Wolf(WitWolfData { + is_tamed: wolf.is_tame(), + owner: wolf.get_owner().map(|u| Uuid::to_wit(&u)), + is_sitting: wolf.is_in_sitting_pose(), + collar_color: to_wit_dye_color(wolf.get_collar_color()), + is_angry: false, + is_begging: false, + })); + } + + if let Some(cat) = any.downcast_ref::() { + return Ok(WitMobData::Cat(WitCatData { + is_tamed: cat.is_tame(), + owner: cat.get_owner().map(|u| Uuid::to_wit(&u)), + is_sitting: cat.is_in_sitting_pose(), + collar_color: to_wit_dye_color(cat.get_collar_color()), + })); + } + + if let Some(villager) = + any.downcast_ref::() + { + let data = *villager + .villager_data + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + return Ok(WitMobData::Villager(WitVillagerData { + profession: to_wit_villager_profession(data.profession.0), + level: (data.level.0).clamp(0, 255) as u8, + experience: villager + .xp + .load(std::sync::atomic::Ordering::Relaxed) + .max(0) as u32, + })); + } + + if let Some(creeper) = any.downcast_ref::() { + return Ok(WitMobData::Creeper(WitCreeperData { + is_powered: creeper.is_charged(), + fuse: creeper.get_fuse(), + is_ignited: creeper.is_ignited(), + explosion_radius: creeper.get_explosion_radius().clamp(0, 255) as u8, + })); + } + + if let Some(slime) = any.downcast_ref::() { + return Ok(WitMobData::Slime(WitSlimeData { + size: slime.get_size(), + })); + } + + if let Some(enderman) = any.downcast_ref::() { + return Ok(WitMobData::Enderman(WitEndermanData { + carried_block_state: enderman + .get_carried_block() + .map(pumpkin_data::BlockStateId::as_u16), + is_screaming: enderman.is_angry(), + is_staring: enderman.is_angry(), + })); + } + + if let Some(iron_golem) = + any.downcast_ref::() + { + return Ok(WitMobData::IronGolem(WitIronGolemData { + is_player_created: iron_golem.is_player_created(), + })); + } + + if let Some(fox) = any.downcast_ref::() { + return Ok(WitMobData::Fox(WitFoxData { + is_sitting: fox.is_sitting(), + is_sleeping: fox.is_sleeping(), + is_crouching: fox.is_crouching(), + })); + } + + if let Some(shulker) = any.downcast_ref::() { + return Ok(WitMobData::Shulker(WitShulkerData { + attached_face: to_wit_block_direction(shulker.get_attach_face()), + peek_amount: shulker.get_raw_peek(), + color: shulker.get_color().map(to_wit_dye_color), + })); + } + + if let Some(zombie) = any.downcast_ref::() + { + return Ok(WitMobData::Zombie(WitZombieData { + is_baby: zombie.is_baby(), + can_break_doors: zombie.can_break_doors(), + })); + } + + if let Some(living) = entity.get_living_entity() { + let age = living.entity.age.load(std::sync::atomic::Ordering::Relaxed); + return Ok(WitMobData::Ageable(WitAgeableData { + is_baby: age < 0, + age, + in_love_ticks: 0, + })); + } + + Ok(WitMobData::Generic) + } + + #[allow(clippy::too_many_lines)] + async fn set_mob_data( + &mut self, + this: Resource, + data: WitMobData, + ) -> wasmtime::Result { + let entity = mob_from_resource(self, &this)?; + let any = entity.cast_any(); + + match data { + WitMobData::Sheep(sheep_data) => { + if let Some(sheep) = + any.downcast_ref::() + { + sheep.set_color(from_wit_dye_color(sheep_data.color)); + sheep.set_sheared(sheep_data.is_sheared); + return Ok(true); + } + } + WitMobData::Wolf(wolf_data) => { + if let Some(wolf) = any.downcast_ref::() { + wolf.set_tame(wolf_data.is_tamed); + wolf.set_owner(wolf_data.owner.map(|u| Uuid::from_wit(&u))); + wolf.set_in_sitting_pose(wolf_data.is_sitting); + wolf.set_collar_color(from_wit_dye_color(wolf_data.collar_color)); + return Ok(true); + } + } + WitMobData::Cat(cat_data) => { + if let Some(cat) = any.downcast_ref::() { + cat.set_tame( + cat_data.is_tamed, + cat_data.owner.map(|u| Uuid::from_wit(&u)), + ); + cat.set_sitting(cat_data.is_sitting); + cat.set_collar_color(from_wit_dye_color(cat_data.collar_color)); + return Ok(true); + } + } + WitMobData::Villager(villager_data) => { + if let Some(villager) = + any.downcast_ref::() + { + { + let mut vdata = villager + .villager_data + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + vdata.profession = pumpkin_protocol::codec::var_int::VarInt( + from_wit_villager_profession(villager_data.profession) as i32, + ); + vdata.level = pumpkin_protocol::codec::var_int::VarInt(i32::from( + villager_data.level, + )); + }; + villager.xp.store( + villager_data.experience as i32, + std::sync::atomic::Ordering::Relaxed, + ); + return Ok(true); + } + } + WitMobData::Creeper(creeper_data) => { + if let Some(creeper) = + any.downcast_ref::() + { + creeper.set_charged(creeper_data.is_powered); + creeper.set_fuse(creeper_data.fuse); + creeper.set_ignited(creeper_data.is_ignited); + creeper.set_explosion_radius(i32::from(creeper_data.explosion_radius)); + return Ok(true); + } + } + WitMobData::Slime(slime_data) => { + if let Some(slime) = any.downcast_ref::() { + slime.set_size(slime_data.size, false); + return Ok(true); + } + } + WitMobData::Enderman(enderman_data) => { + if let Some(enderman) = + any.downcast_ref::() + { + enderman.set_carried_block( + enderman_data + .carried_block_state + .and_then(pumpkin_data::BlockStateId::new), + ); + enderman.set_angry(enderman_data.is_screaming || enderman_data.is_staring); + return Ok(true); + } + } + WitMobData::IronGolem(iron_golem_data) => { + if let Some(iron_golem) = + any.downcast_ref::() + { + iron_golem.set_player_created(iron_golem_data.is_player_created); + return Ok(true); + } + } + WitMobData::Fox(fox_data) => { + if let Some(fox) = any.downcast_ref::() { + fox.set_sitting(fox_data.is_sitting); + fox.set_sleeping(fox_data.is_sleeping); + fox.set_crouching(fox_data.is_crouching); + return Ok(true); + } + } + WitMobData::Shulker(shulker_data) => { + if let Some(shulker) = + any.downcast_ref::() + { + shulker.set_attach_face(from_wit_block_direction(shulker_data.attached_face)); + shulker.set_raw_peek(shulker_data.peek_amount); + shulker.set_color(shulker_data.color.map(from_wit_dye_color)); + return Ok(true); + } + } + WitMobData::Zombie(zombie_data) => { + if let Some(zombie) = + any.downcast_ref::() + { + zombie.set_baby(zombie_data.is_baby); + zombie.set_can_break_doors(zombie_data.can_break_doors); + return Ok(true); + } + } + WitMobData::Ageable(ageable_data) => { + if let Some(living) = entity.get_living_entity() { + let age = if ageable_data.is_baby && ageable_data.age >= 0 { + -24000 + } else { + ageable_data.age + }; + living + .entity + .age + .store(age, std::sync::atomic::Ordering::Relaxed); + return Ok(true); + } + } + WitMobData::Generic => return Ok(true), + } + + Ok(false) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::(Resource::new_own(rep.rep())); + Ok(()) + } +} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs index cf56b8cc9..927283348 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs @@ -22,6 +22,8 @@ pub mod common; #[allow(clippy::unused_async_trait_impl)] pub mod context; #[allow(clippy::unused_async_trait_impl)] +pub mod datapack; +#[allow(clippy::unused_async_trait_impl)] pub mod display; #[allow(clippy::unused_async_trait_impl)] pub mod enchantment; @@ -34,12 +36,18 @@ pub mod generated_packets; pub mod gui; #[allow(clippy::unused_async_trait_impl)] pub mod i18n; +#[allow(clippy::unused_async_trait_impl)] +pub mod inventory; pub mod ipc; #[allow(clippy::unused_async_trait_impl)] pub mod item_stack; pub mod java_dialogs; #[allow(clippy::unused_async_trait_impl)] +pub mod living_entity; +#[allow(clippy::unused_async_trait_impl)] pub mod logging; +#[allow(clippy::unused_async_trait_impl)] +pub mod mob; pub mod permission; #[allow(clippy::unused_async_trait_impl)] pub mod player; diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 19c5ce6c0..a50a41df3 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -15,10 +15,10 @@ use crate::{ GuiResource, PlayerResource, PluginHostState, TextComponentResource, WorldResource, }, wit::v0_1::{ - entity::from_wit_damage_type, events::{ from_wasm_game_mode, from_wasm_position, to_wasm_game_mode, to_wasm_position, }, + living_entity::from_wit_damage_type, pumpkin::{ self, plugin::damage_types::DamageType as WitDamageType, @@ -1136,6 +1136,34 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(()) } + async fn get_inventory( + &mut self, + player: Resource, + ) -> wasmtime::Result< + Resource< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::inventory::PlayerInventory, + >, + >{ + let player = player_from_resource(self, &player)?; + self.add_player_inventory(player) + } + + async fn get_ender_chest( + &mut self, + player: Resource, + ) -> wasmtime::Result< + Resource< + crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::inventory::Inventory, + >, + >{ + let player = player_from_resource(self, &player)?; + self.add_inventory( + crate::plugin::loader::wasm::wasm_host::state::InventoryProvider::PlayerEnderChest( + player, + ), + ) + } + async fn get_inventory_item( &mut self, player: Resource, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs index ccbdd40dc..63ddfa3d4 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs @@ -19,6 +19,7 @@ use crate::plugin::{ wit::v0_1::pumpkin::{ self, plugin::{ + datapack::DatapackManager as WitDatapackManager, player::{BanIpOptions, BanPlayerOptions, Player}, server::{ BanManager as WitBanManager, BannedIpEntry, BannedPlayerEntry, Difficulty, @@ -606,6 +607,17 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { Ok(ids) } + async fn get_datapack_manager( + &mut self, + _rep: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + self.add_datapack_manager(server.clone()) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { self.resource_table .delete::(Resource::new_own(rep.rep())) diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs index d61671854..953cde1cc 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs @@ -2231,7 +2231,7 @@ impl WasmChunkGenerator { proto_chunk, }; - futures::executor::block_on(async { + let run = async { let mut store = self.plugin.store.lock().await; let Ok(buffer_res) = store.data_mut().add_chunk_buffer(chunk_buffer) else { return; @@ -2257,7 +2257,13 @@ impl WasmChunkGenerator { ); } } - }); + }; + + if let Ok(handle) = tokio::runtime::Handle::try_current() { + tokio::task::block_in_place(|| { + handle.block_on(run); + }); + } } } diff --git a/crates/pumpkin/src/plugin/mod.rs b/crates/pumpkin/src/plugin/mod.rs index c1d5d0642..ce6171ec9 100644 --- a/crates/pumpkin/src/plugin/mod.rs +++ b/crates/pumpkin/src/plugin/mod.rs @@ -1009,8 +1009,8 @@ impl PluginManager { /// Checks if plugin active #[must_use] - pub async fn is_plugin_active(&self, name: &str) -> bool { - let plugins = self.plugins.read().await; + pub fn is_plugin_active(&self, name: &str) -> bool { + let plugins = self.plugins.blocking_read(); plugins .iter() .any(|p| p.metadata.name == name && p.is_active && p.instance.is_some()) @@ -1018,8 +1018,8 @@ impl PluginManager { /// Get list of active plugins #[must_use] - pub async fn active_plugins(&self) -> Vec { - let plugins = self.plugins.read().await; + pub fn active_plugins(&self) -> Vec { + let plugins = self.plugins.blocking_read(); plugins .iter() .filter(|p| p.is_active && p.instance.is_some()) @@ -1029,15 +1029,15 @@ impl PluginManager { /// Checks if plugin loaded #[must_use] - pub async fn is_plugin_loaded(&self, name: &str) -> bool { - let plugins = self.plugins.read().await; + pub fn is_plugin_loaded(&self, name: &str) -> bool { + let plugins = self.plugins.blocking_read(); plugins.iter().any(|p| p.metadata.name == name) } /// Get list of loaded plugins #[must_use] - pub async fn loaded_plugins(&self) -> Vec { - let plugins = self.plugins.read().await; + pub fn loaded_plugins(&self) -> Vec { + let plugins = self.plugins.blocking_read(); plugins.iter().map(|p| p.metadata.clone()).collect() } diff --git a/crates/pumpkin/src/server/mod.rs b/crates/pumpkin/src/server/mod.rs index 666ebf525..f034ca79b 100644 --- a/crates/pumpkin/src/server/mod.rs +++ b/crates/pumpkin/src/server/mod.rs @@ -315,16 +315,6 @@ impl Server { }; let server = Arc::new(server); - let gen_pool = Arc::new( - rayon::ThreadPoolBuilder::new() - .thread_name(|i| format!("Gen-Pool-{i}")) - .build() - .unwrap_or_else(|err| { - error!("Failed to build generation thread pool: {err}"); - std::process::exit(1); - }), - ); - // Fetch / generate keys in background tasks to avoid blocking startup let server_clone = server.clone(); tokio::spawn(async move { @@ -400,7 +390,6 @@ impl Server { let l_info = server.level_info.clone(); // Access from struct let weak = Arc::downgrade(&server); let config = Arc::new(server.advanced_config.world.clone()); - let pool = gen_pool.clone(); tokio::task::spawn_blocking(move || { info!( @@ -409,7 +398,7 @@ impl Server { .color_named(NamedColor::DarkGreen) .to_pretty_console() ); - let level = into_level(dim.clone(), &config, path, seed, Some(pool)); + let level = into_level(dim.clone(), &config, path, seed); let world = Arc::new(World::load(level.clone(), l_info, dim, registry, weak)); let portal: Arc = Arc::new(WorldPortal(world.clone())); level.world_portal.store(Arc::new(Some(portal))); @@ -508,14 +497,8 @@ impl Server { let config = Arc::new(server.advanced_config.world.clone()); let seed = server.level_info.load().world_gen_settings.seed; - // TODO: gen_pool should be reused - let level = pumpkin_world::dimension::into_level( - dimension.clone(), - &config, - world_path, - seed, - None, - ); + let level = + pumpkin_world::dimension::into_level(dimension.clone(), &config, world_path, seed); let world: World = World::load(level.clone(), l_info, dimension, registry, weak); let world = Arc::new(world); let portal: Arc = Arc::new(WorldPortal(world.clone())); diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index c03da2b27..552b24e5d 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -84,8 +84,8 @@ use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::bedrock::client::set_actor_data::{CSetActorData, PropertySyncData}; use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData}; use pumpkin_protocol::java::client::play::{ - CBlockUpdate, CChunkBatchEnd, CChunkBatchStart, CChunkData, CDisguisedChatMessage, CExplosion, - CLightUpdate, CRespawn, CSetBlockDestroyStage, CWorldEvent, PlayerSpawnData, + CBlockUpdate, CDisguisedChatMessage, CExplosion, CRespawn, CSetBlockDestroyStage, CWorldEvent, + PlayerSpawnData, }; use pumpkin_protocol::java::client::play::{ CPlayerSpawnPosition, CRecipeBookAdd, CRecipeBookSettings, CSystemChatMessage, @@ -511,7 +511,11 @@ impl World { let mut nbt = NbtCompound::new(); entity.write_nbt(&mut nbt); let chunk = self.level.get_entity_chunk(current_chunk).await; - chunk.data.lock().await.push(nbt); + chunk + .data + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(nbt); chunk.mark_dirty(true); } @@ -3144,20 +3148,23 @@ impl World { if client.version.load() < JavaMinecraftVersion::V_1_20_2 && client.version.load() >= JavaMinecraftVersion::V_1_13 { - let mut tags = Vec::new(); let version = client.version.load(); - for &key in pumpkin_data::tag::RegistryKey::NETWORK_KEYS { - if pumpkin_data::tag::get_registry_key_tags(version, key) - .is_some_and(|map| !map.is_empty()) - { - tags.push(key); + if let Ok(Ok(packet_data)) = tokio::task::spawn_blocking(move || { + let mut tags = Vec::new(); + for &key in pumpkin_data::tag::RegistryKey::NETWORK_KEYS { + if pumpkin_data::tag::get_registry_key_tags(version, key) + .is_some_and(|map| !map.is_empty()) + { + tags.push(key); + } } + let packet = pumpkin_protocol::java::client::play::CUpdateTagsPlay::new(&tags); + JavaClient::serialize_packet_for_version(&packet, version) + }) + .await + { + client.send_packet_now(packet_data).await; } - client - .send_packet(&pumpkin_protocol::java::client::play::CUpdateTagsPlay::new( - &tags, - )) - .await; } let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) { @@ -3205,19 +3212,7 @@ impl World { return; } } - if client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - client.send_packet(&CChunkBatchStart).await; - } - client.send_packet(&CChunkData(&chunk)).await; - if client.version.load() >= JavaMinecraftVersion::V_1_14 - && client.version.load() < JavaMinecraftVersion::V_1_18 - && let Ok(light_packet) = CLightUpdate::from_chunk(&chunk, client.version.load()) - { - client.send_packet(&light_packet).await; - } - if client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - client.send_packet(&CChunkBatchEnd::new(1u16)).await; - } + client.send_chunks(&[chunk]).await; let velocity = player.living_entity.entity.velocity.load(); @@ -4245,20 +4240,7 @@ impl World { .level .get_or_fetch_chunk(center_chunk, std::clone::Clone::clone) .await; - if java_client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - java_client.send_packet(&CChunkBatchStart).await; - } - java_client.send_packet(&CChunkData(&chunk)).await; - if java_client.version.load() >= JavaMinecraftVersion::V_1_14 - && java_client.version.load() < JavaMinecraftVersion::V_1_18 - && let Ok(light_packet) = - CLightUpdate::from_chunk(&chunk, java_client.version.load()) - { - java_client.send_packet(&light_packet).await; - } - if java_client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - java_client.send_packet(&CChunkBatchEnd::new(1u16)).await; - } + java_client.send_chunks(&[chunk]).await; } // Send teleport packet after at least the center chunk was delivered @@ -4360,7 +4342,12 @@ impl World { // truth, so the chunk's NBT is taken (cleared) to avoid keeping // a duplicate copy that would be re-appended on the next unload // and doubled on every reload. - let entity_nbts = std::mem::take(&mut *chunk.data.lock().await); + let entity_nbts = std::mem::take( + &mut *chunk + .data + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner), + ); let mut entities_to_add: Vec> = Vec::with_capacity(entity_nbts.len()); for entity_nbt in &entity_nbts {