Skip to main content

basalt_api/
plugin.rs

1//! Plugin trait and registration API.
2//!
3//! Every server feature — built-in or external — implements the
4//! [`Plugin`] trait. Plugins register event handlers and commands
5//! during [`on_enable`](Plugin::on_enable).
6
7use crate::command::{Arg, CommandArg, CommandArgs, Validation};
8use crate::context::Context;
9use crate::events::{BusKind, Event, EventBus, EventRouting, Stage};
10
11/// A server plugin that registers event handlers and lifecycle hooks.
12pub trait Plugin: Send + Sync + 'static {
13    /// Returns the plugin's identity metadata.
14    fn metadata(&self) -> PluginMetadata;
15
16    /// Called when the plugin is enabled. Register event handlers
17    /// and commands here.
18    fn on_enable(&self, registrar: &mut PluginRegistrar);
19
20    /// Called when the plugin is disabled (server shutdown).
21    fn on_disable(&self) {}
22}
23
24/// Identity metadata for a plugin.
25#[derive(Debug, Clone)]
26pub struct PluginMetadata {
27    /// Human-readable plugin name.
28    pub name: &'static str,
29    /// Semver version string.
30    pub version: &'static str,
31    /// Optional author name.
32    pub author: Option<&'static str>,
33    /// Plugin names that must be loaded before this plugin.
34    pub dependencies: &'static [&'static str],
35}
36
37/// Handler function type for commands with typed arguments.
38pub type CommandHandler = Box<dyn Fn(&CommandArgs, &dyn Context) + Send + Sync>;
39
40/// A registered command entry.
41pub struct CommandEntry {
42    /// Command name without the leading `/`.
43    pub name: String,
44    /// Short description for help listing.
45    pub description: String,
46    /// Single argument list (used when `variants` is empty).
47    pub args: Vec<CommandArg>,
48    /// Multiple argument variants for polymorphic commands.
49    pub variants: Vec<Vec<CommandArg>>,
50    /// The command handler function.
51    pub handler: CommandHandler,
52}
53
54/// Plugin registration interface for events, commands, and systems.
55///
56/// Holds mutable references to both the network and game event buses.
57/// Handler registration is routed automatically based on the event
58/// type's [`EventRouting::BUS`] constant — plugins do not specify
59/// which loop handles their events.
60///
61/// World and recipe fields are trait objects so that basalt-api does not
62/// depend on concrete runtime types at the struct level. Call sites
63/// coerce concrete types to the trait objects when constructing the
64/// registrar.
65pub struct PluginRegistrar<'a> {
66    /// Event bus for the network loop (movement, chat, commands).
67    instant_bus: &'a mut EventBus,
68    /// Event bus for the game loop (blocks, world mutations).
69    game_bus: &'a mut EventBus,
70    /// Collected command entries.
71    commands: &'a mut Vec<CommandEntry>,
72    /// Collected system descriptors.
73    systems: &'a mut Vec<crate::system::SystemDescriptor>,
74    /// Shared world handle, available to all plugins.
75    world: std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
76    /// Mutable recipe registry for plugin customisation.
77    recipes: &'a mut dyn crate::recipes::RecipeRegistryHandle,
78    /// Stub dispatch context for system-level events fired during
79    /// plugin loading (e.g. recipe registry lifecycle). The context
80    /// carries `PlayerInfo::stub()` — handlers must rely on the event
81    /// payload, not `ctx.player()`.
82    bootstrap_ctx: &'a dyn crate::context::Context,
83}
84
85impl<'a> PluginRegistrar<'a> {
86    /// Creates a new registrar with dual event buses and recipe registry.
87    ///
88    /// `bootstrap_ctx` is a stub context used only to dispatch
89    /// system-level events (today: the recipe registry lifecycle) that
90    /// fire before any player exists.
91    #[allow(clippy::too_many_arguments)]
92    pub fn new(
93        instant_bus: &'a mut EventBus,
94        game_bus: &'a mut EventBus,
95        commands: &'a mut Vec<CommandEntry>,
96        systems: &'a mut Vec<crate::system::SystemDescriptor>,
97        world: std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
98        recipes: &'a mut dyn crate::recipes::RecipeRegistryHandle,
99        bootstrap_ctx: &'a dyn crate::context::Context,
100    ) -> Self {
101        Self {
102            instant_bus,
103            game_bus,
104            commands,
105            systems,
106            world,
107            recipes,
108            bootstrap_ctx,
109        }
110    }
111
112    /// Returns a shared reference to the world.
113    ///
114    /// Available to all plugins — use this to capture the world
115    /// in system closures for block access, collision checks, etc.
116    pub fn world(&self) -> std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync> {
117        std::sync::Arc::clone(&self.world)
118    }
119
120    /// Returns a [`RecipeRegistrar`](crate::recipes::RecipeRegistrar)
121    /// that mutates the registry while dispatching the lifecycle
122    /// events on the game bus.
123    ///
124    /// Plugins call this from [`on_enable`](Plugin::on_enable) to add
125    /// or remove recipes. Mutations on the returned wrapper trigger
126    /// [`RecipeRegisterEvent`](crate::events::RecipeRegisterEvent),
127    /// [`RecipeRegisteredEvent`](crate::events::RecipeRegisteredEvent),
128    /// and [`RecipeUnregisteredEvent`](crate::events::RecipeUnregisteredEvent)
129    /// so other plugins can observe or veto changes.
130    ///
131    /// After every plugin's `on_enable` completes, the registry is
132    /// frozen behind `Arc<RecipeRegistry>` and shared immutably with
133    /// the game loop.
134    pub fn recipes(&mut self) -> crate::recipes::RecipeRegistrar<'_> {
135        crate::recipes::RecipeRegistrar::new(self.recipes, self.game_bus, self.bootstrap_ctx)
136    }
137
138    /// Registers an event handler on the correct bus.
139    ///
140    /// The target bus is determined at compile time by `E::BUS`:
141    /// - [`BusKind::Instant`] → network loop bus
142    /// - [`BusKind::Game`] → game loop bus
143    pub fn on<E>(
144        &mut self,
145        stage: Stage,
146        priority: i32,
147        handler: impl Fn(&mut E, &dyn crate::context::Context) + Send + Sync + 'static,
148    ) where
149        E: Event + EventRouting + 'static,
150    {
151        match E::BUS {
152            BusKind::Instant => self.instant_bus.on::<E>(stage, priority, handler),
153            BusKind::Game => self.game_bus.on::<E>(stage, priority, handler),
154        }
155    }
156
157    /// Starts building a system for the game loop.
158    ///
159    /// Returns a [`PluginSystemBuilder`] for fluent configuration of
160    /// phase, frequency, component access, and the system runner.
161    ///
162    /// # Example
163    ///
164    /// ```ignore
165    /// registrar.system("gravity")
166    ///     .phase(Phase::Simulate)
167    ///     .every(1)
168    ///     .writes::<Position>()
169    ///     .writes::<Velocity>()
170    ///     .run(|ctx| { /* apply gravity */ });
171    /// ```
172    pub fn system(&mut self, name: &str) -> PluginSystemBuilder<'_, 'a> {
173        PluginSystemBuilder {
174            registrar: self,
175            builder: crate::system::SystemBuilder::new(name),
176        }
177    }
178
179    /// Starts building a command with typed arguments.
180    pub fn command(&mut self, name: &str) -> CommandBuilder<'_, 'a> {
181        CommandBuilder {
182            registrar: self,
183            name: name.to_string(),
184            description: String::new(),
185            args: Vec::new(),
186            variants: Vec::new(),
187        }
188    }
189}
190
191/// Fluent builder for registering a system via a plugin.
192///
193/// Wraps [`SystemBuilder`](crate::system::SystemBuilder) and pushes the
194/// resulting descriptor into the registrar's system list on `run()`.
195pub struct PluginSystemBuilder<'r, 'a> {
196    registrar: &'r mut PluginRegistrar<'a>,
197    builder: crate::system::SystemBuilder,
198}
199
200impl<'r, 'a> PluginSystemBuilder<'r, 'a> {
201    /// Sets which tick phase this system runs in.
202    pub fn phase(mut self, phase: crate::components::Phase) -> Self {
203        self.builder = self.builder.phase(phase);
204        self
205    }
206
207    /// Sets the frequency divisor (runs when `tick % every == 0`).
208    pub fn every(mut self, every: u64) -> Self {
209        self.builder = self.builder.every(every);
210        self
211    }
212
213    /// Declares that this system reads a component type.
214    pub fn reads<T: crate::components::Component>(mut self) -> Self {
215        self.builder = self.builder.reads::<T>();
216        self
217    }
218
219    /// Declares that this system writes a component type.
220    pub fn writes<T: crate::components::Component>(mut self) -> Self {
221        self.builder = self.builder.writes::<T>();
222        self
223    }
224
225    /// Sets the system runner and registers the system.
226    pub fn run<F: FnMut(&mut dyn crate::system::SystemContext) + Send + 'static>(self, runner: F) {
227        let descriptor = self.builder.run(runner);
228        self.registrar.systems.push(descriptor);
229    }
230}
231
232/// Fluent builder for registering a command with typed arguments.
233pub struct CommandBuilder<'r, 'a> {
234    registrar: &'r mut PluginRegistrar<'a>,
235    name: String,
236    description: String,
237    args: Vec<CommandArg>,
238    variants: Vec<Vec<CommandArg>>,
239}
240
241impl<'r, 'a> CommandBuilder<'r, 'a> {
242    /// Sets the command description (shown in /help).
243    pub fn description(mut self, desc: &str) -> Self {
244        self.description = desc.to_string();
245        self
246    }
247
248    /// Adds a required argument with default validation.
249    pub fn arg(mut self, name: &str, arg_type: Arg) -> Self {
250        self.args.push(CommandArg {
251            name: name.to_string(),
252            arg_type,
253            validation: Validation::Auto,
254            required: true,
255        });
256        self
257    }
258
259    /// Adds a required argument with custom validation.
260    pub fn arg_with(mut self, name: &str, arg_type: Arg, validation: Validation) -> Self {
261        self.args.push(CommandArg {
262            name: name.to_string(),
263            arg_type,
264            validation,
265            required: true,
266        });
267        self
268    }
269
270    /// Adds an optional argument with default validation.
271    pub fn optional_arg(mut self, name: &str, arg_type: Arg) -> Self {
272        self.args.push(CommandArg {
273            name: name.to_string(),
274            arg_type,
275            validation: Validation::Auto,
276            required: false,
277        });
278        self
279    }
280
281    /// Adds a variant for polymorphic commands.
282    ///
283    /// Each variant is a separate argument list. The parser tries
284    /// variants in order and uses the first one that succeeds.
285    pub fn variant(mut self, build: impl FnOnce(VariantBuilder) -> VariantBuilder) -> Self {
286        let builder = build(VariantBuilder { args: Vec::new() });
287        self.variants.push(builder.args);
288        self
289    }
290
291    /// Sets the handler and registers the command.
292    pub fn handler(self, handler: impl Fn(&CommandArgs, &dyn Context) + Send + Sync + 'static) {
293        self.registrar.commands.push(CommandEntry {
294            name: self.name,
295            description: self.description,
296            args: self.args,
297            variants: self.variants,
298            handler: Box::new(handler),
299        });
300    }
301}
302
303/// Builder for a single variant of a polymorphic command.
304pub struct VariantBuilder {
305    args: Vec<CommandArg>,
306}
307
308impl VariantBuilder {
309    /// Adds a required argument to this variant.
310    pub fn arg(mut self, name: &str, arg_type: Arg) -> Self {
311        self.args.push(CommandArg {
312            name: name.to_string(),
313            arg_type,
314            validation: Validation::Auto,
315            required: true,
316        });
317        self
318    }
319
320    /// Adds a required argument with custom validation.
321    pub fn arg_with(mut self, name: &str, arg_type: Arg, validation: Validation) -> Self {
322        self.args.push(CommandArg {
323            name: name.to_string(),
324            arg_type,
325            validation,
326            required: true,
327        });
328        self
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::testing::NoopContext;
336
337    struct TestPlugin;
338
339    impl Plugin for TestPlugin {
340        fn metadata(&self) -> PluginMetadata {
341            PluginMetadata {
342                name: "test",
343                version: "0.1.0",
344                author: Some("Test"),
345                dependencies: &[],
346            }
347        }
348
349        fn on_enable(&self, _registrar: &mut PluginRegistrar) {}
350    }
351
352    #[test]
353    fn plugin_metadata() {
354        let meta = TestPlugin.metadata();
355        assert_eq!(meta.name, "test");
356    }
357
358    #[test]
359    fn plugin_on_disable_default_is_noop() {
360        TestPlugin.on_disable();
361    }
362
363    #[test]
364    fn registrar_routes_to_correct_bus() {
365        use crate::events::{BlockBrokenEvent, ChatMessageEvent};
366
367        let mut instant_bus = EventBus::new();
368        let mut game_bus = EventBus::new();
369        let mut commands = Vec::new();
370        let mut systems = Vec::new();
371        let mut recipes = crate::testing::MockRecipeRegistry::new();
372        let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
373            as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
374        let ctx = NoopContext;
375        {
376            let mut registrar = PluginRegistrar::new(
377                &mut instant_bus,
378                &mut game_bus,
379                &mut commands,
380                &mut systems,
381                std::sync::Arc::clone(&world)
382                    as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
383                &mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
384                &ctx as &dyn crate::context::Context,
385            );
386            registrar.on::<ChatMessageEvent>(Stage::Post, 0, |_event, _ctx| {});
387            registrar.on::<BlockBrokenEvent>(Stage::Process, 0, |_event, _ctx| {});
388        }
389        assert_eq!(instant_bus.handler_count(), 1);
390        assert_eq!(game_bus.handler_count(), 1);
391    }
392
393    #[test]
394    fn command_builder_with_args() {
395        let mut instant_bus = EventBus::new();
396        let mut game_bus = EventBus::new();
397        let mut commands = Vec::new();
398        let mut systems = Vec::new();
399        let mut recipes = crate::testing::MockRecipeRegistry::new();
400        let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
401            as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
402        let ctx = NoopContext;
403        {
404            let mut registrar = PluginRegistrar::new(
405                &mut instant_bus,
406                &mut game_bus,
407                &mut commands,
408                &mut systems,
409                std::sync::Arc::clone(&world)
410                    as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
411                &mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
412                &ctx as &dyn crate::context::Context,
413            );
414            registrar
415                .command("tp")
416                .description("Teleport")
417                .arg("x", Arg::Double)
418                .arg("y", Arg::Double)
419                .arg("z", Arg::Double)
420                .handler(|_args, _ctx| {});
421        }
422        assert_eq!(commands.len(), 1);
423        assert_eq!(commands[0].name, "tp");
424        assert_eq!(commands[0].args.len(), 3);
425        assert!(commands[0].variants.is_empty());
426    }
427
428    #[test]
429    fn command_builder_with_variants() {
430        let mut instant_bus = EventBus::new();
431        let mut game_bus = EventBus::new();
432        let mut commands = Vec::new();
433        let mut systems = Vec::new();
434        let mut recipes = crate::testing::MockRecipeRegistry::new();
435        let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
436            as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
437        let ctx = NoopContext;
438        {
439            let mut registrar = PluginRegistrar::new(
440                &mut instant_bus,
441                &mut game_bus,
442                &mut commands,
443                &mut systems,
444                std::sync::Arc::clone(&world)
445                    as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
446                &mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
447                &ctx as &dyn crate::context::Context,
448            );
449            registrar
450                .command("tp")
451                .description("Teleport")
452                .variant(|v| v.arg("destination", Arg::Player))
453                .variant(|v| {
454                    v.arg("x", Arg::Double)
455                        .arg("y", Arg::Double)
456                        .arg("z", Arg::Double)
457                })
458                .handler(|_args, _ctx| {});
459        }
460        assert_eq!(commands.len(), 1);
461        assert_eq!(commands[0].variants.len(), 2);
462        assert_eq!(commands[0].variants[0].len(), 1); // player
463        assert_eq!(commands[0].variants[1].len(), 3); // x y z
464    }
465
466    #[test]
467    fn command_no_args() {
468        let mut instant_bus = EventBus::new();
469        let mut game_bus = EventBus::new();
470        let mut commands = Vec::new();
471        let mut systems = Vec::new();
472        let mut recipes = crate::testing::MockRecipeRegistry::new();
473        let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
474            as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
475        let ctx = NoopContext;
476        {
477            let mut registrar = PluginRegistrar::new(
478                &mut instant_bus,
479                &mut game_bus,
480                &mut commands,
481                &mut systems,
482                std::sync::Arc::clone(&world)
483                    as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
484                &mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
485                &ctx as &dyn crate::context::Context,
486            );
487            registrar
488                .command("help")
489                .description("Show help")
490                .handler(|_args, _ctx| {});
491        }
492        assert_eq!(commands.len(), 1);
493        assert!(commands[0].args.is_empty());
494        assert!(commands[0].variants.is_empty());
495    }
496
497    #[test]
498    fn recipes_accessor_exposes_registrar_with_dispatch() {
499        use crate::events::RecipeRegisteredEvent;
500        use crate::recipes::{OwnedShapedRecipe, RecipeId};
501        use std::sync::Arc;
502        use std::sync::atomic::{AtomicU32, Ordering};
503
504        let mut instant_bus = EventBus::new();
505        let mut game_bus = EventBus::new();
506        let mut commands = Vec::new();
507        let mut systems = Vec::new();
508        let mut recipes = crate::testing::MockRecipeRegistry::new();
509        let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
510            as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
511        let ctx = NoopContext;
512
513        let post_seen = Arc::new(AtomicU32::new(0));
514        {
515            let p = Arc::clone(&post_seen);
516            game_bus.on::<RecipeRegisteredEvent>(Stage::Post, 0, move |_, _| {
517                p.fetch_add(1, Ordering::Relaxed);
518            });
519        }
520
521        {
522            let mut registrar = PluginRegistrar::new(
523                &mut instant_bus,
524                &mut game_bus,
525                &mut commands,
526                &mut systems,
527                std::sync::Arc::clone(&world)
528                    as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
529                &mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
530                &ctx as &dyn crate::context::Context,
531            );
532            let inserted = registrar.recipes().add_shaped(OwnedShapedRecipe {
533                id: RecipeId::new("plugin", "demo"),
534                width: 1,
535                height: 1,
536                pattern: vec![Some(1)],
537                result_id: 7,
538                result_count: 1,
539            });
540            assert!(inserted);
541        }
542
543        assert_eq!(post_seen.load(Ordering::Relaxed), 1);
544        assert_eq!(recipes.shaped_count(), 1);
545    }
546}