plugin(api): add schedulers
This commit is contained in:
parent
57e9c57244
commit
7785f8bcd4
@ -27,11 +27,13 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::{
|
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 commands;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub mod scheduler;
|
||||||
|
|
||||||
pub mod command {
|
pub mod command {
|
||||||
pub use crate::wit::pumpkin::plugin::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.
|
/// Convenience alias for `core::result::Result<T, String>` used throughout the plugin API.
|
||||||
|
|||||||
134
pumpkin-plugin-api/src/scheduler.rs
Normal file
134
pumpkin-plugin-api/src/scheduler.rs
Normal 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);
|
||||||
|
}
|
||||||
@ -1,15 +1,22 @@
|
|||||||
|
/// Provides common types and utilities used across the entire plugin API.
|
||||||
interface common {
|
interface common {
|
||||||
/// Serialized text component as a postcard byte array.
|
/// 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>;
|
type raw-text-component = list<u8>;
|
||||||
|
|
||||||
|
/// A position in the world grid (integers).
|
||||||
type block-position = tuple<s32, s32, s32>;
|
type block-position = tuple<s32, s32, s32>;
|
||||||
|
|
||||||
|
/// A precise coordinate in the world (floats).
|
||||||
type position = tuple<f64, f64, f64>;
|
type position = tuple<f64, f64, f64>;
|
||||||
|
|
||||||
|
/// Represents the hands of a player.
|
||||||
enum hand {
|
enum hand {
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the game modes available in Minecraft.
|
||||||
enum game-mode {
|
enum game-mode {
|
||||||
survival,
|
survival,
|
||||||
creative,
|
creative,
|
||||||
@ -17,6 +24,7 @@ interface common {
|
|||||||
spectator,
|
spectator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The 16 base Minecraft colors.
|
||||||
enum named-color {
|
enum named-color {
|
||||||
black,
|
black,
|
||||||
dark-blue,
|
dark-blue,
|
||||||
@ -36,12 +44,14 @@ interface common {
|
|||||||
white,
|
white,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An RGB color represented by its red, green, and blue components.
|
||||||
record rgb-color {
|
record rgb-color {
|
||||||
r: u8,
|
r: u8,
|
||||||
g: u8,
|
g: u8,
|
||||||
b: u8,
|
b: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An ARGB color with alpha (transparency) channel.
|
||||||
record argb-color {
|
record argb-color {
|
||||||
a: u8,
|
a: u8,
|
||||||
r: u8,
|
r: u8,
|
||||||
@ -49,11 +59,15 @@ interface common {
|
|||||||
b: u8,
|
b: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a stack of items with its type and count.
|
||||||
record item-stack {
|
record item-stack {
|
||||||
|
/// The unique registry key (e.g., "minecraft:diamond").
|
||||||
registry-key: string,
|
registry-key: string,
|
||||||
|
/// The number of items in the stack.
|
||||||
count: u8,
|
count: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All Minecraft-supported locales.
|
||||||
enum locale {
|
enum locale {
|
||||||
af-za,
|
af-za,
|
||||||
ar-sa,
|
ar-sa,
|
||||||
|
|||||||
@ -1,14 +1,33 @@
|
|||||||
|
/// The core context provided to a plugin during initialization and event handling.
|
||||||
interface context {
|
interface context {
|
||||||
use server.{server};
|
use server.{server};
|
||||||
use event.{event-type, event-priority};
|
use event.{event-type, event-priority};
|
||||||
use command.{command};
|
use command.{command};
|
||||||
use permission.{permission};
|
use permission.{permission};
|
||||||
|
|
||||||
|
/// A contextual handle for plugin-related operations.
|
||||||
resource context {
|
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);
|
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);
|
register-command: func(command: command, permission: string);
|
||||||
|
|
||||||
|
/// Registers a permission node with the server.
|
||||||
register-permission: func(permission: permission) -> result<_, string>;
|
register-permission: func(permission: permission) -> result<_, string>;
|
||||||
|
|
||||||
|
/// Returns the path to the plugin's private data folder.
|
||||||
get-data-folder: func() -> string;
|
get-data-folder: func() -> string;
|
||||||
|
|
||||||
|
/// Returns the global server instance.
|
||||||
get-server: func() -> server;
|
get-server: func() -> server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/// Represents a player connected to the Minecraft server.
|
||||||
interface player {
|
interface player {
|
||||||
use common.{hand, item-stack, position, game-mode};
|
use common.{hand, item-stack, position, game-mode};
|
||||||
use %world.{%world};
|
use %world.{%world};
|
||||||
@ -5,93 +6,167 @@ interface player {
|
|||||||
use text.{text-component};
|
use text.{text-component};
|
||||||
use gui.{gui};
|
use gui.{gui};
|
||||||
|
|
||||||
|
/// Various abilities a player can have.
|
||||||
record player-abilities {
|
record player-abilities {
|
||||||
|
/// Whether the player is invulnerable to damage.
|
||||||
invulnerable: bool,
|
invulnerable: bool,
|
||||||
|
/// Whether the player is currently flying.
|
||||||
flying: bool,
|
flying: bool,
|
||||||
|
/// Whether the player is allowed to fly.
|
||||||
allow-flying: bool,
|
allow-flying: bool,
|
||||||
|
/// Whether the player is in creative mode.
|
||||||
creative: bool,
|
creative: bool,
|
||||||
|
/// Whether the player can modify the world.
|
||||||
allow-modify-world: bool,
|
allow-modify-world: bool,
|
||||||
|
/// The speed at which the player flies.
|
||||||
fly-speed: f32,
|
fly-speed: f32,
|
||||||
|
/// The speed at which the player walks.
|
||||||
walk-speed: f32,
|
walk-speed: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A handle to a player on the server.
|
||||||
resource player {
|
resource player {
|
||||||
|
/// Returns the player's unique UUID as a string.
|
||||||
get-id: func () -> string;
|
get-id: func () -> string;
|
||||||
|
/// Returns the player's username.
|
||||||
get-name: func () -> string;
|
get-name: func () -> string;
|
||||||
|
|
||||||
// Basic state
|
// Basic state
|
||||||
|
|
||||||
|
/// Returns the player's current position in the world.
|
||||||
get-position: func () -> position;
|
get-position: func () -> position;
|
||||||
|
/// Returns the player's current yaw (horizontal rotation).
|
||||||
get-yaw: func () -> f32;
|
get-yaw: func () -> f32;
|
||||||
|
/// Returns the player's current pitch (vertical rotation).
|
||||||
get-pitch: func () -> f32;
|
get-pitch: func () -> f32;
|
||||||
|
/// Returns the world the player is currently in.
|
||||||
get-world: func () -> %world;
|
get-world: func () -> %world;
|
||||||
|
/// Returns the player's current game mode.
|
||||||
get-gamemode: func () -> game-mode;
|
get-gamemode: func () -> game-mode;
|
||||||
|
/// Changes the player's game mode. Returns true if successful.
|
||||||
set-gamemode: func (mode: game-mode) -> bool;
|
set-gamemode: func (mode: game-mode) -> bool;
|
||||||
|
/// Returns the player's locale (e.g., "en_us").
|
||||||
get-locale: func () -> string;
|
get-locale: func () -> string;
|
||||||
|
/// Returns the player's current ping in milliseconds.
|
||||||
get-ping: func () -> u32;
|
get-ping: func () -> u32;
|
||||||
|
|
||||||
// Permissions & metadata
|
// Permissions & metadata
|
||||||
|
|
||||||
|
/// Returns the player's permission level (0-4).
|
||||||
get-permission-level: func () -> permission-level;
|
get-permission-level: func () -> permission-level;
|
||||||
|
/// Sets the player's permission level.
|
||||||
set-permission-level: func (level: permission-level);
|
set-permission-level: func (level: permission-level);
|
||||||
|
/// Checks if the player has a specific permission node.
|
||||||
has-permission: func (node: string) -> bool;
|
has-permission: func (node: string) -> bool;
|
||||||
|
/// Returns the player's display name, which may include formatting.
|
||||||
get-display-name: func () -> text-component;
|
get-display-name: func () -> text-component;
|
||||||
|
|
||||||
// Messaging & titles
|
// Messaging & titles
|
||||||
|
|
||||||
|
/// Sends a system message to the player's chat.
|
||||||
send-system-message: func (text: text-component, overlay: bool);
|
send-system-message: func (text: text-component, overlay: bool);
|
||||||
|
/// Shows a large title on the player's screen.
|
||||||
show-title: func (text: text-component);
|
show-title: func (text: text-component);
|
||||||
|
/// Shows a smaller subtitle on the player's screen.
|
||||||
show-subtitle: func (text: text-component);
|
show-subtitle: func (text: text-component);
|
||||||
|
/// Shows a message in the action bar area.
|
||||||
show-actionbar: func (text: text-component);
|
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);
|
send-title-animation: func (fade-in: s32, stay: s32, fade-out: s32);
|
||||||
|
|
||||||
// Teleport & kick
|
// Teleport & kick
|
||||||
|
|
||||||
|
/// Teleports the player to a position in a world.
|
||||||
teleport: func (position: position, yaw: option<f32>, pitch: option<f32>, %world: %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>);
|
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);
|
kick: func (message: text-component);
|
||||||
|
/// Forces the player to respawn.
|
||||||
respawn: func ();
|
respawn: func ();
|
||||||
|
|
||||||
|
/// Opens a GUI for the player.
|
||||||
open-gui: func (gui-ref: gui);
|
open-gui: func (gui-ref: gui);
|
||||||
|
|
||||||
|
/// Bans the player with an optional reason.
|
||||||
ban: func (reason: option<text-component>);
|
ban: func (reason: option<text-component>);
|
||||||
|
/// Bans the player's IP address with an optional reason.
|
||||||
ban-ip: func (reason: option<text-component>);
|
ban-ip: func (reason: option<text-component>);
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
|
|
||||||
|
/// Returns the index of the currently selected hotbar slot (0-8).
|
||||||
get-selected-slot: func () -> u8;
|
get-selected-slot: func () -> u8;
|
||||||
|
/// Returns the item currently in the specified hand.
|
||||||
get-item-in-hand: func (hand: hand) -> option<item-stack>;
|
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>;
|
get-inventory-item: func (slot: u8) -> option<item-stack>;
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
|
|
||||||
|
/// Returns the player's current health.
|
||||||
get-health: func () -> f32;
|
get-health: func () -> f32;
|
||||||
|
/// Sets the player's current health.
|
||||||
set-health: func (health: f32);
|
set-health: func (health: f32);
|
||||||
|
/// Returns the player's maximum health.
|
||||||
get-max-health: func () -> f32;
|
get-max-health: func () -> f32;
|
||||||
|
/// Sets the player's maximum health.
|
||||||
set-max-health: func (max-health: f32);
|
set-max-health: func (max-health: f32);
|
||||||
|
/// Returns the player's food level (0-20).
|
||||||
get-food-level: func () -> u8;
|
get-food-level: func () -> u8;
|
||||||
|
/// Sets the player's food level.
|
||||||
set-food-level: func (food-level: u8);
|
set-food-level: func (food-level: u8);
|
||||||
|
/// Returns the player's food saturation level.
|
||||||
get-saturation: func () -> f32;
|
get-saturation: func () -> f32;
|
||||||
|
/// Sets the player's food saturation level.
|
||||||
set-saturation: func (saturation: f32);
|
set-saturation: func (saturation: f32);
|
||||||
|
/// Returns the player's food exhaustion level.
|
||||||
get-exhaustion: func () -> f32;
|
get-exhaustion: func () -> f32;
|
||||||
|
/// Sets the player's food exhaustion level.
|
||||||
set-exhaustion: func (exhaustion: f32);
|
set-exhaustion: func (exhaustion: f32);
|
||||||
|
/// Returns the player's current absorption amount.
|
||||||
get-absorption: func () -> f32;
|
get-absorption: func () -> f32;
|
||||||
|
/// Sets the player's current absorption amount.
|
||||||
set-absorption: func (absorption: f32);
|
set-absorption: func (absorption: f32);
|
||||||
|
|
||||||
|
/// Returns the player's total experience level.
|
||||||
get-experience-level: func() -> s32;
|
get-experience-level: func() -> s32;
|
||||||
|
/// Returns the player's progress toward the next level (0.0 to 1.0).
|
||||||
get-experience-progress: func() -> f32;
|
get-experience-progress: func() -> f32;
|
||||||
|
/// Returns the total experience points the player has.
|
||||||
get-experience-points: func() -> s32;
|
get-experience-points: func() -> s32;
|
||||||
|
/// Sets the player's total experience level.
|
||||||
set-experience-level: func(level: s32);
|
set-experience-level: func(level: s32);
|
||||||
|
/// Sets the player's progress toward the next level.
|
||||||
set-experience-progress: func(progress: f32);
|
set-experience-progress: func(progress: f32);
|
||||||
|
/// Sets the player's total experience points.
|
||||||
set-experience-points: func(points: s32);
|
set-experience-points: func(points: s32);
|
||||||
|
/// Adds experience levels to the player.
|
||||||
add-experience-levels: func(levels: s32);
|
add-experience-levels: func(levels: s32);
|
||||||
|
/// Adds experience points to the player.
|
||||||
add-experience-points: func(points: s32);
|
add-experience-points: func(points: s32);
|
||||||
|
|
||||||
|
/// Returns whether the player is currently sneaking.
|
||||||
is-sneaking: func() -> bool;
|
is-sneaking: func() -> bool;
|
||||||
|
/// Sets whether the player is currently sneaking.
|
||||||
set-sneaking: func(sneaking: bool);
|
set-sneaking: func(sneaking: bool);
|
||||||
|
/// Returns whether the player is currently sprinting.
|
||||||
is-sprinting: func() -> bool;
|
is-sprinting: func() -> bool;
|
||||||
|
/// Sets whether the player is currently sprinting.
|
||||||
set-sprinting: func(sprinting: bool);
|
set-sprinting: func(sprinting: bool);
|
||||||
|
/// Returns whether the player is currently on the ground.
|
||||||
is-on-ground: func() -> bool;
|
is-on-ground: func() -> bool;
|
||||||
|
/// Returns whether the player is currently flying.
|
||||||
is-flying: func() -> bool;
|
is-flying: func() -> bool;
|
||||||
|
/// Sets whether the player should be flying.
|
||||||
set-flying: func(flying: bool);
|
set-flying: func(flying: bool);
|
||||||
|
|
||||||
|
/// Returns the player's current abilities.
|
||||||
get-abilities: func() -> player-abilities;
|
get-abilities: func() -> player-abilities;
|
||||||
|
/// Updates the player's abilities.
|
||||||
set-abilities: func(abilities: player-abilities);
|
set-abilities: func(abilities: player-abilities);
|
||||||
|
|
||||||
|
/// Returns the player's IP address.
|
||||||
get-ip: func() -> string;
|
get-ip: func() -> string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
package pumpkin:plugin;
|
||||||
|
|
||||||
world plugin {
|
world plugin {
|
||||||
@ -6,22 +9,53 @@ world plugin {
|
|||||||
use server.{server as server-instance};
|
use server.{server as server-instance};
|
||||||
use command.{command-sender, consumed-args, command-error};
|
use command.{command-sender, consumed-args, command-error};
|
||||||
|
|
||||||
// This is what the host should provide to the plugin
|
// --- Host-provided imports ---
|
||||||
import logging;
|
|
||||||
import gui;
|
|
||||||
import scoreboard;
|
|
||||||
import server;
|
|
||||||
import text;
|
|
||||||
import command;
|
|
||||||
import context;
|
|
||||||
import i18n;
|
|
||||||
|
|
||||||
// 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();
|
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>;
|
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>;
|
export on-unload: func(context: context) -> result<_, string>;
|
||||||
|
|
||||||
|
/// Provides plugin metadata (name, version, etc.) to the host.
|
||||||
export metadata;
|
export metadata;
|
||||||
|
|
||||||
|
/// Common types used across the API.
|
||||||
export common;
|
export common;
|
||||||
|
|
||||||
|
/// Dispatches an event to the plugin for handling.
|
||||||
export handle-event: func(event-id: u32, server: server-instance, event: event) -> event;
|
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>;
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
24
pumpkin-plugin-wit/v0.1.0/scheduler.wit
Normal file
24
pumpkin-plugin-wit/v0.1.0/scheduler.wit
Normal 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);
|
||||||
|
}
|
||||||
@ -1,33 +1,41 @@
|
|||||||
interface server {
|
interface server {
|
||||||
use player.{player};
|
use player.{player};
|
||||||
|
|
||||||
|
/// Represents the difficulty setting of a Minecraft server.
|
||||||
enum difficulty {
|
enum difficulty {
|
||||||
|
/// Hostile mobs do not spawn. Players regain health automatically.
|
||||||
peaceful,
|
peaceful,
|
||||||
|
/// Hostile mobs deal less damage.
|
||||||
easy,
|
easy,
|
||||||
|
/// Standard Minecraft difficulty.
|
||||||
normal,
|
normal,
|
||||||
|
/// Hostile mobs deal more damage.
|
||||||
hard,
|
hard,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the global server instance.
|
||||||
resource server {
|
resource server {
|
||||||
/// Returns the current difficulty level of the server
|
/// Returns the current difficulty level of the server.
|
||||||
get-difficulty: func() -> difficulty;
|
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;
|
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;
|
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;
|
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>;
|
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>;
|
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>;
|
get-player-by-uuid: func(id: string) -> option<player>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,79 +3,167 @@ interface %world {
|
|||||||
use text.{text-component};
|
use text.{text-component};
|
||||||
use scoreboard.{scoreboard};
|
use scoreboard.{scoreboard};
|
||||||
|
|
||||||
|
/// Defines how a block reacts when pushed by a piston.
|
||||||
enum piston-behavior {
|
enum piston-behavior {
|
||||||
|
/// Normal behavior; the block can be pushed and pulled.
|
||||||
normal,
|
normal,
|
||||||
|
/// The block is destroyed when pushed.
|
||||||
destroy,
|
destroy,
|
||||||
|
/// The block cannot be pushed or pulled.
|
||||||
block,
|
block,
|
||||||
|
/// The block is ignored by pistons.
|
||||||
ignore,
|
ignore,
|
||||||
|
/// The block can only be pushed, not pulled.
|
||||||
push-only
|
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 {
|
flags block-flags {
|
||||||
|
/// Notifies neighbor blocks of the change.
|
||||||
notify-neighbors,
|
notify-neighbors,
|
||||||
|
/// Notifies client listeners of the change.
|
||||||
notify-listeners,
|
notify-listeners,
|
||||||
|
/// Forces the block state even if it's the same.
|
||||||
force-state,
|
force-state,
|
||||||
|
/// Skips dropping items when the block is removed.
|
||||||
skip-drops,
|
skip-drops,
|
||||||
|
/// Indicates the block was moved (e.g., by a piston).
|
||||||
moved,
|
moved,
|
||||||
|
/// Skips state replacement for redstone wires.
|
||||||
skip-redstone-wire-state-replacement,
|
skip-redstone-wire-state-replacement,
|
||||||
|
/// Skips the callback when a block entity is replaced.
|
||||||
skip-block-entity-replaced-callback,
|
skip-block-entity-replaced-callback,
|
||||||
|
/// Skips the callback when a block is added.
|
||||||
skip-block-added-callback,
|
skip-block-added-callback,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detailed information about a block's state.
|
||||||
record block-state {
|
record block-state {
|
||||||
|
/// The internal numerical ID of the block state.
|
||||||
id: u16,
|
id: u16,
|
||||||
|
/// The light level emitted by this block (0-15).
|
||||||
luminance: u8,
|
luminance: u8,
|
||||||
|
/// How much light this block blocks.
|
||||||
opacity: u8,
|
opacity: u8,
|
||||||
|
/// The hardness of the block, determining mining speed.
|
||||||
hardness: f32,
|
hardness: f32,
|
||||||
|
/// Whether this block is considered air.
|
||||||
is-air: bool,
|
is-air: bool,
|
||||||
|
/// Whether this block is a liquid.
|
||||||
is-liquid: bool,
|
is-liquid: bool,
|
||||||
|
/// Whether this block is a solid.
|
||||||
is-solid: bool,
|
is-solid: bool,
|
||||||
|
/// Whether this block occupies a full cube.
|
||||||
is-full-cube: bool,
|
is-full-cube: bool,
|
||||||
|
/// How this block behaves when pushed by a piston.
|
||||||
piston-behavior: piston-behavior,
|
piston-behavior: piston-behavior,
|
||||||
|
/// Whether this block receives random ticks.
|
||||||
has-random-ticks: bool,
|
has-random-ticks: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A simple wrapper for a block state ID.
|
||||||
record block-state-id {
|
record block-state-id {
|
||||||
id: u16,
|
id: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A position in the world grid.
|
||||||
record block-pos {
|
record block-pos {
|
||||||
x: s32,
|
x: s32,
|
||||||
y: s32,
|
y: s32,
|
||||||
z: s32,
|
z: s32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a Minecraft world (dimension).
|
||||||
resource %world {
|
resource %world {
|
||||||
|
/// Returns the unique identifier of this world.
|
||||||
get-id: func() -> string;
|
get-id: func() -> string;
|
||||||
|
|
||||||
|
/// Gets the block state ID at the specified position.
|
||||||
get-block-state-id: func(pos: block-pos) -> u16;
|
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;
|
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);
|
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;
|
get-time-of-day: func() -> u64;
|
||||||
|
|
||||||
|
/// Sets the current time of day in ticks.
|
||||||
set-time-of-day: func(time: u64);
|
set-time-of-day: func(time: u64);
|
||||||
|
|
||||||
|
/// Returns the total age of the world in ticks.
|
||||||
get-world-age: func() -> u64;
|
get-world-age: func() -> u64;
|
||||||
|
|
||||||
|
/// Returns the dimension type (e.g., "minecraft:overworld").
|
||||||
get-dimension: func() -> string;
|
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;
|
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;
|
get-motion-blocking-height: func(x: s32, z: s32) -> s32;
|
||||||
|
|
||||||
|
/// Returns whether it is currently raining or snowing.
|
||||||
is-raining: func() -> bool;
|
is-raining: func() -> bool;
|
||||||
|
|
||||||
|
/// Sets whether it should be raining or snowing.
|
||||||
set-raining: func(raining: bool);
|
set-raining: func(raining: bool);
|
||||||
|
|
||||||
|
/// Returns whether there is currently a thunderstorm.
|
||||||
is-thundering: func() -> bool;
|
is-thundering: func() -> bool;
|
||||||
|
|
||||||
|
/// Sets whether there should be a thunderstorm.
|
||||||
set-thundering: func(thundering: bool);
|
set-thundering: func(thundering: bool);
|
||||||
|
|
||||||
|
/// Broadcasts a system message to all players in this world.
|
||||||
broadcast-system-message: func(message: text-component, overlay: bool);
|
broadcast-system-message: func(message: text-component, overlay: bool);
|
||||||
|
|
||||||
|
/// Returns the scoreboard associated with this world.
|
||||||
get-scoreboard: func() -> scoreboard;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -182,6 +182,16 @@ impl WasmPlugin {
|
|||||||
) -> Result<Result<(), String>, wasmtime::Error> {
|
) -> Result<Result<(), String>, wasmtime::Error> {
|
||||||
let mut store = self.store.lock().await;
|
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 {
|
match self.plugin_instance {
|
||||||
PluginInstance::V0_1_0(ref plugin) => {
|
PluginInstance::V0_1_0(ref plugin) => {
|
||||||
let context = store.data_mut().add_context(context)?;
|
let context = store.data_mut().add_context(context)?;
|
||||||
|
|||||||
@ -16,6 +16,7 @@ pub mod i18n;
|
|||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod permission;
|
pub mod permission;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
pub mod scheduler;
|
||||||
pub mod scoreboard;
|
pub mod scoreboard;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|||||||
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ use crate::plugin::loader::wasm::wasm_host::{
|
|||||||
state::{PluginHostState, TextComponentResource, WorldResource},
|
state::{PluginHostState, TextComponentResource, WorldResource},
|
||||||
wit::v0_1_0::pumpkin::{self, plugin::world::World},
|
wit::v0_1_0::pumpkin::{self, plugin::world::World},
|
||||||
};
|
};
|
||||||
|
use crate::world::explosion::Explosion;
|
||||||
|
|
||||||
// --- Trapping Helpers ---
|
// --- Trapping Helpers ---
|
||||||
impl PluginHostState {
|
impl PluginHostState {
|
||||||
@ -228,6 +229,120 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
|
|||||||
self.add_scoreboard(world_provider)
|
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<()> {
|
async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> {
|
||||||
self.resource_table
|
self.resource_table
|
||||||
.delete::<WorldResource>(Resource::new_own(rep.rep()))
|
.delete::<WorldResource>(Resource::new_own(rep.rep()))
|
||||||
|
|||||||
@ -51,13 +51,15 @@ use tokio_util::task::TaskTracker;
|
|||||||
|
|
||||||
mod connection_cache;
|
mod connection_cache;
|
||||||
mod key_store;
|
mod key_store;
|
||||||
|
pub mod scheduler;
|
||||||
pub mod seasonal_events;
|
pub mod seasonal_events;
|
||||||
pub mod tick_rate_manager;
|
pub mod tick_rate_manager;
|
||||||
pub mod ticker;
|
pub mod ticker;
|
||||||
|
|
||||||
use super::command::args::entities::{
|
use crate::command::args::entities::{
|
||||||
EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition,
|
EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition,
|
||||||
};
|
};
|
||||||
|
use crate::server::scheduler::TaskScheduler;
|
||||||
|
|
||||||
/// Represents a Minecraft server instance.
|
/// Represents a Minecraft server instance.
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
@ -115,6 +117,8 @@ pub struct Server {
|
|||||||
pub server_guid: u64,
|
pub server_guid: u64,
|
||||||
/// Player idle timeout in minutes (0 = disabled)
|
/// Player idle timeout in minutes (0 = disabled)
|
||||||
pub player_idle_timeout: AtomicI32,
|
pub player_idle_timeout: AtomicI32,
|
||||||
|
/// Manages scheduled tasks (e.g. from plugins)
|
||||||
|
pub task_scheduler: Arc<TaskScheduler>,
|
||||||
tasks: TaskTracker,
|
tasks: TaskTracker,
|
||||||
|
|
||||||
// world stuff which maybe should be put into a struct
|
// world stuff which maybe should be put into a struct
|
||||||
@ -240,6 +244,7 @@ impl Server {
|
|||||||
aggregated_tick_times_nanos: AtomicI64::new(0),
|
aggregated_tick_times_nanos: AtomicI64::new(0),
|
||||||
tick_count: AtomicI32::new(0),
|
tick_count: AtomicI32::new(0),
|
||||||
tasks: TaskTracker::new(),
|
tasks: TaskTracker::new(),
|
||||||
|
task_scheduler: Arc::new(TaskScheduler::new()),
|
||||||
server_guid: rand::random(),
|
server_guid: rand::random(),
|
||||||
player_idle_timeout: AtomicI32::new(0),
|
player_idle_timeout: AtomicI32::new(0),
|
||||||
mojang_public_keys: ArcSwap::from_pointee(Vec::new()),
|
mojang_public_keys: ArcSwap::from_pointee(Vec::new()),
|
||||||
@ -733,6 +738,8 @@ impl Server {
|
|||||||
pub async fn tick_worlds(self: &Arc<Self>) {
|
pub async fn tick_worlds(self: &Arc<Self>) {
|
||||||
let mut set = JoinSet::new();
|
let mut set = JoinSet::new();
|
||||||
|
|
||||||
|
self.task_scheduler.tick(self).await;
|
||||||
|
|
||||||
for world in self.worlds.load().iter() {
|
for world in self.worlds.load().iter() {
|
||||||
let world = world.clone();
|
let world = world.clone();
|
||||||
let server = self.clone();
|
let server = self.clone();
|
||||||
|
|||||||
165
pumpkin/src/server/scheduler.rs
Normal file
165
pumpkin/src/server/scheduler.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user