basalt-api 0.2.1

Public plugin API for the Basalt Minecraft server: traits, components, events, and the plugin registration system
Documentation
//! Event dispatch context, trait definitions, and response queue.
//!
//! The [`Context`] trait provides sub-contexts for different domains:
//! [`PlayerContext`], [`ChatContext`], [`WorldContext`], [`EntityContext`],
//! and [`ContainerContext`]. Plugins access them via `ctx.player()`,
//! `ctx.chat()`, etc.
//!
//! The concrete `ServerContext` implementation lives in `basalt-server`.

pub mod response;

pub use response::Response;
pub use response::ResponseQueue;

use crate::recipes::RecipeId;
use basalt_types::{TextComponent, Uuid};

use crate::broadcast::BroadcastMessage;
use crate::gamemode::Gamemode;
use crate::logger::PluginLogger;

// ── Sub-context traits ─────────────────────��─────────────────────────

/// Why a recipe was unlocked for a player.
///
/// Surfaced in [`RecipeContext::unlock`] and on the
/// `RecipeUnlockedEvent` so plugins can branch on the source. For
/// example, an analytics plugin records `AutoDiscovered` differently
/// from `Manual` admin grants; a tutorial plugin only triggers on
/// `InitialJoin`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnlockReason {
    /// The player crafted (or otherwise encountered) the recipe and
    /// the server auto-granted it.
    AutoDiscovered,
    /// A plugin or admin command granted the recipe.
    Manual,
    /// Granted as part of the initial recipe set when the player
    /// joined (starter recipes).
    InitialJoin,
}

/// Player identity and state.
pub trait PlayerContext {
    /// Returns the UUID of the player who triggered this action.
    fn uuid(&self) -> Uuid;
    /// Returns the entity ID of the player.
    fn entity_id(&self) -> i32;
    /// Returns the username of the player.
    fn username(&self) -> &str;
    /// Returns the player's current yaw rotation (horizontal, degrees).
    fn yaw(&self) -> f32;
    /// Returns the player's current pitch rotation (vertical, degrees).
    fn pitch(&self) -> f32;
    /// Returns the player's current world position. Captured at
    /// context-construction time — stale by the next tick.
    fn position(&self) -> (f64, f64, f64);
    /// Teleports the current player to the given coordinates.
    fn teleport(&self, x: f64, y: f64, z: f64, yaw: f32, pitch: f32);
    /// Changes the current player's gamemode.
    fn set_gamemode(&self, mode: Gamemode);
    /// Returns a list of (name, description) for all registered commands.
    fn registered_commands(&self) -> Vec<(String, String)>;
}

/// Chat and messaging.
pub trait ChatContext {
    /// Sends a plain text message to the current player.
    fn send(&self, text: &str);
    /// Sends a styled message to the current player.
    fn send_component(&self, component: &TextComponent);
    /// Sends an action bar message to the current player.
    fn action_bar(&self, text: &str);
    /// Broadcasts a plain text message to ALL connected players.
    fn broadcast(&self, text: &str);
    /// Broadcasts a styled message to ALL connected players.
    fn broadcast_component(&self, component: &TextComponent);
}

/// Per-dispatch context for world-related operations.
///
/// Extends [`WorldHandle`](crate::world::handle::WorldHandle) with
/// methods that queue deferred responses (chunk streaming, block
/// acknowledgements, block-entity destruction events). The pure
/// world ops (`get_block`, `set_block`, `check_overlap`, ...) come
/// from `WorldHandle` and are not redeclared here.
///
/// Persistence is split between the two traits:
/// [`WorldHandle::persist_chunk`](crate::world::handle::WorldHandle::persist_chunk)
/// is synchronous, while [`queue_persist_chunk`](Self::queue_persist_chunk)
/// queues a `Response::PersistChunk` for async I/O-thread routing.
/// The distinct names prevent E0034 ambiguity on `&dyn WorldContext`.
pub trait WorldContext: crate::world::handle::WorldHandle {
    /// Sends a block action acknowledgement to the current player.
    fn send_block_ack(&self, sequence: i32);

    /// Streams chunks around the given chunk coordinates.
    fn stream_chunks(&self, cx: i32, cz: i32);

    /// Schedules a chunk for asynchronous persistence on the I/O thread.
    ///
    /// Named `queue_persist_chunk` to avoid ambiguity with
    /// [`WorldHandle::persist_chunk`](crate::world::handle::WorldHandle::persist_chunk)
    /// (synchronous, calls `World::persist_chunk` directly). This
    /// version queues a `Response::PersistChunk` for the game loop to
    /// route to the I/O thread asynchronously.
    fn queue_persist_chunk(&self, cx: i32, cz: i32);

    /// Removes a block entity at the given position and fires a
    /// `BlockEntityDestroyedEvent` carrying the last state.
    ///
    /// No-op if no block entity exists at the position. Plugins use
    /// this from a `BlockBrokenEvent` Post handler to drive the
    /// destroy → drop-items chain through the event pipeline.
    fn destroy_block_entity(&self, x: i32, y: i32, z: i32);
}

/// Entity management: spawn, despawn, broadcast.
pub trait EntityContext {
    /// Spawns a dropped item entity at the given block coordinates.
    fn spawn_dropped_item(&self, x: i32, y: i32, z: i32, item_id: i32, count: i32);

