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}