plugin(api): add schedulers

This commit is contained in:
Alexander Medvedev 2026-04-10 10:36:44 +02:00
parent 57e9c57244
commit 7785f8bcd4
15 changed files with 782 additions and 20 deletions

View File

@ -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<T, String>` used throughout the plugin API.

View File

@ -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<dyn FnMut(Server) + Send>;
pub(crate) struct Task {
handler: TaskHandler,
}
pub(crate) static TASK_HANDLERS: Mutex<LazyTaskHandlers> = Mutex::new(LazyTaskHandlers {
handlers: BTreeMap::new(),
next_id: 0,
});
pub(crate) struct LazyTaskHandlers {
handlers: BTreeMap<u32, Task>,
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<F>(&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<F>(&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<F>(&self, delay_ticks: u64, handler: F) -> u32
where
F: FnMut(Server) + Send + 'static,
{
schedule_delayed_task(delay_ticks, handler)
}
fn schedule_repeating_task<F>(&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<F>(&self, delay_ticks: u64, handler: F) -> u32
where
F: FnMut(Server) + Send + 'static,
{
schedule_delayed_task(delay_ticks, handler)
}
fn schedule_repeating_task<F>(&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<F>(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<F>(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);
}

View File

@ -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<u8>;
/// A position in the world grid (integers).
type block-position = tuple<s32, s32, s32>;
/// A precise coordinate in the world (floats).
type position = tuple<f64, f64, f64>;
/// 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,

View File

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

View File

@ -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<f32>, pitch: option<f32>, %world: %world);
/// Teleports the player to another world at a specific position.
teleport-world: func (world-ref: %world, position: position, yaw: option<f32>, pitch: option<f32>);
/// 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<text-component>);
/// Bans the player's IP address with an optional reason.
ban-ip: func (reason: option<text-component>);
// 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<item-stack>;
/// Returns the item in the specified inventory slot.
get-inventory-item: func (slot: u8) -> option<item-stack>;
// 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;
}
}

View File

@ -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<s32, command-error>;
/// Dispatches a scheduled task execution to the plugin.
export handle-task: func(handler-id: u32, server: server-instance);
}

View File

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

View File

@ -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<player>;
/// 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<player>;
/// 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<player>;
}
}

View File

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

View File

@ -182,6 +182,16 @@ impl WasmPlugin {
) -> Result<Result<(), String>, 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)?;

View File

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

View File

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

View File

@ -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<World>,
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<World>,
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<World>,
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<World>) -> wasmtime::Result<i32> {
Ok(self.get_world_res(&world)?.provider.sea_level)
}
async fn get_min_y(&mut self, world: Resource<World>) -> wasmtime::Result<i32> {
Ok(self.get_world_res(&world)?.provider.min_y)
}
async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> {
self.resource_table
.delete::<WorldResource>(Resource::new_own(rep.rep()))

View File

@ -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<TaskScheduler>,
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<Self>) {
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();

View File

@ -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<WasmPlugin>,
pub handler_id: u32,
pub next_tick: u64,
pub period: Option<u64>,
}
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<Ordering> {
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<BinaryHeap<ScheduledTask>>,
cancelled_tasks: Mutex<HashSet<TaskId>>,
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<WasmPlugin>,
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<WasmPlugin>,
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<WasmPlugin>) {
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<Server>) {
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);
}
}
}
}