use crate::command::{Arg, CommandArg, CommandArgs, Validation};
use crate::context::Context;
use crate::events::{BusKind, Event, EventBus, EventRouting, Stage};
pub trait Plugin: Send + Sync + 'static {
fn metadata(&self) -> PluginMetadata;
fn on_enable(&self, registrar: &mut PluginRegistrar);
fn on_disable(&self) {}
}
#[derive(Debug, Clone)]
pub struct PluginMetadata {
pub name: &'static str,
pub version: &'static str,
pub author: Option<&'static str>,
pub dependencies: &'static [&'static str],
}
pub type CommandHandler = Box<dyn Fn(&CommandArgs, &dyn Context) + Send + Sync>;
pub struct CommandEntry {
pub name: String,
pub description: String,
pub args: Vec<CommandArg>,
pub variants: Vec<Vec<CommandArg>>,
pub handler: CommandHandler,
}
pub struct PluginRegistrar<'a> {
instant_bus: &'a mut EventBus,
game_bus: &'a mut EventBus,
commands: &'a mut Vec<CommandEntry>,
systems: &'a mut Vec<crate::system::SystemDescriptor>,
world: std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
recipes: &'a mut dyn crate::recipes::RecipeRegistryHandle,
bootstrap_ctx: &'a dyn crate::context::Context,
}
impl<'a> PluginRegistrar<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
instant_bus: &'a mut EventBus,
game_bus: &'a mut EventBus,
commands: &'a mut Vec<CommandEntry>,
systems: &'a mut Vec<crate::system::SystemDescriptor>,
world: std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
recipes: &'a mut dyn crate::recipes::RecipeRegistryHandle,
bootstrap_ctx: &'a dyn crate::context::Context,
) -> Self {
Self {
instant_bus,
game_bus,
commands,
systems,
world,
recipes,
bootstrap_ctx,
}
}
pub fn world(&self) -> std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync> {
std::sync::Arc::clone(&self.world)
}
pub fn recipes(&mut self) -> crate::recipes::RecipeRegistrar<'_> {
crate::recipes::RecipeRegistrar::new(self.recipes, self.game_bus, self.bootstrap_ctx)
}
pub fn on<E>(
&mut self,
stage: Stage,
priority: i32,
handler: impl Fn(&mut E, &dyn crate::context::Context) + Send + Sync + 'static,
) where
E: Event + EventRouting + 'static,
{
match E::BUS {
BusKind::Instant => self.instant_bus.on::<E>(stage, priority, handler),
BusKind::Game => self.game_bus.on::<E>(stage, priority, handler),
}
}
pub fn system(&mut self, name: &str) -> PluginSystemBuilder<'_, 'a> {
PluginSystemBuilder {
registrar: self,
builder: crate::system::SystemBuilder::new(name),
}
}
pub fn command(&mut self, name: &str) -> CommandBuilder<'_, 'a> {
CommandBuilder {
registrar: self,
name: name.to_string(),
description: String::new(),
args: Vec::new(),
variants: Vec::new(),
}
}
}
pub struct PluginSystemBuilder<'r, 'a> {
registrar: &'r mut PluginRegistrar<'a>,
builder: crate::system::SystemBuilder,
}
impl<'r, 'a> PluginSystemBuilder<'r, 'a> {
pub fn phase(mut self, phase: crate::components::Phase) -> Self {
self.builder = self.builder.phase(phase);
self
}
pub fn every(mut self, every: u64) -> Self {
self.builder = self.builder.every(every);
self
}
pub fn reads<T: crate::components::Component>(mut self) -> Self {
self.builder = self.builder.reads::<T>();
self
}
pub fn writes<T: crate::components::Component>(mut self) -> Self {
self.builder = self.builder.writes::<T>();
self
}
pub fn run<F: FnMut(&mut dyn crate::system::SystemContext) + Send + 'static>(self, runner: F) {
let descriptor = self.builder.run(runner);
self.registrar.systems.push(descriptor);
}
}
pub struct CommandBuilder<'r, 'a> {
registrar: &'r mut PluginRegistrar<'a>,
name: String,
description: String,
args: Vec<CommandArg>,
variants: Vec<Vec<CommandArg>>,
}
impl<'r, 'a> CommandBuilder<'r, 'a> {
pub fn description(mut self, desc: &str) -> Self {
self.description = desc.to_string();
self
}
pub fn arg(mut self, name: &str, arg_type: Arg) -> Self {
self.args.push(CommandArg {
name: name.to_string(),
arg_type,
validation: Validation::Auto,
required: true,
});
self
}
pub fn arg_with(mut self, name: &str, arg_type: Arg, validation: Validation) -> Self {
self.args.push(CommandArg {
name: name.to_string(),
arg_type,
validation,
required: true,
});
self
}
pub fn optional_arg(mut self, name: &str, arg_type: Arg) -> Self {
self.args.push(CommandArg {
name: name.to_string(),
arg_type,
validation: Validation::Auto,
required: false,
});
self
}
pub fn variant(mut self, build: impl FnOnce(VariantBuilder) -> VariantBuilder) -> Self {
let builder = build(VariantBuilder { args: Vec::new() });
self.variants.push(builder.args);
self
}
pub fn handler(self, handler: impl Fn(&CommandArgs, &dyn Context) + Send + Sync + 'static) {
self.registrar.commands.push(CommandEntry {
name: self.name,
description: self.description,
args: self.args,
variants: self.variants,
handler: Box::new(handler),
});
}
}
pub struct VariantBuilder {
args: Vec<CommandArg>,
}
impl VariantBuilder {
pub fn arg(mut self, name: &str, arg_type: Arg) -> Self {
self.args.push(CommandArg {
name: name.to_string(),
arg_type,
validation: Validation::Auto,
required: true,
});
self
}
pub fn arg_with(mut self, name: &str, arg_type: Arg, validation: Validation) -> Self {
self.args.push(CommandArg {
name: name.to_string(),
arg_type,
validation,
required: true,
});
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::NoopContext;
struct TestPlugin;
impl Plugin for TestPlugin {
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
name: "test",
version: "0.1.0",
author: Some("Test"),
dependencies: &[],
}
}
fn on_enable(&self, _registrar: &mut PluginRegistrar) {}
}
#[test]
fn plugin_metadata() {
let meta = TestPlugin.metadata();
assert_eq!(meta.name, "test");
}
#[test]
fn plugin_on_disable_default_is_noop() {
TestPlugin.on_disable();
}
#[test]
fn registrar_routes_to_correct_bus() {
use crate::events::{BlockBrokenEvent, ChatMessageEvent};
let mut instant_bus = EventBus::new();
let mut game_bus = EventBus::new();
let mut commands = Vec::new();
let mut systems = Vec::new();
let mut recipes = crate::testing::MockRecipeRegistry::new();
let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
let ctx = NoopContext;
{
let mut registrar = PluginRegistrar::new(
&mut instant_bus,
&mut game_bus,
&mut commands,
&mut systems,
std::sync::Arc::clone(&world)
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
&mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
&ctx as &dyn crate::context::Context,
);
registrar.on::<ChatMessageEvent>(Stage::Post, 0, |_event, _ctx| {});
registrar.on::<BlockBrokenEvent>(Stage::Process, 0, |_event, _ctx| {});
}
assert_eq!(instant_bus.handler_count(), 1);
assert_eq!(game_bus.handler_count(), 1);
}
#[test]
fn command_builder_with_args() {
let mut instant_bus = EventBus::new();
let mut game_bus = EventBus::new();
let mut commands = Vec::new();
let mut systems = Vec::new();
let mut recipes = crate::testing::MockRecipeRegistry::new();
let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
let ctx = NoopContext;
{
let mut registrar = PluginRegistrar::new(
&mut instant_bus,
&mut game_bus,
&mut commands,
&mut systems,
std::sync::Arc::clone(&world)
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
&mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
&ctx as &dyn crate::context::Context,
);
registrar
.command("tp")
.description("Teleport")
.arg("x", Arg::Double)
.arg("y", Arg::Double)
.arg("z", Arg::Double)
.handler(|_args, _ctx| {});
}
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].name, "tp");
assert_eq!(commands[0].args.len(), 3);
assert!(commands[0].variants.is_empty());
}
#[test]
fn command_builder_with_variants() {
let mut instant_bus = EventBus::new();
let mut game_bus = EventBus::new();
let mut commands = Vec::new();
let mut systems = Vec::new();
let mut recipes = crate::testing::MockRecipeRegistry::new();
let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
let ctx = NoopContext;
{
let mut registrar = PluginRegistrar::new(
&mut instant_bus,
&mut game_bus,
&mut commands,
&mut systems,
std::sync::Arc::clone(&world)
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
&mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
&ctx as &dyn crate::context::Context,
);
registrar
.command("tp")
.description("Teleport")
.variant(|v| v.arg("destination", Arg::Player))
.variant(|v| {
v.arg("x", Arg::Double)
.arg("y", Arg::Double)
.arg("z", Arg::Double)
})
.handler(|_args, _ctx| {});
}
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].variants.len(), 2);
assert_eq!(commands[0].variants[0].len(), 1); assert_eq!(commands[0].variants[1].len(), 3); }
#[test]
fn command_no_args() {
let mut instant_bus = EventBus::new();
let mut game_bus = EventBus::new();
let mut commands = Vec::new();
let mut systems = Vec::new();
let mut recipes = crate::testing::MockRecipeRegistry::new();
let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
let ctx = NoopContext;
{
let mut registrar = PluginRegistrar::new(
&mut instant_bus,
&mut game_bus,
&mut commands,
&mut systems,
std::sync::Arc::clone(&world)
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
&mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
&ctx as &dyn crate::context::Context,
);
registrar
.command("help")
.description("Show help")
.handler(|_args, _ctx| {});
}
assert_eq!(commands.len(), 1);
assert!(commands[0].args.is_empty());
assert!(commands[0].variants.is_empty());
}
#[test]
fn recipes_accessor_exposes_registrar_with_dispatch() {
use crate::events::RecipeRegisteredEvent;
use crate::recipes::{OwnedShapedRecipe, RecipeId};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let mut instant_bus = EventBus::new();
let mut game_bus = EventBus::new();
let mut commands = Vec::new();
let mut systems = Vec::new();
let mut recipes = crate::testing::MockRecipeRegistry::new();
let world = std::sync::Arc::new(crate::testing::MockWorld::flat())
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>;
let ctx = NoopContext;
let post_seen = Arc::new(AtomicU32::new(0));
{
let p = Arc::clone(&post_seen);
game_bus.on::<RecipeRegisteredEvent>(Stage::Post, 0, move |_, _| {
p.fetch_add(1, Ordering::Relaxed);
});
}
{
let mut registrar = PluginRegistrar::new(
&mut instant_bus,
&mut game_bus,
&mut commands,
&mut systems,
std::sync::Arc::clone(&world)
as std::sync::Arc<dyn crate::world::handle::WorldHandle + Send + Sync>,
&mut recipes as &mut dyn crate::recipes::RecipeRegistryHandle,
&ctx as &dyn crate::context::Context,
);
let inserted = registrar.recipes().add_shaped(OwnedShapedRecipe {
id: RecipeId::new("plugin", "demo"),
width: 1,
height: 1,
pattern: vec![Some(1)],
result_id: 7,
result_count: 1,
});
assert!(inserted);
}
assert_eq!(post_seen.load(Ordering::Relaxed), 1);
assert_eq!(recipes.shaped_count(), 1);
}
}