1use crate::command::{Arg, CommandArg, CommandArgs, Validation};
8use crate::context::Context;
9use crate::events::{BusKind, Event, EventBus, EventRouting, Stage};
10
11pub trait Plugin: Send + Sync + 'static {
13 fn metadata(&self) -> PluginMetadata;
15
16 fn on_enable(&self, registrar: &mut PluginRegistrar);
19
20 fn on_disable(&self) {}
22}
23
24#[derive(Debug, Clone)]
26pub struct PluginMetadata {
27 pub name: &'static str,
29 pub version: &'static str,
31 pub author: Option<&'static str>,
33 pub dependencies: &'static [&'static str],
35}
36
37pub type CommandHandler = Box<dyn Fn(&CommandArgs, &dyn Context) + Send + Sync>;
39
40pub struct CommandEntry {
42 pub name: String,
44 pub description: String,
46 pub args: Vec<CommandArg>,
48 pub variants: Vec<Vec<CommandArg>>,
50 pub handler: CommandHandler,
52}
53
54pub struct PluginRegistrar<'a> {
66 instant_bus: &'a mut EventBus,
68 game_bus: &'a mut EventBus,
70 commands: &'a mut Vec<CommandEntry>,
72 systems: &'a mut Vec<crate::system::SystemDescriptor>,
74 world: std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
76 recipes: &'a mut dyn crate::recipes::RecipeRegistryHandle,
78 bootstrap_ctx: &'a dyn crate::context::Context,
83}
84
85impl<'a> PluginRegistrar<'a> {
86 #[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 pub fn world(&self) -> std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync> {
117 std::sync::Arc::clone(&self.world)
118 }
119
120 pub fn recipes(&mut self) -> crate::recipes::RecipeRegistrar<'_> {
135 crate::recipes::RecipeRegistrar::new(self.recipes, self.game_bus, self.bootstrap_ctx)
136 }
137
138 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 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 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
191pub struct PluginSystemBuilder<'r, 'a> {
196 registrar: &'r mut PluginRegistrar<'a>,
197 builder: crate::system::SystemBuilder,
198}
199
200impl<'r, 'a> PluginSystemBuilder<'r, 'a> {
201 pub fn phase(mut self, phase: crate::components::Phase) -> Self {
203 self.builder = self.builder.phase(phase);
204 self
205 }
206
207 pub fn every(mut self, every: u64) -> Self {
209 self.builder = self.builder.every(every);
210 self
211 }
212
213 pub fn reads<T: crate::components::Component>(mut self) -> Self {
215 self.builder = self.builder.reads::<T>();
216 self
217 }
218
219 pub fn writes<T: crate::components::Component>(mut self) -> Self {
221 self.builder = self.builder.writes::<T>();
222 self
223 }
224
225 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
232pub 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 pub fn description(mut self, desc: &str) -> Self {
244 self.description = desc.to_string();
245 self
246 }
247
248 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 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 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 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 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
303pub struct VariantBuilder {
305 args: Vec<CommandArg>,
306}
307
308impl VariantBuilder {
309 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 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); assert_eq!(commands[0].variants[1].len(), 3); }
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}