diff --git a/pumpkin-plugin-api/src/lib.rs b/pumpkin-plugin-api/src/lib.rs index 522835791..3d3bb88c5 100644 --- a/pumpkin-plugin-api/src/lib.rs +++ b/pumpkin-plugin-api/src/lib.rs @@ -27,11 +27,13 @@ //! ``` use crate::{ - commands::COMMAND_HANDLERS, events::EVENT_HANDLERS, logging::WitSubscriber, text::TextComponent, + commands::COMMAND_HANDLERS, events::EVENT_HANDLERS, logging::WitSubscriber, + scheduler::TASK_HANDLERS, text::TextComponent, }; pub mod commands; pub mod events; +pub mod scheduler; pub mod command { pub use crate::wit::pumpkin::plugin::command::{ @@ -132,6 +134,12 @@ impl wit::Guest for Component { ))) } } + + /// WIT entry point — dispatches a scheduled task invocation to the registered handler for `handler_id`. + fn handle_task(handler_id: u32, server: Server) { + let mut handlers = TASK_HANDLERS.lock().unwrap(); + handlers.handle(handler_id, server); + } } /// Convenience alias for `core::result::Result` used throughout the plugin API. diff --git a/pumpkin-plugin-api/src/scheduler.rs b/pumpkin-plugin-api/src/scheduler.rs new file mode 100644 index 000000000..705ab41fe --- /dev/null +++ b/pumpkin-plugin-api/src/scheduler.rs @@ -0,0 +1,134 @@ +//! Task scheduling API for plugins. +//! +//! This module provides the ability for plugins to schedule closures to be +//! executed after a delay or repeatedly in the server's main tick loop. +//! +//! # Example +//! +//! ```rust,ignore +//! use pumpkin_plugin_api::scheduler::SchedulerExt; +//! +//! context.schedule_delayed_task(20, |server| { +//! server.log("One second has passed!"); +//! }); +//! ``` + +use crate::wit::pumpkin::plugin::context::Server; +use crate::wit::pumpkin::plugin::scheduler; +use std::collections::BTreeMap; +use std::sync::Mutex; + +/// A type alias for a closure that can be scheduled as a task. +pub type TaskHandler = Box; + +pub(crate) struct Task { + handler: TaskHandler, +} + +pub(crate) static TASK_HANDLERS: Mutex = Mutex::new(LazyTaskHandlers { + handlers: BTreeMap::new(), + next_id: 0, +}); + +pub(crate) struct LazyTaskHandlers { + handlers: BTreeMap, + next_id: u32, +} + +impl LazyTaskHandlers { + /// Registers a new task handler and returns its unique ID. + pub fn register(&mut self, handler: TaskHandler) -> u32 { + let id = self.next_id; + self.next_id += 1; + self.handlers.insert(id, Task { handler }); + id + } + + /// Executes the task handler for the given ID. + pub fn handle(&mut self, id: u32, server: Server) { + if let Some(task) = self.handlers.get_mut(&id) { + (task.handler)(server); + } + } +} + +/// Extension trait to provide ergonomic task scheduling on `Context` and `Server`. +pub trait SchedulerExt { + /// Schedules a task to be executed once after the specified number of ticks. + /// + /// * `delay_ticks`: Number of game ticks to wait before execution. + /// * `handler`: Closure to execute. + /// + /// Returns a unique task ID. + fn schedule_delayed_task(&self, delay_ticks: u64, handler: F) -> u32 + where + F: FnMut(Server) + Send + 'static; + + /// Schedules a task to be executed repeatedly. + /// + /// * `delay_ticks`: Number of game ticks to wait before the first execution. + /// * `period_ticks`: Number of ticks between subsequent executions. + /// * `handler`: Closure to execute. + /// + /// Returns a unique task ID. + fn schedule_repeating_task(&self, delay_ticks: u64, period_ticks: u64, handler: F) -> u32 + where + F: FnMut(Server) + Send + 'static; +} + +impl SchedulerExt for crate::Context { + fn schedule_delayed_task(&self, delay_ticks: u64, handler: F) -> u32 + where + F: FnMut(Server) + Send + 'static, + { + schedule_delayed_task(delay_ticks, handler) + } + + fn schedule_repeating_task(&self, delay_ticks: u64, period_ticks: u64, handler: F) -> u32 + where + F: FnMut(Server) + Send + 'static, + { + schedule_repeating_task(delay_ticks, period_ticks, handler) + } +} + +impl SchedulerExt for crate::Server { + fn schedule_delayed_task(&self, delay_ticks: u64, handler: F) -> u32 + where + F: FnMut(Server) + Send + 'static, + { + schedule_delayed_task(delay_ticks, handler) + } + + fn schedule_repeating_task(&self, delay_ticks: u64, period_ticks: u64, handler: F) -> u32 + where + F: FnMut(Server) + Send + 'static, + { + schedule_repeating_task(delay_ticks, period_ticks, handler) + } +} + +/// Lower-level function to schedule a delayed task. +/// Prefer using [`SchedulerExt`] for a more ergonomic API. +pub fn schedule_delayed_task(delay_ticks: u64, handler: F) -> u32 +where + F: FnMut(Server) + Send + 'static, +{ + let handler_id = TASK_HANDLERS.lock().unwrap().register(Box::new(handler)); + scheduler::schedule_delayed_task(handler_id, delay_ticks) +} + +/// Lower-level function to schedule a repeating task. +/// Prefer using [`SchedulerExt`] for a more ergonomic API. +pub fn schedule_repeating_task(delay_ticks: u64, period_ticks: u64, handler: F) -> u32 +where + F: FnMut(Server) + Send + 'static, +{ + let handler_id = TASK_HANDLERS.lock().unwrap().register(Box::new(handler)); + scheduler::schedule_repeating_task(handler_id, delay_ticks, period_ticks) +} + +/// Cancels a scheduled task. +pub fn cancel_task(task_id: u32) { + scheduler::cancel_task(task_id); +} diff --git a/pumpkin-plugin-wit/v0.1.0/common.wit b/pumpkin-plugin-wit/v0.1.0/common.wit index 986ce941d..6eab86557 100644 --- a/pumpkin-plugin-wit/v0.1.0/common.wit +++ b/pumpkin-plugin-wit/v0.1.0/common.wit @@ -1,15 +1,22 @@ +/// Provides common types and utilities used across the entire plugin API. interface common { /// Serialized text component as a postcard byte array. - /// Deprecated: use the text-component resource from the text interface instead. + /// @deprecated Use the text-component resource from the text interface instead. type raw-text-component = list; + + /// A position in the world grid (integers). type block-position = tuple; + + /// A precise coordinate in the world (floats). type position = tuple; + /// Represents the hands of a player. enum hand { left, right, } + /// Represents the game modes available in Minecraft. enum game-mode { survival, creative, @@ -17,6 +24,7 @@ interface common { spectator, } + /// The 16 base Minecraft colors. enum named-color { black, dark-blue, @@ -36,12 +44,14 @@ interface common { white, } + /// An RGB color represented by its red, green, and blue components. record rgb-color { r: u8, g: u8, b: u8, } + /// An ARGB color with alpha (transparency) channel. record argb-color { a: u8, r: u8, @@ -49,11 +59,15 @@ interface common { b: u8, } + /// Represents a stack of items with its type and count. record item-stack { + /// The unique registry key (e.g., "minecraft:diamond"). registry-key: string, + /// The number of items in the stack. count: u8, } + /// All Minecraft-supported locales. enum locale { af-za, ar-sa, diff --git a/pumpkin-plugin-wit/v0.1.0/context.wit b/pumpkin-plugin-wit/v0.1.0/context.wit index 88be2f8de..d702de602 100644 --- a/pumpkin-plugin-wit/v0.1.0/context.wit +++ b/pumpkin-plugin-wit/v0.1.0/context.wit @@ -1,14 +1,33 @@ +/// The core context provided to a plugin during initialization and event handling. interface context { use server.{server}; use event.{event-type, event-priority}; use command.{command}; use permission.{permission}; + /// A contextual handle for plugin-related operations. resource context { + /// Registers a handler for a specific event. + /// + /// * `handler-id`: Unique ID for the event handler. + /// * `event-type`: The type of event to listen for. + /// * `event-priority`: When this handler should be called relative to others. + /// * `blocking`: Whether the event should wait for this handler to finish. register-event: func(handler-id: u32, event-type: event-type, event-priority: event-priority, blocking: bool); + + /// Registers a new command. + /// + /// * `command`: The command definition. + /// * `permission`: The required permission node to execute this command. register-command: func(command: command, permission: string); + + /// Registers a permission node with the server. register-permission: func(permission: permission) -> result<_, string>; + + /// Returns the path to the plugin's private data folder. get-data-folder: func() -> string; + + /// Returns the global server instance. get-server: func() -> server; } } diff --git a/pumpkin-plugin-wit/v0.1.0/player.wit b/pumpkin-plugin-wit/v0.1.0/player.wit index aea4dcbcc..48385d518 100644 --- a/pumpkin-plugin-wit/v0.1.0/player.wit +++ b/pumpkin-plugin-wit/v0.1.0/player.wit @@ -1,3 +1,4 @@ +/// Represents a player connected to the Minecraft server. interface player { use common.{hand, item-stack, position, game-mode}; use %world.{%world}; @@ -5,93 +6,167 @@ interface player { use text.{text-component}; use gui.{gui}; + /// Various abilities a player can have. record player-abilities { + /// Whether the player is invulnerable to damage. invulnerable: bool, + /// Whether the player is currently flying. flying: bool, + /// Whether the player is allowed to fly. allow-flying: bool, + /// Whether the player is in creative mode. creative: bool, + /// Whether the player can modify the world. allow-modify-world: bool, + /// The speed at which the player flies. fly-speed: f32, + /// The speed at which the player walks. walk-speed: f32, } + /// A handle to a player on the server. resource player { + /// Returns the player's unique UUID as a string. get-id: func () -> string; + /// Returns the player's username. get-name: func () -> string; // Basic state + + /// Returns the player's current position in the world. get-position: func () -> position; + /// Returns the player's current yaw (horizontal rotation). get-yaw: func () -> f32; + /// Returns the player's current pitch (vertical rotation). get-pitch: func () -> f32; + /// Returns the world the player is currently in. get-world: func () -> %world; + /// Returns the player's current game mode. get-gamemode: func () -> game-mode; + /// Changes the player's game mode. Returns true if successful. set-gamemode: func (mode: game-mode) -> bool; + /// Returns the player's locale (e.g., "en_us"). get-locale: func () -> string; + /// Returns the player's current ping in milliseconds. get-ping: func () -> u32; // Permissions & metadata + + /// Returns the player's permission level (0-4). get-permission-level: func () -> permission-level; + /// Sets the player's permission level. set-permission-level: func (level: permission-level); + /// Checks if the player has a specific permission node. has-permission: func (node: string) -> bool; + /// Returns the player's display name, which may include formatting. get-display-name: func () -> text-component; // Messaging & titles + + /// Sends a system message to the player's chat. send-system-message: func (text: text-component, overlay: bool); + /// Shows a large title on the player's screen. show-title: func (text: text-component); + /// Shows a smaller subtitle on the player's screen. show-subtitle: func (text: text-component); + /// Shows a message in the action bar area. show-actionbar: func (text: text-component); + /// Sets the fade-in, stay, and fade-out times for titles. send-title-animation: func (fade-in: s32, stay: s32, fade-out: s32); // Teleport & kick + + /// Teleports the player to a position in a world. teleport: func (position: position, yaw: option, pitch: option, %world: %world); + /// Teleports the player to another world at a specific position. teleport-world: func (world-ref: %world, position: position, yaw: option, pitch: option); + /// Kicks the player from the server with the specified reason. kick: func (message: text-component); + /// Forces the player to respawn. respawn: func (); + /// Opens a GUI for the player. open-gui: func (gui-ref: gui); + /// Bans the player with an optional reason. ban: func (reason: option); + /// Bans the player's IP address with an optional reason. ban-ip: func (reason: option); // Inventory + + /// Returns the index of the currently selected hotbar slot (0-8). get-selected-slot: func () -> u8; + /// Returns the item currently in the specified hand. get-item-in-hand: func (hand: hand) -> option; + /// Returns the item in the specified inventory slot. get-inventory-item: func (slot: u8) -> option; // Status + + /// Returns the player's current health. get-health: func () -> f32; + /// Sets the player's current health. set-health: func (health: f32); + /// Returns the player's maximum health. get-max-health: func () -> f32; + /// Sets the player's maximum health. set-max-health: func (max-health: f32); + /// Returns the player's food level (0-20). get-food-level: func () -> u8; + /// Sets the player's food level. set-food-level: func (food-level: u8); + /// Returns the player's food saturation level. get-saturation: func () -> f32; + /// Sets the player's food saturation level. set-saturation: func (saturation: f32); + /// Returns the player's food exhaustion level. get-exhaustion: func () -> f32; + /// Sets the player's food exhaustion level. set-exhaustion: func (exhaustion: f32); + /// Returns the player's current absorption amount. get-absorption: func () -> f32; + /// Sets the player's current absorption amount. set-absorption: func (absorption: f32); + /// Returns the player's total experience level. get-experience-level: func() -> s32; + /// Returns the player's progress toward the next level (0.0 to 1.0). get-experience-progress: func() -> f32; + /// Returns the total experience points the player has. get-experience-points: func() -> s32; + /// Sets the player's total experience level. set-experience-level: func(level: s32); + /// Sets the player's progress toward the next level. set-experience-progress: func(progress: f32); + /// Sets the player's total experience points. set-experience-points: func(points: s32); + /// Adds experience levels to the player. add-experience-levels: func(levels: s32); + /// Adds experience points to the player. add-experience-points: func(points: s32); + /// Returns whether the player is currently sneaking. is-sneaking: func() -> bool; + /// Sets whether the player is currently sneaking. set-sneaking: func(sneaking: bool); + /// Returns whether the player is currently sprinting. is-sprinting: func() -> bool; + /// Sets whether the player is currently sprinting. set-sprinting: func(sprinting: bool); + /// Returns whether the player is currently on the ground. is-on-ground: func() -> bool; + /// Returns whether the player is currently flying. is-flying: func() -> bool; + /// Sets whether the player should be flying. set-flying: func(flying: bool); + /// Returns the player's current abilities. get-abilities: func() -> player-abilities; + /// Updates the player's abilities. set-abilities: func(abilities: player-abilities); + /// Returns the player's IP address. get-ip: func() -> string; } } diff --git a/pumpkin-plugin-wit/v0.1.0/plugin.wit b/pumpkin-plugin-wit/v0.1.0/plugin.wit index a38781fe4..dd6a27298 100644 --- a/pumpkin-plugin-wit/v0.1.0/plugin.wit +++ b/pumpkin-plugin-wit/v0.1.0/plugin.wit @@ -1,3 +1,6 @@ +/// The main entry point for a Pumpkin plugin. +/// This world defines the exports that the plugin must provide to the host +/// and the imports that the host provides to the plugin. package pumpkin:plugin; world plugin { @@ -6,22 +9,53 @@ world plugin { use server.{server as server-instance}; use command.{command-sender, consumed-args, command-error}; - // This is what the host should provide to the plugin - import logging; - import gui; - import scoreboard; - import server; - import text; - import command; - import context; - import i18n; + // --- Host-provided imports --- - // This is what the plugin should provide + /// Provides logging capabilities to the plugin. + import logging; + /// Provides GUI creation and management. + import gui; + /// Provides scoreboard management. + import scoreboard; + /// Provides global server information and player lookup. + import server; + /// Provides text component building and formatting. + import text; + /// Provides command registration and argument parsing. + import command; + /// Provides the plugin execution context. + import context; + /// Provides internationalization and translation support. + import i18n; + /// Provides task scheduling for delayed or repeating logic. + import scheduler; + + // --- Plugin-provided exports --- + + /// Called by the host once to initialize the plugin. + /// Internal use only; plugins should use the `register_plugin!` macro. export init-plugin: func(); + + /// Called when the plugin is being loaded. + /// Use this to register commands, events, and perform initial setup. export on-load: func(context: context) -> result<_, string>; + + /// Called when the plugin is being unloaded. + /// Use this to save data and clean up resources. export on-unload: func(context: context) -> result<_, string>; + + /// Provides plugin metadata (name, version, etc.) to the host. export metadata; + + /// Common types used across the API. export common; + + /// Dispatches an event to the plugin for handling. export handle-event: func(event-id: u32, server: server-instance, event: event) -> event; + + /// Dispatches a command execution to the plugin. export handle-command: func(command-id: u32, sender: command-sender, server: server-instance, args: consumed-args) -> result; + + /// Dispatches a scheduled task execution to the plugin. + export handle-task: func(handler-id: u32, server: server-instance); } diff --git a/pumpkin-plugin-wit/v0.1.0/scheduler.wit b/pumpkin-plugin-wit/v0.1.0/scheduler.wit new file mode 100644 index 000000000..cd06e37ee --- /dev/null +++ b/pumpkin-plugin-wit/v0.1.0/scheduler.wit @@ -0,0 +1,24 @@ +/// Provides functionality to schedule tasks to be executed later on the server. +interface scheduler { + /// Schedules a task to be executed once after the specified number of ticks. + /// + /// * `handler-id`: The unique ID of the handler to call when the task executes. + /// * `delay-ticks`: The number of game ticks to wait before execution (20 ticks = 1 second). + /// + /// Returns a unique task ID that can be used to cancel the task. + schedule-delayed-task: func(handler-id: u32, delay-ticks: u64) -> u32; + + /// Schedules a task to be executed repeatedly. + /// + /// * `handler-id`: The unique ID of the handler to call when the task executes. + /// * `delay-ticks`: The number of game ticks to wait before the first execution. + /// * `period-ticks`: The number of game ticks between subsequent executions. + /// + /// Returns a unique task ID that can be used to cancel the task. + schedule-repeating-task: func(handler-id: u32, delay-ticks: u64, period-ticks: u64) -> u32; + + /// Cancels a scheduled task if it hasn't finished yet. + /// + /// * `task-id`: The ID of the task to cancel. + cancel-task: func(task-id: u32); +} diff --git a/pumpkin-plugin-wit/v0.1.0/server.wit b/pumpkin-plugin-wit/v0.1.0/server.wit index a2a95fd79..f71f82bbf 100644 --- a/pumpkin-plugin-wit/v0.1.0/server.wit +++ b/pumpkin-plugin-wit/v0.1.0/server.wit @@ -1,33 +1,41 @@ interface server { use player.{player}; + /// Represents the difficulty setting of a Minecraft server. enum difficulty { + /// Hostile mobs do not spawn. Players regain health automatically. peaceful, + /// Hostile mobs deal less damage. easy, + /// Standard Minecraft difficulty. normal, + /// Hostile mobs deal more damage. hard, } + /// Represents the global server instance. resource server { - /// Returns the current difficulty level of the server + /// Returns the current difficulty level of the server. get-difficulty: func() -> difficulty; - /// Returns the total number of players connected across all worlds. + /// Returns the total number of players currently connected to the server. get-player-count: func() -> u32; - /// Returns the rolling average Milliseconds Per Tick (MSPT) over the last 100 ticks + /// Returns the rolling average Milliseconds Per Tick (MSPT). + /// A value below 50.0 is required for a stable 20 TPS. get-mspt: func() -> f64; - /// Returns the effective Ticks Per Second (TPS) + /// Returns the effective Ticks Per Second (TPS). + /// Ideal value is 20.0. get-tps: func() -> f64; - /// Returns all online players from all worlds on the server. + /// Returns a list of all online players across all worlds. get-all-players: func() -> list; - /// Searches for a player by their name across all worlds. + /// Searches for a player by their exact name. get-player-by-name: func(name: string) -> option; - /// Searches for a player by their UUID across all worlds. + /// Searches for a player by their UUID string. get-player-by-uuid: func(id: string) -> option; } } diff --git a/pumpkin-plugin-wit/v0.1.0/world.wit b/pumpkin-plugin-wit/v0.1.0/world.wit index 4fcc4a9d6..c1ac9c77f 100644 --- a/pumpkin-plugin-wit/v0.1.0/world.wit +++ b/pumpkin-plugin-wit/v0.1.0/world.wit @@ -3,79 +3,167 @@ interface %world { use text.{text-component}; use scoreboard.{scoreboard}; + /// Defines how a block reacts when pushed by a piston. enum piston-behavior { + /// Normal behavior; the block can be pushed and pulled. normal, + /// The block is destroyed when pushed. destroy, + /// The block cannot be pushed or pulled. block, + /// The block is ignored by pistons. ignore, + /// The block can only be pushed, not pulled. push-only } + /// Categories for sounds to control their volume independently. + enum sound-category { + master, + music, + records, + weather, + blocks, + hostile, + neutral, + players, + ambient, + voice, + } + + /// Defines how an explosion interacts with blocks in the world. + enum explosion-interaction { + /// No interaction with blocks. + none, + /// Blocks are destroyed and may drop loot. + block, + /// Interaction typical for mob-caused explosions. + mob, + /// Interaction typical for TNT explosions. + tnt, + /// Triggers specific block behaviors. + trigger, + } + + /// Flags used to control block updates and notifications. flags block-flags { + /// Notifies neighbor blocks of the change. notify-neighbors, + /// Notifies client listeners of the change. notify-listeners, + /// Forces the block state even if it's the same. force-state, + /// Skips dropping items when the block is removed. skip-drops, + /// Indicates the block was moved (e.g., by a piston). moved, + /// Skips state replacement for redstone wires. skip-redstone-wire-state-replacement, + /// Skips the callback when a block entity is replaced. skip-block-entity-replaced-callback, + /// Skips the callback when a block is added. skip-block-added-callback, } + /// Detailed information about a block's state. record block-state { + /// The internal numerical ID of the block state. id: u16, + /// The light level emitted by this block (0-15). luminance: u8, + /// How much light this block blocks. opacity: u8, + /// The hardness of the block, determining mining speed. hardness: f32, + /// Whether this block is considered air. is-air: bool, + /// Whether this block is a liquid. is-liquid: bool, + /// Whether this block is a solid. is-solid: bool, + /// Whether this block occupies a full cube. is-full-cube: bool, + /// How this block behaves when pushed by a piston. piston-behavior: piston-behavior, + /// Whether this block receives random ticks. has-random-ticks: bool, } + /// A simple wrapper for a block state ID. record block-state-id { id: u16, } + /// A position in the world grid. record block-pos { x: s32, y: s32, z: s32, } + /// Represents a Minecraft world (dimension). resource %world { + /// Returns the unique identifier of this world. get-id: func() -> string; + /// Gets the block state ID at the specified position. get-block-state-id: func(pos: block-pos) -> u16; + /// Gets detailed block state information at the specified position. get-block-state: func(pos: block-pos) -> block-state; + /// Sets the block state at the specified position. set-block-state: func(pos: block-pos, state: u16, update-flags: block-flags); + /// Returns the current time of day in ticks. get-time-of-day: func() -> u64; + /// Sets the current time of day in ticks. set-time-of-day: func(time: u64); + /// Returns the total age of the world in ticks. get-world-age: func() -> u64; + /// Returns the dimension type (e.g., "minecraft:overworld"). get-dimension: func() -> string; + /// Gets the highest non-air block Y coordinate at the specified X and Z. get-top-block-y: func(x: s32, z: s32) -> s32; + /// Gets the highest motion-blocking block Y coordinate. get-motion-blocking-height: func(x: s32, z: s32) -> s32; + /// Returns whether it is currently raining or snowing. is-raining: func() -> bool; + /// Sets whether it should be raining or snowing. set-raining: func(raining: bool); + /// Returns whether there is currently a thunderstorm. is-thundering: func() -> bool; + /// Sets whether there should be a thunderstorm. set-thundering: func(thundering: bool); + /// Broadcasts a system message to all players in this world. broadcast-system-message: func(message: text-component, overlay: bool); + /// Returns the scoreboard associated with this world. get-scoreboard: func() -> scoreboard; + + /// Plays a sound at the specified position for all players in the world. + play-sound: func(sound: string, category: sound-category, pos: position, volume: f32, pitch: f32); + + /// Spawns particles at the specified position. + spawn-particle: func(particle: string, pos: position, offset: position, max-speed: f32, count: s32); + + /// Creates an explosion at the specified position. + create-explosion: func(pos: position, power: f32, create-fire: bool, interaction: explosion-interaction); + + /// Returns the sea level of this world. + get-sea-level: func() -> s32; + + /// Returns the minimum Y coordinate (bottom of the world). + get-min-y: func() -> s32; } } diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs index 275b4124d..954fed5bf 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs @@ -182,6 +182,16 @@ impl WasmPlugin { ) -> Result, wasmtime::Error> { let mut store = self.store.lock().await; + if let Some(weak_plugin) = &store.data().plugin + && let Some(plugin) = weak_plugin.upgrade() + { + context + .server + .task_scheduler + .cancel_all_tasks(&plugin) + .await; + } + match self.plugin_instance { PluginInstance::V0_1_0(ref plugin) => { let context = store.data_mut().add_context(context)?; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs index e297ddf3f..3bfaaf739 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs @@ -16,6 +16,7 @@ pub mod i18n; pub mod logging; pub mod permission; pub mod player; +pub mod scheduler; pub mod scoreboard; pub mod server; pub mod text; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/scheduler.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/scheduler.rs new file mode 100644 index 000000000..20136c3fe --- /dev/null +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/scheduler.rs @@ -0,0 +1,60 @@ +use crate::plugin::loader::wasm::wasm_host::{ + state::PluginHostState, wit::v0_1_0::pumpkin::plugin::scheduler, +}; +use std::sync::atomic::Ordering; + +impl scheduler::Host for PluginHostState { + async fn schedule_delayed_task( + &mut self, + handler_id: u32, + delay: u64, + ) -> wasmtime::Result { + let plugin = self + .plugin + .as_ref() + .and_then(std::sync::Weak::upgrade) + .ok_or_else(|| wasmtime::Error::msg("Plugin not found"))?; + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not found"))?; + let tick_count = server.tick_count.load(Ordering::Relaxed) as u64; + let task_id = server + .task_scheduler + .schedule_delayed_task(plugin, handler_id, delay, tick_count) + .await; + Ok(task_id) + } + + async fn schedule_repeating_task( + &mut self, + handler_id: u32, + delay: u64, + period: u64, + ) -> wasmtime::Result { + let plugin = self + .plugin + .as_ref() + .and_then(std::sync::Weak::upgrade) + .ok_or_else(|| wasmtime::Error::msg("Plugin not found"))?; + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not found"))?; + let tick_count = server.tick_count.load(Ordering::Relaxed) as u64; + let task_id = server + .task_scheduler + .schedule_repeating_task(plugin, handler_id, delay, period, tick_count) + .await; + Ok(task_id) + } + + async fn cancel_task(&mut self, task_id: u32) -> wasmtime::Result<()> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not found"))?; + server.task_scheduler.cancel_task(task_id).await; + Ok(()) + } +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs index f61284c9c..7d15bc0c4 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs @@ -11,6 +11,7 @@ use crate::plugin::loader::wasm::wasm_host::{ state::{PluginHostState, TextComponentResource, WorldResource}, wit::v0_1_0::pumpkin::{self, plugin::world::World}, }; +use crate::world::explosion::Explosion; // --- Trapping Helpers --- impl PluginHostState { @@ -228,6 +229,120 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { self.add_scoreboard(world_provider) } + async fn play_sound( + &mut self, + world: Resource, + sound: String, + category: pumpkin::plugin::world::SoundCategory, + pos: pumpkin::plugin::common::Position, + volume: f32, + pitch: f32, + ) -> wasmtime::Result<()> { + let world_ref = self.get_world_res(&world)?; + let sound_data = pumpkin_data::sound::Sound::from_name(&sound) + .ok_or_else(|| wasmtime::Error::msg(format!("Unknown sound: {sound}")))?; + let internal_category = match category { + pumpkin::plugin::world::SoundCategory::Master => { + pumpkin_data::sound::SoundCategory::Master + } + pumpkin::plugin::world::SoundCategory::Music => { + pumpkin_data::sound::SoundCategory::Music + } + pumpkin::plugin::world::SoundCategory::Records => { + pumpkin_data::sound::SoundCategory::Records + } + pumpkin::plugin::world::SoundCategory::Weather => { + pumpkin_data::sound::SoundCategory::Weather + } + pumpkin::plugin::world::SoundCategory::Blocks => { + pumpkin_data::sound::SoundCategory::Blocks + } + pumpkin::plugin::world::SoundCategory::Hostile => { + pumpkin_data::sound::SoundCategory::Hostile + } + pumpkin::plugin::world::SoundCategory::Neutral => { + pumpkin_data::sound::SoundCategory::Neutral + } + pumpkin::plugin::world::SoundCategory::Players => { + pumpkin_data::sound::SoundCategory::Players + } + pumpkin::plugin::world::SoundCategory::Ambient => { + pumpkin_data::sound::SoundCategory::Ambient + } + pumpkin::plugin::world::SoundCategory::Voice => { + pumpkin_data::sound::SoundCategory::Voice + } + }; + + world_ref + .provider + .play_sound_raw( + sound_data as u16, + internal_category, + &pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2), + volume, + pitch, + ) + .await; + Ok(()) + } + + async fn spawn_particle( + &mut self, + world: Resource, + particle: String, + pos: pumpkin::plugin::common::Position, + offset: pumpkin::plugin::common::Position, + max_speed: f32, + count: i32, + ) -> wasmtime::Result<()> { + let world_ref = self.get_world_res(&world)?; + let particle_data = pumpkin_data::particle::Particle::from_name(&particle) + .ok_or_else(|| wasmtime::Error::msg(format!("Unknown particle: {particle}")))?; + + world_ref + .provider + .spawn_particle( + pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2), + pumpkin_util::math::vector3::Vector3::new( + offset.0 as f32, + offset.1 as f32, + offset.2 as f32, + ), + max_speed, + count, + particle_data, + ) + .await; + Ok(()) + } + + async fn create_explosion( + &mut self, + world: Resource, + pos: pumpkin::plugin::common::Position, + power: f32, + _create_fire: bool, + _interaction: pumpkin::plugin::world::ExplosionInteraction, + ) -> wasmtime::Result<()> { + let world_ref = self.get_world_res(&world)?; + // Currently Explosion only supports power and position in this codebase + let explosion = Explosion::new( + power, + pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2), + ); + explosion.explode(&world_ref.provider).await; + Ok(()) + } + + async fn get_sea_level(&mut self, world: Resource) -> wasmtime::Result { + Ok(self.get_world_res(&world)?.provider.sea_level) + } + + async fn get_min_y(&mut self, world: Resource) -> wasmtime::Result { + Ok(self.get_world_res(&world)?.provider.min_y) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { self.resource_table .delete::(Resource::new_own(rep.rep())) diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 8aa1448ea..e6b86e6de 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -51,13 +51,15 @@ use tokio_util::task::TaskTracker; mod connection_cache; mod key_store; +pub mod scheduler; pub mod seasonal_events; pub mod tick_rate_manager; pub mod ticker; -use super::command::args::entities::{ +use crate::command::args::entities::{ EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition, }; +use crate::server::scheduler::TaskScheduler; /// Represents a Minecraft server instance. pub struct Server { @@ -115,6 +117,8 @@ pub struct Server { pub server_guid: u64, /// Player idle timeout in minutes (0 = disabled) pub player_idle_timeout: AtomicI32, + /// Manages scheduled tasks (e.g. from plugins) + pub task_scheduler: Arc, tasks: TaskTracker, // world stuff which maybe should be put into a struct @@ -240,6 +244,7 @@ impl Server { aggregated_tick_times_nanos: AtomicI64::new(0), tick_count: AtomicI32::new(0), tasks: TaskTracker::new(), + task_scheduler: Arc::new(TaskScheduler::new()), server_guid: rand::random(), player_idle_timeout: AtomicI32::new(0), mojang_public_keys: ArcSwap::from_pointee(Vec::new()), @@ -733,6 +738,8 @@ impl Server { pub async fn tick_worlds(self: &Arc) { let mut set = JoinSet::new(); + self.task_scheduler.tick(self).await; + for world in self.worlds.load().iter() { let world = world.clone(); let server = self.clone(); diff --git a/pumpkin/src/server/scheduler.rs b/pumpkin/src/server/scheduler.rs new file mode 100644 index 000000000..9ca92df70 --- /dev/null +++ b/pumpkin/src/server/scheduler.rs @@ -0,0 +1,165 @@ +use crate::plugin::loader::wasm::wasm_host::WasmPlugin; +use crate::server::Server; +use std::cmp::Ordering; +use std::collections::{BinaryHeap, HashSet}; +use std::sync::Arc; +use std::sync::atomic::Ordering as AtomicOrdering; +use tokio::sync::Mutex; + +pub type TaskId = u32; + +pub struct ScheduledTask { + pub id: TaskId, + pub plugin: Arc, + pub handler_id: u32, + pub next_tick: u64, + pub period: Option, +} + +impl PartialEq for ScheduledTask { + fn eq(&self, other: &Self) -> bool { + self.next_tick == other.next_tick + } +} + +impl Eq for ScheduledTask {} + +impl PartialOrd for ScheduledTask { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScheduledTask { + fn cmp(&self, other: &Self) -> Ordering { + // Reverse order so BinaryHeap is a min-heap + other.next_tick.cmp(&self.next_tick) + } +} + +pub struct TaskScheduler { + tasks: Mutex>, + cancelled_tasks: Mutex>, + next_task_id: std::sync::atomic::AtomicU32, +} + +impl Default for TaskScheduler { + fn default() -> Self { + Self::new() + } +} + +impl TaskScheduler { + #[must_use] + pub fn new() -> Self { + Self { + tasks: Mutex::new(BinaryHeap::new()), + cancelled_tasks: Mutex::new(HashSet::new()), + next_task_id: std::sync::atomic::AtomicU32::new(0), + } + } + + pub async fn schedule_delayed_task( + &self, + plugin: Arc, + handler_id: u32, + delay: u64, + current_tick: u64, + ) -> TaskId { + let id = self.next_task_id.fetch_add(1, AtomicOrdering::SeqCst); + let task = ScheduledTask { + id, + plugin, + handler_id, + next_tick: current_tick + delay, + period: None, + }; + self.tasks.lock().await.push(task); + id + } + + pub async fn schedule_repeating_task( + &self, + plugin: Arc, + handler_id: u32, + delay: u64, + period: u64, + current_tick: u64, + ) -> TaskId { + let id = self.next_task_id.fetch_add(1, AtomicOrdering::SeqCst); + let task = ScheduledTask { + id, + plugin, + handler_id, + next_tick: current_tick + delay, + period: Some(period), + }; + self.tasks.lock().await.push(task); + id + } + + pub async fn cancel_task(&self, id: TaskId) { + self.cancelled_tasks.lock().await.insert(id); + } + + pub async fn cancel_all_tasks(&self, plugin: &Arc) { + let tasks = self.tasks.lock().await; + let mut cancelled = self.cancelled_tasks.lock().await; + for task in tasks.iter() { + if Arc::ptr_eq(&task.plugin, plugin) { + cancelled.insert(task.id); + } + } + } + + pub async fn tick(&self, server: &Arc) { + let current_tick = server.tick_count.load(AtomicOrdering::Relaxed) as u64; + let mut tasks_to_run = Vec::new(); + + { + let mut tasks = self.tasks.lock().await; + let mut cancelled = self.cancelled_tasks.lock().await; + + while let Some(task) = tasks.peek() { + if task.next_tick > current_tick { + break; + } + + let task = tasks.pop().unwrap(); + if cancelled.remove(&task.id) { + continue; + } + + tasks_to_run.push(task); + } + } + + for mut task in tasks_to_run { + // Run the task + let plugin = task.plugin.clone(); + let handler_id = task.handler_id; + let server_clone = server.clone(); + + tokio::spawn(async move { + let mut store = plugin.store.lock().await; + match plugin.plugin_instance { + crate::plugin::loader::wasm::wasm_host::PluginInstance::V0_1_0( + ref instance, + ) => { + if let Ok(server_res) = store.data_mut().add_server(server_clone) { + let _ = instance + .call_handle_task(&mut *store, handler_id, server_res) + .await; + } + } + } + }); + + // If repeating, schedule next run + if let Some(period) = task.period { + task.next_tick = current_tick + period; + self.tasks.lock().await.push(task); + } + } + } +}