    /// Broadcasts a block change to all connected players.
    fn broadcast_block_change(&self, x: i32, y: i32, z: i32, block_state: i32);

    /// Broadcasts an entity movement to all connected players.
    #[allow(clippy::too_many_arguments)]
    fn broadcast_entity_moved(
        &self,
        entity_id: i32,
        x: f64,
        y: f64,
        z: f64,
        yaw: f32,
        pitch: f32,
        on_ground: bool,
    );

    /// Broadcasts that the current player has joined the server.
    ///
    /// The server constructs the broadcast payload (including skin data)
    /// from the context's player state.
    fn broadcast_player_joined(&self);

    /// Broadcasts that the current player has left the server.
    fn broadcast_player_left(&self);

    /// Sends a raw broadcast message to all connected players.
    ///
    /// Prefer the typed broadcast methods when possible. This method
    /// is for server-internal broadcasts that don't have typed wrappers.
    fn broadcast_raw(&self, msg: BroadcastMessage);

    /// Broadcasts a `BlockAction` packet to all connected players.
    ///
    /// Used for state-change animations driven by plugins (chest
    /// lid open/close, door swing, note-block pitch chime, etc.).
    /// The meaning of `action_id` and `action_param` is block-specific
    /// — see the wiki for the full table.
    fn broadcast_block_action(
        &self,
        x: i32,
        y: i32,
        z: i32,
        action_id: u8,
        action_param: u8,
        block_id: i32,
    );
}

/// Container interaction: chests, crafting tables, custom windows.
pub trait ContainerContext {
    /// Opens a chest container at the given position for the current player.
    fn open_chest(&self, x: i32, y: i32, z: i32);
    /// Opens a crafting table window at the given position for the current player.
    fn open_crafting_table(&self, x: i32, y: i32, z: i32);

    /// Opens a custom container window for the current player.
    ///
    /// Takes a reference so the `Container` can be stored in a static,
    /// cloned across calls, or shared — opening doesn't consume it.
    ///
    /// # Example
    /// ```ignore
    /// static SHOP: LazyLock<Container> = LazyLock::new(|| {
    ///     Container::builder()
    ///         .inventory_type(InventoryType::Generic9x6)
    ///         .title("Shop")
    ///         .build()
    /// });
    /// ctx.containers().open(&SHOP);
    /// ```
    fn open(&self, container: &crate::container::Container);

    /// Notifies every other player viewing the same block-backed
    /// container that a slot changed.
    ///
    /// Sends `SetContainerSlot` to all players whose `OpenContainer`
    /// component points at `(x, y, z)`, **excluding** the current
    /// player. Used by `ContainerPlugin` from the
    /// `ContainerSlotChangedEvent` handler to keep co-viewers in sync.
    /// No-op for virtual containers (they are per-player).
    fn notify_viewers(&self, x: i32, y: i32, z: i32, slot_index: i16, item: basalt_types::Slot);
}

/// Per-player recipe-book state.
///
/// Plugins use this to grant or revoke recipes for the current player
/// — the dispatch context's player. Mutations queue a deferred
/// response that the game loop translates into the appropriate S2C
/// recipe-book packet and dispatches `RecipeUnlockedEvent` /
/// `RecipeLockedEvent` after commit.
///
/// `has` and `unlocked` are synchronous reads against the player's
/// `KnownRecipes` component.
pub trait RecipeContext {
    /// Unlocks the recipe for the current player.
    ///
    /// Queues a `Recipe Book Add` packet, inserts into the player's
    /// `KnownRecipes`, and dispatches `RecipeUnlockedEvent` at Post.
    /// No-op if the recipe is already unlocked.
    fn unlock(&self, id: &RecipeId, reason: UnlockReason);

    /// Locks the recipe for the current player.
    ///
    /// Queues a `Recipe Book Remove` packet, removes from the player's
    /// `KnownRecipes`, and dispatches `RecipeLockedEvent` at Post.
    /// No-op if the recipe is not currently unlocked.
    fn lock(&self, id: &RecipeId);

    /// Returns true if the recipe is unlocked for the current player.
    fn has(&self, id: &RecipeId) -> bool;

    /// Returns a snapshot of every recipe id the player has unlocked.
    ///
    /// Allocates a new `Vec` — callers that only need to test for
    /// membership should prefer [`has`](Self::has).
    fn unlocked(&self) -> Vec<RecipeId>;
}

// ── Main Context trait ───────────────────────────────────────────────

/// Execution context for commands and event handlers.
///
/// Provides sub-context accessors for domain-specific operations.
/// Implemented by `ServerContext` (in basalt-server) and potentially
/// `ConsoleContext` (server terminal) in the future.
pub trait Context:
    PlayerContext + ChatContext + WorldContext + EntityContext + ContainerContext + RecipeContext
{
    /// Returns a logger scoped to the current plugin.
    fn logger(&self) -> PluginLogger;

    /// Access player identity and state.
    fn player(&self) -> &dyn PlayerContext;

    /// Access chat and messaging.
    fn chat(&self) -> &dyn ChatContext;

    /// Access world, blocks, chunks, and persistence.
    fn world_ctx(&self) -> &dyn WorldContext;

    /// Access entity management.
    fn entities(&self) -> &dyn EntityContext;

    /// Access container interaction.
    fn containers(&self) -> &dyn ContainerContext;

    /// Access the current player's recipe-book state.
    fn recipes(&self) -> &dyn RecipeContext;
}