Skip to main content

basalt_api/context/
mod.rs

1//! Event dispatch context, trait definitions, and response queue.
2//!
3//! The [`Context`] trait provides sub-contexts for different domains:
4//! [`PlayerContext`], [`ChatContext`], [`WorldContext`], [`EntityContext`],
5//! and [`ContainerContext`]. Plugins access them via `ctx.player()`,
6//! `ctx.chat()`, etc.
7//!
8//! The concrete `ServerContext` implementation lives in `basalt-server`.
9
10pub mod response;
11
12pub use response::Response;
13pub use response::ResponseQueue;
14
15use crate::recipes::RecipeId;
16use basalt_types::{TextComponent, Uuid};
17
18use crate::broadcast::BroadcastMessage;
19use crate::gamemode::Gamemode;
20use crate::logger::PluginLogger;
21
22// ── Sub-context traits ─────────────────────��─────────────────────────
23
24/// Why a recipe was unlocked for a player.
25///
26/// Surfaced in [`RecipeContext::unlock`] and on the
27/// `RecipeUnlockedEvent` so plugins can branch on the source. For
28/// example, an analytics plugin records `AutoDiscovered` differently
29/// from `Manual` admin grants; a tutorial plugin only triggers on
30/// `InitialJoin`.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum UnlockReason {
33    /// The player crafted (or otherwise encountered) the recipe and
34    /// the server auto-granted it.
35    AutoDiscovered,
36    /// A plugin or admin command granted the recipe.
37    Manual,
38    /// Granted as part of the initial recipe set when the player
39    /// joined (starter recipes).
40    InitialJoin,
41}
42
43/// Player identity and state.
44pub trait PlayerContext {
45    /// Returns the UUID of the player who triggered this action.
46    fn uuid(&self) -> Uuid;
47    /// Returns the entity ID of the player.
48    fn entity_id(&self) -> i32;
49    /// Returns the username of the player.
50    fn username(&self) -> &str;
51    /// Returns the player's current yaw rotation (horizontal, degrees).
52    fn yaw(&self) -> f32;
53    /// Returns the player's current pitch rotation (vertical, degrees).
54    fn pitch(&self) -> f32;
55    /// Returns the player's current world position. Captured at
56    /// context-construction time — stale by the next tick.
57    fn position(&self) -> (f64, f64, f64);
58    /// Teleports the current player to the given coordinates.
59    fn teleport(&self, x: f64, y: f64, z: f64, yaw: f32, pitch: f32);
60    /// Changes the current player's gamemode.
61    fn set_gamemode(&self, mode: Gamemode);
62    /// Returns a list of (name, description) for all registered commands.
63    fn registered_commands(&self) -> Vec<(String, String)>;
64}
65
66/// Chat and messaging.
67pub trait ChatContext {
68    /// Sends a plain text message to the current player.
69    fn send(&self, text: &str);
70    /// Sends a styled message to the current player.
71    fn send_component(&self, component: &TextComponent);
72    /// Sends an action bar message to the current player.
73    fn action_bar(&self, text: &str);
74    /// Broadcasts a plain text message to ALL connected players.
75    fn broadcast(&self, text: &str);
76    /// Broadcasts a styled message to ALL connected players.
77    fn broadcast_component(&self, component: &TextComponent);
78}
79
80/// Per-dispatch context for world-related operations.
81///
82/// Extends [`WorldHandle`](crate::world::handle::WorldHandle) with
83/// methods that queue deferred responses (chunk streaming, block
84/// acknowledgements, block-entity destruction events). The pure
85/// world ops (`get_block`, `set_block`, `check_overlap`, ...) come
86/// from `WorldHandle` and are not redeclared here.
87///
88/// Persistence is split between the two traits:
89/// [`WorldHandle::persist_chunk`](crate::world::handle::WorldHandle::persist_chunk)
90/// is synchronous, while [`queue_persist_chunk`](Self::queue_persist_chunk)
91/// queues a `Response::PersistChunk` for async I/O-thread routing.
92/// The distinct names prevent E0034 ambiguity on `&dyn WorldContext`.
93pub trait WorldContext: crate::world::handle::WorldHandle {
94    /// Sends a block action acknowledgement to the current player.
95    fn send_block_ack(&self, sequence: i32);
96
97    /// Streams chunks around the given chunk coordinates.
98    fn stream_chunks(&self, cx: i32, cz: i32);
99
100    /// Schedules a chunk for asynchronous persistence on the I/O thread.
101    ///
102    /// Named `queue_persist_chunk` to avoid ambiguity with
103    /// [`WorldHandle::persist_chunk`](crate::world::handle::WorldHandle::persist_chunk)
104    /// (synchronous, calls `World::persist_chunk` directly). This
105    /// version queues a `Response::PersistChunk` for the game loop to
106    /// route to the I/O thread asynchronously.
107    fn queue_persist_chunk(&self, cx: i32, cz: i32);
108
109    /// Removes a block entity at the given position and fires a
110    /// `BlockEntityDestroyedEvent` carrying the last state.
111    ///
112    /// No-op if no block entity exists at the position. Plugins use
113    /// this from a `BlockBrokenEvent` Post handler to drive the
114    /// destroy → drop-items chain through the event pipeline.
115    fn destroy_block_entity(&self, x: i32, y: i32, z: i32);
116}
117
118/// Entity management: spawn, despawn, broadcast.
119pub trait EntityContext {
120    /// Spawns a dropped item entity at the given block coordinates.
121    fn spawn_dropped_item(&self, x: i32, y: i32, z: i32, item_id: i32, count: i32);
122
123    /// Broadcasts a block change to all connected players.
124    fn broadcast_block_change(&self, x: i32, y: i32, z: i32, block_state: i32);
125
126    /// Broadcasts an entity movement to all connected players.
127    #[allow(clippy::too_many_arguments)]
128    fn broadcast_entity_moved(
129        &self,
130        entity_id: i32,
131        x: f64,
132        y: f64,
133        z: f64,
134        yaw: f32,
135        pitch: f32,
136        on_ground: bool,
137    );
138
139    /// Broadcasts that the current player has joined the server.
140    ///
141    /// The server constructs the broadcast payload (including skin data)
142    /// from the context's player state.
143    fn broadcast_player_joined(&self);
144
145    /// Broadcasts that the current player has left the server.
146    fn broadcast_player_left(&self);
147
148    /// Sends a raw broadcast message to all connected players.
149    ///
150    /// Prefer the typed broadcast methods when possible. This method
151    /// is for server-internal broadcasts that don't have typed wrappers.
152    fn broadcast_raw(&self, msg: BroadcastMessage);
153
154    /// Broadcasts a `BlockAction` packet to all connected players.
155    ///
156    /// Used for state-change animations driven by plugins (chest
157    /// lid open/close, door swing, note-block pitch chime, etc.).
158    /// The meaning of `action_id` and `action_param` is block-specific
159    /// — see the wiki for the full table.
160    fn broadcast_block_action(
161        &self,
162        x: i32,
163        y: i32,
164        z: i32,
165        action_id: u8,
166        action_param: u8,
167        block_id: i32,
168    );
169}
170
171/// Container interaction: chests, crafting tables, custom windows.
172pub trait ContainerContext {
173    /// Opens a chest container at the given position for the current player.
174    fn open_chest(&self, x: i32, y: i32, z: i32);
175    /// Opens a crafting table window at the given position for the current player.
176    fn open_crafting_table(&self, x: i32, y: i32, z: i32);
177
178    /// Opens a custom container window for the current player.
179    ///
180    /// Takes a reference so the `Container` can be stored in a static,
181    /// cloned across calls, or shared — opening doesn't consume it.
182    ///
183    /// # Example
184    /// ```ignore
185    /// static SHOP: LazyLock<Container> = LazyLock::new(|| {
186    ///     Container::builder()
187    ///         .inventory_type(InventoryType::Generic9x6)
188    ///         .title("Shop")
189    ///         .build()
190    /// });
191    /// ctx.containers().open(&SHOP);
192    /// ```
193    fn open(&self, container: &crate::container::Container);
194
195    /// Notifies every other player viewing the same block-backed
196    /// container that a slot changed.
197    ///
198    /// Sends `SetContainerSlot` to all players whose `OpenContainer`
199    /// component points at `(x, y, z)`, **excluding** the current
200    /// player. Used by `ContainerPlugin` from the
201    /// `ContainerSlotChangedEvent` handler to keep co-viewers in sync.
202    /// No-op for virtual containers (they are per-player).
203    fn notify_viewers(&self, x: i32, y: i32, z: i32, slot_index: i16, item: basalt_types::Slot);
204}
205
206/// Per-player recipe-book state.
207///
208/// Plugins use this to grant or revoke recipes for the current player
209/// — the dispatch context's player. Mutations queue a deferred
210/// response that the game loop translates into the appropriate S2C
211/// recipe-book packet and dispatches `RecipeUnlockedEvent` /
212/// `RecipeLockedEvent` after commit.
213///
214/// `has` and `unlocked` are synchronous reads against the player's
215/// `KnownRecipes` component.
216pub trait RecipeContext {
217    /// Unlocks the recipe for the current player.
218    ///
219    /// Queues a `Recipe Book Add` packet, inserts into the player's
220    /// `KnownRecipes`, and dispatches `RecipeUnlockedEvent` at Post.
221    /// No-op if the recipe is already unlocked.
222    fn unlock(&self, id: &RecipeId, reason: UnlockReason);
223
224    /// Locks the recipe for the current player.
225    ///
226    /// Queues a `Recipe Book Remove` packet, removes from the player's
227    /// `KnownRecipes`, and dispatches `RecipeLockedEvent` at Post.
228    /// No-op if the recipe is not currently unlocked.
229    fn lock(&self, id: &RecipeId);
230
231    /// Returns true if the recipe is unlocked for the current player.
232    fn has(&self, id: &RecipeId) -> bool;
233
234    /// Returns a snapshot of every recipe id the player has unlocked.
235    ///
236    /// Allocates a new `Vec` — callers that only need to test for
237    /// membership should prefer [`has`](Self::has).
238    fn unlocked(&self) -> Vec<RecipeId>;
239}
240
241// ── Main Context trait ───────────────────────────────────────────────
242
243/// Execution context for commands and event handlers.
244///
245/// Provides sub-context accessors for domain-specific operations.
246/// Implemented by `ServerContext` (in basalt-server) and potentially
247/// `ConsoleContext` (server terminal) in the future.
248pub trait Context:
249    PlayerContext + ChatContext + WorldContext + EntityContext + ContainerContext + RecipeContext
250{
251    /// Returns a logger scoped to the current plugin.
252    fn logger(&self) -> PluginLogger;
253
254    /// Access player identity and state.
255    fn player(&self) -> &dyn PlayerContext;
256
257    /// Access chat and messaging.
258    fn chat(&self) -> &dyn ChatContext;
259
260    /// Access world, blocks, chunks, and persistence.
261    fn world_ctx(&self) -> &dyn WorldContext;
262
263    /// Access entity management.
264    fn entities(&self) -> &dyn EntityContext;
265
266    /// Access container interaction.
267    fn containers(&self) -> &dyn ContainerContext;
268
269    /// Access the current player's recipe-book state.
270    fn recipes(&self) -> &dyn RecipeContext;
271}